Skip to content

Stylo hacking guide

Emilio Cobos Álvarez edited this page Sep 12, 2017 · 16 revisions

Guide for hacking on Stylo

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).

Building Stylo

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.

Overview

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.

Generated files

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.

Implementing a simple keyword property

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.

Implementing a predefined type property

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.

Implementing a general property (Advanced)

TODO. Ask in IRC if you need help with this.

Testing changes

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.

Clone this wiki locally