Stylo hacking guide
Stylo is an attempt at integrating Servo's styling system into Firefox (specifically, Firefox's rendering engine called Gecko). This means that things like CSS parsing, cascading, etc are all handled by Servo's code, in parallel (providing some nice performance wins).
This is a process involving a long build step and some large downloads, so it's recommended that you initiate this beforehand.
The Stylo code is in the mozilla-central Mercurial repo, but is not built by default.
First, clone the mozilla-central repo. It might be beneficial to clone servo as well. If you want to try the more complicated workflow, we
use git-cinnabar
(git clone hg::https://hg.mozilla.org/mozilla-central
)
with the mercurial repo (which lets you work with mercurial repos using git).
Create a mozconfig file if it doesn't already exist, and add the following line:
ac_add_options --enable-stylo
Set up the dependencies for servo. Also, run ./mach bootstrap
in your stylo clone to get the Gecko dependencies. Rust needs to be
installed as well; It is recommended to install it from rustup.rs with the stable
toolchain.
Once all the dependencies are sorted out, run ./mach build
in the stylo clone.
Once it builds, you should be able to run it with ./mach run --disable-e10s https://en.wikipedia.org
(or some other site)
Note: By default you get a non-optimized build with debug symbols, so expect it to be slow.
These options can be changed in the mozconfig
file if you need to.
The Stylo clone contains a copy of Servo in the servo/
directory. Any changes to that code will require making a pull request to
Servo on github, however it is also okay to make a pull request to stylo-flat instead.
Most stylo issues marked for newcomers will require implementing some CSS property and/or the glue code that copies it over to Gecko. This document will focus on the code involved in those tasks.
components/style
is where almost
all of this code lives. All file paths in this document will be relative to this folder.
Most properties live inside properties/longhand
.
("longhands" are CSS properties like background-color
, whereas
background
is a "shorthand" and can be used to specify many longhands at once). These files are
all Mako templates — Mako is a Python template library for code generation.
Each CSS property is implemented inside a <helpers:longhand>
tag. For example, here is the code for the column-width
property. It controls how the property value is stored, how to
parse/serialize it, and how to convert between the "specified" and "computed" forms.
properties/gecko.mako.rs
contains the code that copies the value for the properties over to the Gecko side. For example, the
code which handles column-width
is here.
There are some generated bindings in components/style/gecko/generated/
. If you think you need to touch those files, you don't.
Those files are there so Servo can build Stylo without pulling mozilla-central, but on normal mozilla-central builds, those files are generated at build time, and should be on obj/dist/rust_bindings/style/
. If you make changes to the servo repo that depend on those changes to build, you'll need to update the generated/
files on your PR, so that Servo CI passes.
A lot of the properties are just a choice of keywords. Let's take object-fit
as an example.
The code for it is here.
${helpers.single_keyword("object-fit", "fill contain cover none scale-down",
products="gecko", animatable=False)}
That's it! This will generate all the code needed to parse the keyword and handle its values. The
expanded code will look something like the code for column-width
above. This also generates the
code for copying the property over to Gecko.
Let's unpack this a bit. What it's saying is that object-fit
is a keyword property. It
can take one of fill | contain | cover | none | scale-down
as values. As the spec
and MDN page say, the default/initial value is fill
,
so fill
is the first one in the list. The order of the rest doesn't matter.
The spec/docs also say that its animation is "discrete", which means that if you try to animate this property
it will be the first value at t < 0.5
and the second at t > 0.5
. In Servo we use animatable=False
for this.
Now, Servo doesn't actually know how to handle this property in layout. We could add code for that,
but that can get pretty complicated. So in many cases we have properties with products="gecko"
,
meaning that they're only parsed inside Stylo. Most new properties you implement will be like that.
page-break-inside
is a slightly more complicated property. It's implemented here.
${helpers.single_keyword("page-break-inside",
"auto avoid",
products="gecko",
gecko_ffi_name="mBreakInside",
gecko_constant_prefix="NS_STYLE_PAGE_BREAK",
animatable=False)}
It's clear from the invocation that this is a gecko-only non-animatable property with the values "auto" and "avoid", where the default value is "auto".
But there are some extra gecko_
fields there. What do they mean?
To learn this, lets revisit object-fit
— Recall that the code for copying the property
over to Gecko is autogenerated. On the Gecko side, this is stored inside
nsStylePosition
,
inside mObjectFit
.
These values are NS_STYLE_OBJECT_FIT_*
constants defined here.
When generating the code for the object-fit
keyword, this was guessed by the code generation. It knew that object-fit
was probably stored inside a field with the name mObjectFit
, and its values probably mapped to NS_STYLE_OBJECT_FIT_
constants. So it generated code that mapped Servo-side object-fit
enum values to these constants, and stored them
inside mObjectFit
. (The style struct that it was stored in, nsStylePosition
, was known because of the file
object-fit
was defined in — each file in properties/longhands/
corresponds to a Gecko style struct.)
The generated code for copying over to Gecko looks like
#[allow(non_snake_case)]
pub fn set_object_fit(&mut self, v: longhands::object_fit::computed_value::T) {
use properties::longhands::object_fit::computed_value::T as Keyword;
let result = match v {
Keyword::fill => structs::NS_STYLE_OBJECT_FIT_FILL as u8,
Keyword::contain => structs::NS_STYLE_OBJECT_FIT_CONTAIN as u8,
Keyword::cover => structs::NS_STYLE_OBJECT_FIT_COVER as u8,
Keyword::none => structs::NS_STYLE_OBJECT_FIT_NONE as u8,
Keyword::scale_down => structs::NS_STYLE_OBJECT_FIT_SCALE_DOWN as u8,
};
self.gecko.mObjectFit = result;
}
#[allow(non_snake_case)]
pub fn copy_object_fit_from(&mut self, other: &Self) {
self.gecko.mObjectFit = other.gecko.mObjectFit;
}
However, page-break-inside
does not have guessable field names. It is stored in mBreakInside
(not mPageBreakInside
), using NS_STYLE_PAGE_BREAK_*
(not NS_STYLE_PAGE_BREAK_INSIDE_*
).
So, we find out the correct field name, and the correct prefix for the constants, and explicitly
specify them in the single_keyword()
call.
When working on one of these, if you can't find the style struct or field name, don't hesitate to ask. Usually the easy issue will mention these, but first try not specifying these things, and if it doesn't compile, only then look for the actual field name.
Once you have made these changes, build with ./mach build
, and run. Test out the property —
usually there are some testcases on the MDN documentation for the CSS property in question. Take screenshots of the testcases running in stylo.
Make a pull request. This can be done in two ways — one is to make one to the stylo-flat repository itself. Manishearth can review the PR there, and once done, graft it over to the main Servo repository.
Alternatively, you can make the pull request directly to Servo. To do this, make a single commit out of
your changes, and make a patch out of it using git format-patch @^ servo --relative=servo
. This will
create a .patch
file. In a Servo clone, run git am /path/to/patch
, and it should create a commit for
you containing the same changes. From there, make a pull request to the Servo repository. This
method is preferred, but it's okay to do it the other way too.
Be sure to include the screenshots in your pull request! (and a link to the code behind the testcase if it wasn't the MDN one). We have automated tests as well, but right now Stylo doesn't implement enough to get reliable test results, so the relevant reftests may fail due to unrelated issues.
Unlike the above properties, some properties take values instead of keywords. Many of those share a basic type in their implementation, and so they have some common code for their initial values, parsing, serialization and the gecko glue. For example, the background-color
longhand property takes color
. So do the other longhand properties like border-top-color
(or any other border-<side>-color
), text-decoration-color
, etc.
In such cases, we define the types along with their necessary methods in the modules inside values/specified
and values/computed
and use them to ease with writing the parsing/serialization code.
Now, background-color
takes only color
as input and nothing else. The necessary code for parsing color
as a specified value is implemented here, whereas for the computed value, it's been re-exported here. So, we can make use of this directly to write the longhand parsing and serialization code. Please read the properties hacking guide for more about this part.
The code for implementing background-color
is here.
${helpers.predefined_type("background-color", "CSSColor",
"::cssparser::Color::RGBA(::cssparser::RGBA { red: 0., green: 0., blue: 0., alpha: 0. }) /* transparent */",
animatable=True)}
Let's break this up. Firstly, predefined_type
is a helper method very similar to simple_keyword
- only difference is that it generates the code for properties that make use of predefined types for their values. The first argument says how the property is parsed (i.e., what the CSS parser should expect), the second one is the type to which its value should be parsed to. Now, in order for this to work, we should've already implemented the necessary traits for the type (which, as said above, has already been done for CSSColor
, and hence the name "predefined" type). The next argument is the initial value, followed by animatable
, which has the same meaning as explained in simple_keyword
method.
For easy issues, you often don't have to worry about all this, we'll mention it in the issue for you. But, this is useful to know if you plan to fiddle around the glue code.
In the gecko glue, we have a nice setup for auto-generating the value setting methods for these types. This will call the impl_color
defined up top, and it will generate a code something like this.
#[allow(unreachable_code)]
#[allow(non_snake_case)]
pub fn set_background_color(&mut self, v: longhands::background_color::computed_value::T) {
use cssparser::Color;
let result = match v {
Color::RGBA(rgba) => convert_rgba_to_nscolor(&rgba),
Color::CurrentColor => 0,
};
self.gecko.mBackgroundColor = result;
}
#[allow(non_snake_case)]
pub fn copy_background_color_from(&mut self, other: &Self) {
let color = other.gecko.mBackgroundColor;
self.gecko.mBackgroundColor = color;;
}
This makes some assumptions about the gecko side. We assume that these simple predefined types exist as a struct
field in gecko (with a camel case name, prefixed with "m"
, as in mBorderSpacing
for border-spacing
). But sometimes, this doesn't work out very well. For instance, let's take border-top-color
as an example. In Gecko, this is a linked list. So, we have to explicitly specify the node to which the border color has to be applied. In code generation, this is implemented by directly calling impl_color
with custom arguments.
We don't need to worry about the complicated code generated for this. The point is that if a struct field has a different name, it can always be specified in the method with the key, gecko_ffi_name=mFoobar
. This will also be mentioned in the easy bug.
TODO. Ask in IRC if you need help with this.
When your code is complete and you're ready to submit a PR, first make sure the following commands succeed:
-
/mach test-tidy
(checks some style guidelines) -
./mach build -d
(to make sure your code builds in Servo mode) -
./mach build-geckolib
(to make sure your code builds in Gecko mode) -
./mach test-unit -p style
(unit tests for the style crate) -
./mach test-stylo
(unit tests for Gecko integration)
See also Contributing for more general (not Stylo-specific) information on submitting PRs, and the stylo etherpad for details of how Stylo patches are merged into mozilla-central.