From d324173f6d2749830136f8928226e1c628f4cd11 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Wed, 7 Feb 2024 17:52:11 +0800 Subject: [PATCH 01/30] Servo initial downstream commit Any ancestors of this commit are from upstream mozilla-central, with some filtering and renaming. Our patches and sync tooling start here. The sync tooling has all been squashed into this commit, based on: https://github.com/servo/stylo/commits/64731e10dc8ef87ef52aa2fb9f988c3b2530f3a7 --- .github/workflows/main.yml | 51 +++++++++++ .../workflows/mirror-to-release-branch.yml | 26 ++++++ .github/workflows/sync-upstream.yml | 23 +++++ .gitignore | 5 ++ README.md | 85 +++++++++++++++++++ commit-from-merge.sh | 15 ++++ commit-from-squashed.sh | 14 +++ shell.nix | 6 ++ start-rebase.sh | 10 +++ style.paths | 8 ++ sync.sh | 43 ++++++++++ 11 files changed, 286 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/mirror-to-release-branch.yml create mode 100644 .github/workflows/sync-upstream.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100755 commit-from-merge.sh create mode 100755 commit-from-squashed.sh create mode 100644 shell.nix create mode 100755 start-rebase.sh create mode 100644 style.paths create mode 100755 sync.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..aafc8d0e84 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + workflow_dispatch: + merge_group: + types: [checks_requested] + + +jobs: + linux-debug: + name: Linux (Debug) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Run Tests + run: cargo build --features servo + env: + RUST_BACKTRACE: 1 + + linux-release: + name: Linux (Release) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Run Tests + run: cargo build --release --features servo + env: + RUST_BACKTRACE: 1 + + build-result: + name: Result + runs-on: ubuntu-latest + if: ${{ always() }} + needs: + - linux-debug + - linux-release + steps: + - name: Success + if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + run: exit 0 + - name: Failure + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + run: exit 1 + diff --git a/.github/workflows/mirror-to-release-branch.yml b/.github/workflows/mirror-to-release-branch.yml new file mode 100644 index 0000000000..c8593195da --- /dev/null +++ b/.github/workflows/mirror-to-release-branch.yml @@ -0,0 +1,26 @@ +name: 🪞 Mirror `main` +on: + push: + branches: + - main + +jobs: + mirror: + name: Mirror + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get branch name + id: branch-name + run: | + first_commit=$(git log --pretty=\%H --grep='Servo initial downstream commit') + upstream_base="$first_commit~" + echo BRANCH_NAME=$(git log -n1 --pretty='%as' $upstream_base) >> $GITHUB_OUTPUT + - uses: google/mirror-branch-action@v1.0 + name: Mirror to ${{ steps.branch-name.outputs.BRANCH_NAME }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + source: main + dest: ${{ steps.branch-name.outputs.BRANCH_NAME }} diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000000..992fe6d2ce --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,23 @@ +name: Sync upstream with mozilla-central + +on: + schedule: + - cron: '0 13 * * *' + workflow_dispatch: + +jobs: + sync: + name: Sync + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - uses: actions/cache@v3 + with: + path: _cache/upstream + key: upstream + - run: | + ./sync.sh _filtered + git fetch -f --progress ./_filtered master:upstream + git push -fu --progress origin upstream diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..fc3c2f9b3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/_cache/ +/_filtered/ +/target/ +/style/properties/__pycache__/ +Cargo.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000000..b280c0c4a7 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +Stylo +===== + +This repo contains Servo’s downstream fork of [Stylo](https://searchfox.org/mozilla-central/source/servo). + +The branches are as follows: + +- [`upstream`](https://github.com/servo/style/tree/upstream) has upstream mozilla-central filtered to the paths we care about ([style.paths](style.paths)), but is otherwise unmodified +- [`main`](https://github.com/servo/style/tree/ci) has our downstream patches, plus the scripts and workflows for syncing with mozilla-central, to be rebased onto `upstream` + +## Building Servo against your local Stylo + +Assuming your local `servo` and `stylo` directories are siblings, you can build `servo` against `stylo` by adding the following to `servo/Cargo.toml`: + +```toml +[patch."https://github.com/servo/stylo"] +selectors = { path = "../stylo/selectors" } +servo_arc = { path = "../stylo/servo_arc" } +stylo_atoms = { path = "../stylo/stylo_atoms" } +style = { path = "../stylo/style" } +stylo_config = { path = "../stylo/stylo_config" } +stylo_dom = { path = "../stylo/stylo_dom" } +style_malloc_size_of = { path = "../stylo/malloc_size_of", package = "malloc_size_of" } +style_traits = { path = "../stylo/style_traits" } +``` + +## Syncing `upstream` with mozilla-central + +Start by generating a filtered copy of mozilla-central. This will cache the raw mozilla-central in `_cache/upstream`, storing the result in `_filtered`: + +```sh +$ ./sync.sh _filtered +``` + +If `_filtered` already exists, you will need to delete it and try again: + +```sh +$ rm -Rf _filtered +``` + +Now overwrite our `upstream` with those commits and push: + +```sh +$ git fetch -f --progress ./_filtered master:upstream +$ git push -fu --progress origin upstream +``` + +## Rebasing `main` onto `upstream` + +Start by fetching `upstream` into your local repo: + +```sh +$ git fetch -f origin upstream:upstream +``` + +In general, the filtering process is deterministic, yielding the same commit hashes each time, so we can rebase normally: + +```sh +$ git rebase upstream +``` + +But if the filtering config changes or Mozilla moves to GitHub, the commit hashes on `upstream` may change. In this case, we need to tell git where the old upstream ends and our own commits start (notice the `~`): + +```sh +$ git log --pretty=\%H --grep='Servo initial downstream commit' +e62d7f0090941496e392e1dc91df103a38e3f488 + +$ git rebase --onto upstream e62d7f0090941496e392e1dc91df103a38e3f488~ +Successfully rebased and updated refs/heads/main. +``` + +`start-rebase.sh` takes care of this automatically, but you should still use `git rebase` for subsequent steps like `--continue` and `--abort`: + +```sh +$ ./start-rebase.sh upstream +$ ./start-rebase.sh upstream -i # interactive +$ git rebase --continue # not ./start-rebase.sh --continue +$ git rebase --abort # not ./start-rebase.sh --abort +``` + +Or if we aren’t ready to rebase onto the tip of upstream: + +```sh +$ ./start-rebase.sh upstream~10 -i +``` diff --git a/commit-from-merge.sh b/commit-from-merge.sh new file mode 100755 index 0000000000..94aa606f02 --- /dev/null +++ b/commit-from-merge.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Usage: commit-from-merge.sh [extra git-commit(1) arguments ...] +# Given a merge commit made by bors, runs git-commit(1) with your local changes +# while borrowing the author name/email from the right-hand parent of the merge, +# and the author date from the committer date of the merge. +set -eu + +lookup_repo=$1; shift +merge_commit=$1; shift +author_name_email=$(git -C "$lookup_repo" log -n1 --pretty='%aN <%aE>' "$merge_commit"\^2) +committer_date=$(git -C "$lookup_repo" log -n1 --pretty='%cd' "$merge_commit") + +set -- git commit --author="$author_name_email" --date="$committer_date" "$@" +echo "$@" +"$@" diff --git a/commit-from-squashed.sh b/commit-from-squashed.sh new file mode 100755 index 0000000000..004e0f7840 --- /dev/null +++ b/commit-from-squashed.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Usage: commit-from-squashed.sh [extra git-commit(1) arguments ...] +# Given a squashed commit made by the GitHub merge queue, runs git-commit(1) with your local changes +# while borrowing our author name/email from that commit, our author date from its committer date, +# and our commit message from that commit. +set -eu + +squashed_commit=$1; shift +committer_date=$(git log -n1 --pretty='%cd' "$squashed_commit") + +# -c is equivalent to --author=$(...'%aN <%aE>') -m $(...'%B'), but allows editing +set -- git commit -c "$squashed_commit" --date="$committer_date" "$@" +echo "$@" +"$@" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000..2c96e200aa --- /dev/null +++ b/shell.nix @@ -0,0 +1,6 @@ +with import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/46ae0210ce163b3cba6c7da08840c1d63de9c701.tar.gz"; +}) {}; +stdenv.mkDerivation rec { + name = "style-sync-shell"; +} diff --git a/start-rebase.sh b/start-rebase.sh new file mode 100755 index 0000000000..fe417f7f08 --- /dev/null +++ b/start-rebase.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Usage: start-rebase.sh [extra git-rebase(1) arguments ...] +# Equivalent to git rebase --onto . +set -eu + +new_base=$1; shift +first_commit=$(git log --pretty=\%H --grep='Servo initial downstream commit') +old_base=$first_commit~ + +git rebase --onto "$new_base" "$old_base" "$@" diff --git a/style.paths b/style.paths new file mode 100644 index 0000000000..d1d2d02638 --- /dev/null +++ b/style.paths @@ -0,0 +1,8 @@ +# Filters and renames use git-filter-repo(1) --paths-from-file: +# https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html#_filtering_based_on_many_paths + +servo/components/ +servo/rustfmt.toml + +regex:servo/components/(.+)==>\1 +servo/rustfmt.toml==>rustfmt.toml diff --git a/sync.sh b/sync.sh new file mode 100755 index 0000000000..e92182c746 --- /dev/null +++ b/sync.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Usage: sync.sh +set -eu + +root=$(pwd) +mkdir -p "$1" +cd -- "$1" +filtered=$(pwd) +mkdir -p "$root/_cache" +cd "$root/_cache" +export PATH="$PWD:$PATH" + +step() { + if [ "${TERM-}" != '' ]; then + tput setaf 12 + fi + >&2 printf '* %s\n' "$*" + if [ "${TERM-}" != '' ]; then + tput sgr0 + fi +} + +step Downloading git-filter-repo if needed +if ! git filter-repo --version 2> /dev/null; then + curl -O https://raw.githubusercontent.com/newren/git-filter-repo/v2.38.0/git-filter-repo + chmod +x git-filter-repo + + git filter-repo --version +fi + +step Cloning upstream if needed +if ! [ -e upstream ]; then + git clone --bare --single-branch --progress https://github.com/mozilla/gecko-dev.git upstream +fi + +step Updating upstream +branch=$(git -C upstream rev-parse --abbrev-ref HEAD) +git -C upstream fetch origin $branch:$branch + +step Filtering upstream +# Cloning and filtering is much faster than git filter-repo --source --target. +git clone --bare upstream -- "$filtered" +git -C "$filtered" filter-repo --force --paths-from-file "$root/style.paths" From a5eabbdfc294e42c9cb9dab0aace7c6cbcb1ad97 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Thu, 15 Feb 2024 18:30:14 +0800 Subject: [PATCH 02/30] Commit our changes on top of upstream Stylo This is a rebase of ab75cec2f1fdb1c6d5731b4c685a3bfc798f9619 Signed-off-by: Oriol Brufau --- .github/workflows/main.yml | 24 ++ .github/workflows/sync-upstream.yml | 2 +- Cargo.toml | 36 ++ README.md | 134 +++--- SYNCING.md | 63 +++ malloc_size_of/Cargo.toml | 14 +- selectors/Cargo.toml | 8 +- servo_arc/Cargo.toml | 4 +- style/Cargo.toml | 48 +-- style/build.rs | 8 +- style/global_style_data.rs | 2 +- style/lib.rs | 13 + style/parallel.rs | 15 +- style/properties/build.py | 4 + style/properties/properties.mako.rs | 8 +- .../mako-1.3.10-py3-none-any.whl | Bin 0 -> 78509 bytes .../vendored_python/markupsafe/LICENSE.txt | 28 ++ .../vendored_python/markupsafe/__init__.py | 384 ++++++++++++++++++ .../toml-0.10.2-py2.py3-none-any.whl | Bin 0 -> 16588 bytes style/servo/media_queries.rs | 118 +++++- style/servo/url.rs | 12 +- style/values/specified/box.rs | 2 +- style/values/specified/color.rs | 84 +++- style/values/specified/font.rs | 2 +- style_derive/Cargo.toml | 5 +- style_traits/Cargo.toml | 23 +- stylo_atoms/Cargo.toml | 20 + stylo_atoms/build.rs | 31 ++ stylo_atoms/lib.rs | 5 + stylo_atoms/predefined_counter_styles.rs | 66 +++ stylo_atoms/static_atoms.txt | 186 +++++++++ stylo_dom/Cargo.toml | 18 + stylo_dom/lib.rs | 181 +++++++++ stylo_static_prefs/Cargo.toml | 11 + stylo_static_prefs/src/lib.rs | 253 ++++++++++++ sync.sh | 2 +- to_shmem/Cargo.toml | 6 +- 37 files changed, 1693 insertions(+), 127 deletions(-) create mode 100644 Cargo.toml create mode 100644 SYNCING.md create mode 100644 style/properties/vendored_python/mako-1.3.10-py3-none-any.whl create mode 100644 style/properties/vendored_python/markupsafe/LICENSE.txt create mode 100644 style/properties/vendored_python/markupsafe/__init__.py create mode 100644 style/properties/vendored_python/toml-0.10.2-py2.py3-none-any.whl create mode 100644 stylo_atoms/Cargo.toml create mode 100644 stylo_atoms/build.rs create mode 100644 stylo_atoms/lib.rs create mode 100644 stylo_atoms/predefined_counter_styles.rs create mode 100644 stylo_atoms/static_atoms.txt create mode 100644 stylo_dom/Cargo.toml create mode 100644 stylo_dom/lib.rs create mode 100644 stylo_static_prefs/Cargo.toml create mode 100644 stylo_static_prefs/src/lib.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aafc8d0e84..fa6c67068c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,30 @@ jobs: env: RUST_BACKTRACE: 1 + macos-debug: + name: macOS (Debug) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Run Tests + run: cargo build --features servo + env: + RUST_BACKTRACE: 1 + + windows-debug: + name: Windows (Debug) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Run Tests + run: cargo build --features servo + env: + RUST_BACKTRACE: 1 + build-result: name: Result runs-on: ubuntu-latest diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 992fe6d2ce..adf329ffa6 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -19,5 +19,5 @@ jobs: key: upstream - run: | ./sync.sh _filtered - git fetch -f --progress ./_filtered master:upstream + git fetch -f --progress ./_filtered main:upstream git push -fu --progress origin upstream diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..93acb338d2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[workspace] +resolver = "2" +members = [ + "stylo_atoms", + "stylo_dom", + "malloc_size_of", + "rustfmt.toml", + "selectors", + "servo_arc", + "style", + "style_derive", + "stylo_static_prefs", + "style_traits", + "to_shmem", + "to_shmem_derive", +] +default-members = ["style"] + +[workspace.package] +version = "0.12.0" + +[workspace.dependencies] +# in-repo dependencies (separately versioned) +servo_arc = { version = "0.4.3", path = "./servo_arc" } +selectors = { version = "0.35.0", path = "./selectors" } +to_shmem = { version = "0.3.0", path = "./to_shmem", features = ["servo"] } +to_shmem_derive = { version = "0.1.0", path = "./to_shmem_derive" } + +# in-repo dependencies (main version) +malloc_size_of = { version = "0.12.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } +static_prefs = { version = "0.12.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } +stylo_atoms = { version = "0.12.0", path = "./stylo_atoms" } +dom = { version = "0.12.0", path = "./stylo_dom", package = "stylo_dom" } +style_traits = { version = "0.12.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} +style_derive = { version = "0.12.0", path = "./style_derive", package = "stylo_derive"} +stylo = { version = "0.12.0", path = "./style" } diff --git a/README.md b/README.md index b280c0c4a7..bfc3eb5b28 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,105 @@ Stylo ===== -This repo contains Servo’s downstream fork of [Stylo](https://searchfox.org/mozilla-central/source/servo). +**High-Performance CSS Style Engine** -The branches are as follows: +[![Build Status](https://github.com/servo/stylo/actions/workflows/main.yml/badge.svg)](https://github.com/servo/stylo/actions) +[![Crates.io](https://img.shields.io/crates/v/stylo.svg)](https://crates.io/crates/stylo) +[![Docs](https://docs.rs/stylo/badge.svg)](https://docs.rs/stylo) +![Crates.io License](https://img.shields.io/crates/l/stylo) -- [`upstream`](https://github.com/servo/style/tree/upstream) has upstream mozilla-central filtered to the paths we care about ([style.paths](style.paths)), but is otherwise unmodified -- [`main`](https://github.com/servo/style/tree/ci) has our downstream patches, plus the scripts and workflows for syncing with mozilla-central, to be rebased onto `upstream` +Stylo is a high-performance, browser-grade CSS style engine written in Rust that powers [Servo](https://servo.org) and [Firefox](https://firefox.com). This repo contains Servo’s downstream version of Stylo. The upstream version lives in mozilla-central with the rest of the Gecko/Firefox codebase. -## Building Servo against your local Stylo +Coordination of Stylo development happens: -Assuming your local `servo` and `stylo` directories are siblings, you can build `servo` against `stylo` by adding the following to `servo/Cargo.toml`: +- Here in Github Issues +- In the [#stylo](https://servo.zulipchat.com/#narrow/channel/417109-stylo) channel of the [Servo Zulip](https://servo.zulipchat.com/) +- In the [#layout](https://chat.mozilla.org/#/room/#layout:mozilla.org) room of the Mozilla Matrix instance (matrix.mozilla.org) -```toml -[patch."https://github.com/servo/stylo"] -selectors = { path = "../stylo/selectors" } -servo_arc = { path = "../stylo/servo_arc" } -stylo_atoms = { path = "../stylo/stylo_atoms" } -style = { path = "../stylo/style" } -stylo_config = { path = "../stylo/stylo_config" } -stylo_dom = { path = "../stylo/stylo_dom" } -style_malloc_size_of = { path = "../stylo/malloc_size_of", package = "malloc_size_of" } -style_traits = { path = "../stylo/style_traits" } -``` +## High-Level Documentation -## Syncing `upstream` with mozilla-central +- This [Mozilla Hacks article](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo) contains a high-level overview of the Stylo architecture. +- There is a [chapter](https://book.servo.org/architecture/style.html) in the Servo Book (although it is a little out of date). -Start by generating a filtered copy of mozilla-central. This will cache the raw mozilla-central in `_cache/upstream`, storing the result in `_filtered`: +## Branches -```sh -$ ./sync.sh _filtered -``` +The branches are as follows: -If `_filtered` already exists, you will need to delete it and try again: +- [**upstream**](https://github.com/servo/style/tree/upstream) has upstream [mozilla-central](https://searchfox.org/mozilla-central/source/servo) filtered to the paths we care about ([style.paths](style.paths)), but is otherwise unmodified. +- [**main**](https://github.com/servo/style/tree/ci) adds our downstream patches, plus the scripts and workflows for syncing with mozilla-central on top of **upstream**. -```sh -$ rm -Rf _filtered -``` +> [!WARNING] +> This repo syncs from upstream by creating a new branch and then rebasing our changes on top of it. This means that `git pull` will not work across syncs (you will need to use `git fetch`, `git reset` and `git rebase`). -Now overwrite our `upstream` with those commits and push: +More information on the syncing process is available in [SYNCING.md](SYNCING.md) -```sh -$ git fetch -f --progress ./_filtered master:upstream -$ git push -fu --progress origin upstream -``` +## Crates -## Rebasing `main` onto `upstream` +A guide to the crates contained within this repo -Start by fetching `upstream` into your local repo: +### Stylo Crates -```sh -$ git fetch -f origin upstream:upstream -``` +These crates are largely implementation details of Stylo, although you may need to use some of them directly if you use Stylo. -In general, the filtering process is deterministic, yielding the same commit hashes each time, so we can rebase normally: +| Directory | Crate | Notes | +| --- | --- | --- | +| style | [![Crates.io](https://img.shields.io/crates/v/stylo.svg)](https://crates.io/crates/stylo) | The main Stylo crate containing the entire CSS engine | +| style_traits | [![Crates.io](https://img.shields.io/crates/v/stylo_traits.svg)](https://crates.io/crates/stylo_traits) | Types and traits which allow other code to interoperate with Stylo without depending on the main crate directly. | +| stylo_dom | [![Crates.io](https://img.shields.io/crates/v/stylo_dom.svg)](https://crates.io/crates/stylo_dom) | Similar to stylo_traits (but much smaller) | +| stylo_atoms | [![Crates.io](https://img.shields.io/crates/v/stylo_atoms.svg)](https://crates.io/crates/stylo_atoms) | [Atoms](https://docs.rs/string_cache/latest/string_cache/struct.Atom.html) for CSS and HTML event related strings | +| stylo_static_prefs | [![Crates.io](https://img.shields.io/crates/v/stylo_static_prefs.svg)](https://crates.io/crates/stylo_static_prefs) | Configuration for Stylo. Can be used to set runtime preferences (enabling/disabling properties, etc) | +| style_derive | [![Crates.io](https://img.shields.io/crates/v/stylo_derive.svg)](https://crates.io/crates/stylo_derive) | Internal derive macro for stylo crate | -```sh -$ git rebase upstream -``` +### Standalone Crates -But if the filtering config changes or Mozilla moves to GitHub, the commit hashes on `upstream` may change. In this case, we need to tell git where the old upstream ends and our own commits start (notice the `~`): +These crates form part of Stylo but are also be useful standalone. -```sh -$ git log --pretty=\%H --grep='Servo initial downstream commit' -e62d7f0090941496e392e1dc91df103a38e3f488 +| Directory | Crate | Notes | +| --- | --- | --- | +| selectors | [![Crates.io](https://img.shields.io/crates/v/selectors.svg)](https://crates.io/crates/selectors) | CSS Selector matching | +| servo_arc | [![Crates.io](https://img.shields.io/crates/v/servo_arc.svg)](https://crates.io/crates/servo_arc) | A variant on `std::Arc` | -$ git rebase --onto upstream e62d7f0090941496e392e1dc91df103a38e3f488~ -Successfully rebased and updated refs/heads/main. -``` +You may also be interested in the `cssparser` crate which lives in the [servo/rust-cssparser](https://github.com/servo/rust-cssparser) repo. -`start-rebase.sh` takes care of this automatically, but you should still use `git rebase` for subsequent steps like `--continue` and `--abort`: +### Support Crates -```sh -$ ./start-rebase.sh upstream -$ ./start-rebase.sh upstream -i # interactive -$ git rebase --continue # not ./start-rebase.sh --continue -$ git rebase --abort # not ./start-rebase.sh --abort -``` +Low-level crates which could technically be used standalone but are unlikely to be generally useful in practice. + +| Directory | Crate | Notes | +| --- | --- | --- | +| malloc_size_of | [![Crates.io](https://img.shields.io/crates/v/stylo_malloc_size_of.svg)](https://crates.io/crates/stylo_malloc_size_of) | Heap size measurement for Stylo values | +| to_shmem | [![Crates.io](https://img.shields.io/crates/v/to_shmem.svg)](https://crates.io/crates/to_shmem) | Internal utility crate for sharing memory across processes. | +| to_shmem_derive | [![Crates.io](https://img.shields.io/crates/v/to_shmem_derive.svg)](https://crates.io/crates/to_shmem_derive) | Internal derive macro for to_shmem crate | -Or if we aren’t ready to rebase onto the tip of upstream: +## Building Servo Against a Local Copy of Stylo + +Assuming your local `servo` and `stylo` directories are siblings, you can build `servo` against `stylo` by adding the following to `servo/Cargo.toml`: -```sh -$ ./start-rebase.sh upstream~10 -i +```toml +[patch."https://github.com/servo/stylo"] +selectors = { path = "../stylo/selectors" } +servo_arc = { path = "../stylo/servo_arc" } +stylo = { path = "../stylo/style" } +stylo_atoms = { path = "../stylo/stylo_atoms" } +stylo_dom = { path = "../stylo/stylo_dom" } +stylo_malloc_size_of = { path = "../stylo/malloc_size_of" } +stylo_static_prefs = { path = "../stylo/stylo_static_prefs" } +stylo_traits = { path = "../stylo/style_traits" } ``` + +## Releases + +Releases are made every time this repository rebases its changes on top of the latest version of upstream Stylo. There are a lot of crates here. In order to publish them, they must be done in order. One order that works is: + +- selectors +- stylo_static_prefs +- stylo_atoms +- stylo_malloc_size_of +- stylo_dom +- stylo_derive +- stylo_traits +- stylo + +## License + +Stylo is licensed under MPL 2.0 diff --git a/SYNCING.md b/SYNCING.md new file mode 100644 index 0000000000..72a0a53d1d --- /dev/null +++ b/SYNCING.md @@ -0,0 +1,63 @@ +# Syncing + +This file documents the process of syncing this repository with the upstream copy of Stylo in mozilla-central. + +## Syncing `upstream` with mozilla-central + +Start by generating a filtered copy of mozilla-central. This will cache the raw mozilla-central in `_cache/upstream`, storing the result in `_filtered`: + +```sh +$ ./sync.sh _filtered +``` + +If `_filtered` already exists, you will need to delete it and try again: + +```sh +$ rm -Rf _filtered +``` + +Now overwrite our `upstream` with those commits and push: + +```sh +$ git fetch -f --progress ./_filtered main:upstream +$ git push -fu --progress origin upstream +``` + +## Rebasing `main` onto `upstream` + +Start by fetching `upstream` into your local repo: + +```sh +$ git fetch -f origin upstream:upstream +``` + +In general, the filtering process is deterministic, yielding the same commit hashes each time, so we can rebase normally: + +```sh +$ git rebase upstream +``` + +But if the filtering config changes or Mozilla moves to GitHub, the commit hashes on `upstream` may change. In this case, we need to tell git where the old upstream ends and our own commits start (notice the `~`): + +```sh +$ git log --pretty=\%H --grep='Servo initial downstream commit' +e62d7f0090941496e392e1dc91df103a38e3f488 + +$ git rebase --onto upstream e62d7f0090941496e392e1dc91df103a38e3f488~ +Successfully rebased and updated refs/heads/main. +``` + +`start-rebase.sh` takes care of this automatically, but you should still use `git rebase` for subsequent steps like `--continue` and `--abort`: + +```sh +$ ./start-rebase.sh upstream +$ ./start-rebase.sh upstream -i # interactive +$ git rebase --continue # not ./start-rebase.sh --continue +$ git rebase --abort # not ./start-rebase.sh --abort +``` + +Or if we aren’t ready to rebase onto the tip of upstream: + +```sh +$ ./start-rebase.sh upstream~10 -i +``` diff --git a/malloc_size_of/Cargo.toml b/malloc_size_of/Cargo.toml index 4859a962c0..66288a8204 100644 --- a/malloc_size_of/Cargo.toml +++ b/malloc_size_of/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "malloc_size_of" -version = "0.0.1" +name = "stylo_malloc_size_of" +version.workspace = true authors = ["The Servo Project Developers"] license = "MIT OR Apache-2.0" repository = "https://github.com/servo/stylo" @@ -18,10 +18,10 @@ servo = ["string_cache"] app_units = "0.7" cssparser = "0.36" euclid = "0.22" -selectors = { path = "../selectors" } -servo_arc = { path = "../servo_arc" } +selectors = { workspace = true } +servo_arc = { workspace = true } smallbitvec = "2.3.0" -smallvec = "1.0" -string_cache = { version = "0.8", optional = true } -thin-vec = { version = "0.2.1" } +smallvec = "1.13" +string_cache = { version = "0.9", optional = true } +thin-vec = { version = "0.2.13" } void = "1.0.2" diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index de502baffd..d566b36fe8 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selectors" -version = "0.26.0" +version = "0.35.0" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/selectors/" description = "CSS Selectors matching for Rust" @@ -27,10 +27,10 @@ rustc-hash = "2.1.1" log = "0.4" phf = "0.13" precomputed-hash = "0.1" -servo_arc = { version = "0.4", path = "../servo_arc" } +servo_arc = { workspace = true } smallvec = "1.0" -to_shmem = { version = "0.1", path = "../to_shmem", features = ["servo_arc"], optional = true } -to_shmem_derive = { version = "0.1", path = "../to_shmem_derive", optional = true } +to_shmem = { workspace = true, optional = true } +to_shmem_derive = { workspace = true, optional = true } new_debug_unreachable = "1" [build-dependencies] diff --git a/servo_arc/Cargo.toml b/servo_arc/Cargo.toml index 8b0976b75d..f5ae244a50 100644 --- a/servo_arc/Cargo.toml +++ b/servo_arc/Cargo.toml @@ -1,17 +1,19 @@ [package] name = "servo_arc" -version = "0.4.0" +version = "0.4.3" authors = ["The Servo Project Developers"] license = "MIT OR Apache-2.0" repository = "https://github.com/servo/stylo" description = "A fork of std::sync::Arc with some extra functionality and without weak references" edition = "2021" +readme = "../README.md" [lib] name = "servo_arc" path = "lib.rs" [features] +default = ["track_alloc_size"] gecko_refcount_logging = [] servo = ["serde", "track_alloc_size"] track_alloc_size = [] diff --git a/style/Cargo.toml b/style/Cargo.toml index 1c37aa62be..ab89b94872 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "style" -version = "0.0.1" +name = "stylo" +version.workspace = true authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" edition = "2021" description = "The Stylo CSS engine" +readme = "../README.md" build = "build.rs" @@ -18,6 +19,7 @@ path = "lib.rs" doctest = false [features] +default = ["servo"] gecko = [ "bindgen", "malloc_size_of/gecko", @@ -31,15 +33,14 @@ gecko = [ "to_shmem/gecko", ] servo = [ - "arrayvec/use_union", "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "web_atoms", + "mime", "serde", "servo_arc/servo", "stylo_atoms", - "servo_config", "string_cache", "style_traits/servo", "url", @@ -48,6 +49,7 @@ servo = [ ] gecko_debug = [] gecko_refcount_logging = [] +nsstring = [] [dependencies] app_units = "0.7.8" @@ -57,20 +59,20 @@ bitflags = "2" byteorder = "1.0" cssparser = "0.36" derive_more = { version = "2", features = ["add", "add_assign", "deref", "deref_mut", "from"] } -dom = { path = "../../../dom/base/rust" } +dom = { workspace = true } new_debug_unreachable = "1.0" encoding_rs = {version = "0.8", optional = true} euclid = "0.22" rustc-hash = "2.1.1" -icu_segmenter = { version = "2.0", default-features = false, features = ["auto", "compiled_data"] } +icu_segmenter = { version = ">= 1.5, <= 2.*", default-features = false, features = ["auto", "compiled_data"] } indexmap = {version = "2", features = ["std"]} itertools = "0.14" itoa = "1.0" log = "0.4" -malloc_size_of = { path = "../malloc_size_of" } -malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" } -web_atoms = { version = "0.1", optional = true } -nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true} +malloc_size_of = { workspace = true } +malloc_size_of_derive = "0.1" +web_atoms = { version = "0.2.0", optional = true } +mime = { version = "0.3.13", optional = true } num_cpus = {version = "1.1.0"} num-integer = "0.1" num-traits = "0.2" @@ -79,26 +81,24 @@ parking_lot = "0.12" precomputed-hash = "0.1.1" rayon = "1" rayon-core = "1" -selectors = { path = "../selectors" } +selectors = { workspace = true } serde = {version = "1.0", optional = true, features = ["derive"]} -servo_arc = { path = "../servo_arc" } -stylo_atoms = {path = "../atoms", optional = true} -servo_config = {path = "../config", optional = true} +servo_arc = { workspace = true} +stylo_atoms = { workspace = true, optional = true} smallbitvec = "2.3.0" smallvec = "1.0" static_assertions = "1.1" -static_prefs = { path = "../../../modules/libpref/init/static_prefs" } -string_cache = { version = "0.8", optional = true } -strum = "0.27" -strum_macros = "0.27" -style_derive = {path = "../style_derive"} -style_traits = {path = "../style_traits"} -to_shmem = {path = "../to_shmem"} -to_shmem_derive = {path = "../to_shmem_derive"} -thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +static_prefs = { workspace = true} +string_cache = { version = "0.9", optional = true } +strum = "0.28" +strum_macros = "0.28" +style_derive = { workspace = true } +style_traits = { workspace = true } +to_shmem = { workspace = true} +to_shmem_derive = { workspace = true } +thin-vec = "0.2.1" uluru = "3.0" void = "1.0.2" -gecko-profiler = { path = "../../../tools/profiler/rust-api" } url = { version = "2.5", optional = true, features = ["serde"] } [build-dependencies] diff --git a/style/build.rs b/style/build.rs index fb1f5e36ea..26176df9ad 100644 --- a/style/build.rs +++ b/style/build.rs @@ -19,7 +19,7 @@ mod build_gecko { pub static PYTHON: LazyLock = LazyLock::new(|| { env::var("PYTHON3").ok().unwrap_or_else(|| { let candidates = if cfg!(windows) { - ["python3.exe"] + ["python.exe"] } else { ["python3"] }; @@ -56,6 +56,12 @@ fn generate_properties(engine: &str) { .join("build.py"); let status = Command::new(&*PYTHON) + // `cargo publish` isn't happy with the `__pycache__` files that are created + // when we run the property generator. + // + // TODO(mrobinson): Is this happening because of how we run this script? It + // would be better to ensure are just placed in the output directory. + .env("PYTHONDONTWRITEBYTECODE", "1") .arg(&script) .arg(engine) .arg("style-crate") diff --git a/style/global_style_data.rs b/style/global_style_data.rs index c1ab0f7c6e..3775f69ed5 100644 --- a/style/global_style_data.rs +++ b/style/global_style_data.rs @@ -156,7 +156,7 @@ impl StyleThreadPool { #[cfg(feature = "servo")] fn stylo_threads_pref() -> i32 { - style_config::get_i32("layout.threads") + static_prefs::pref!("layout.threads") } #[cfg(feature = "gecko")] diff --git a/style/lib.rs b/style/lib.rs index 1f6a873073..e22a0ff239 100644 --- a/style/lib.rs +++ b/style/lib.rs @@ -17,6 +17,8 @@ //! //! [recalc_style_at]: traversal/fn.recalc_style_at.html //! +//! A list of supported style properties can be found as [docs::supported_properties] +//! //! Major dependencies are the [cssparser][cssparser] and [selectors][selectors] //! crates. //! @@ -37,6 +39,7 @@ extern crate gecko_profiler; pub mod gecko_string_cache; #[macro_use] extern crate log; +#[macro_use] extern crate serde; pub use servo_arc; #[cfg(feature = "servo")] @@ -117,6 +120,16 @@ pub mod use_counters; #[allow(non_camel_case_types)] pub mod values; +#[cfg(all(doc, feature = "servo"))] +/// Documentation +pub mod docs { + /// The CSS properties supported by the style system. + /// Generated from the `properties.mako.rs` template by `build.rs` + pub mod supported_properties { + #![doc = include_str!(concat!(env!("OUT_DIR"), "/css-properties.html"))] + } +} + #[cfg(feature = "gecko")] pub use crate::gecko_string_cache as string_cache; #[cfg(feature = "gecko")] diff --git a/style/parallel.rs b/style/parallel.rs index d05524363b..40946a8cbe 100644 --- a/style/parallel.rs +++ b/style/parallel.rs @@ -34,8 +34,21 @@ pub const STYLE_THREAD_STACK_SIZE_KB: usize = 256; /// The minimum stack size for a thread in the styling pool, in kilobytes. /// Servo requires a bigger stack in debug builds. +/// We allow configuring the size, since running with ASAN requires an even larger +/// stack size. #[cfg(feature = "servo")] -pub const STYLE_THREAD_STACK_SIZE_KB: usize = 512; +pub const STYLE_THREAD_STACK_SIZE_KB: usize = const { + let default_stack_size = 512; + if let Some(user_def_size) = option_env!("SERVO_STYLE_THREAD_STACK_SIZE_KB") { + if let Ok(user_def_size) = usize::from_str_radix(user_def_size, 10) { + user_def_size + } else { + panic!("SERVO_STYLE_THREAD_STACK_SIZE_KB must be a valid integer") + } + } else { + default_stack_size + } +}; /// The stack margin. If we get this deep in the stack, we will skip recursive /// optimizations to ensure that there is sufficient room for non-recursive work. diff --git a/style/properties/build.py b/style/properties/build.py index e6b4bb63e1..5ad2bc0666 100644 --- a/style/properties/build.py +++ b/style/properties/build.py @@ -8,6 +8,9 @@ import sys BASE = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(BASE, "vendored_python", "mako-1.3.10-py3-none-any.whl")) +sys.path.insert(0, os.path.join(BASE, "vendored_python", "toml-0.10.2-py2.py3-none-any.whl")) +sys.path.insert(0, os.path.join(BASE, "vendored_python")) # For importing markupsafe sys.path.insert(0, BASE) # For importing `data.py` from mako import exceptions @@ -72,6 +75,7 @@ def main(): write(doc_servo, "css-properties.html", as_html) write(doc_servo, "css-properties.json", as_json) write(OUT_DIR, "css-properties.json", as_json) + write(OUT_DIR, "css-properties.html", as_html) def abort(message): diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index 075cccd88b..ef2b2432aa 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -391,7 +391,10 @@ impl NonCustomPropertyId { pref = getattr(property, "servo_pref") %> % if pref: - Some("${pref}"), + { + const_assert!(!static_prefs::default_value!("${pref}")); + Some("${pref}") + }, % else: None, % endif @@ -402,7 +405,8 @@ impl NonCustomPropertyId { Some(pref) => pref, }; - style_config::get_bool(pref) + // The assertions above guarantee that the pref defaults to false. + static_prefs::Preference::get(pref, false) % endif }; diff --git a/style/properties/vendored_python/mako-1.3.10-py3-none-any.whl b/style/properties/vendored_python/mako-1.3.10-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..2f85cd7b576bf7998b55999cfe2f5519e3b75be3 GIT binary patch literal 78509 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
    X?O4`zuspQ{P-N3wH2jwQmX{zwD`%wDPR zKj;&WF$+qKV{ABBcYm@@`^RE-8~B*=$y;6u>M1&A+G>RxfP>!@UO+HVq0=10Mnruw4_)0vt88=en=d~>C2S2i^6-PG_6uApW<0AHtMB@y~KZP39ZS|KaovvNu!g^u~4f3R1s>pY}pGRADQ*V8~Zuob* z$mfu8?PYvYH7zZ?3cH~XTJ&x={96`1dZd~4EZUk9@YbBn-{vdZ>r4er20~Yih8QzL zTRa13;W9AT)T2)J?joB-X;gpK*sG3tEd`g}d9evvLI6|nWG$}rsuAu|x`iy1H&wOK zw=5G$BO{4CVLL?eTrQ$z;FX?fE2bC#WOq zq~C>N%&UR;6qLAA$X}=}dmifW za8XS|o;#0wzc``Lz+G+ZGzU3I63q$O-_3kl!hSXG_8=z!;B(74k{BaNwo*GOaPzPQ zbk|D^+o3^yMdh5st({(?gV4mdP7OmE;z>S0&>!eKh*w&6j9afDTFoxAEr7w9NNawV zW`pZXZElnkSYUh&A@)Fga9s&w=~ReU?S*eq3i1Ihl~wMiR8S5_v}i>x*&JAj#4DRg zRuO^8eTD55yF8=6oT(IeTbn>`Ezr22q4s3+GpHa#Si%kH!zq? z>J&WXHTeM&VwOQ3fFPsDlUE{proH-y&MUVGd`6gV?DDBm6;Y9xl|Hy{i)RRHTdDkl z(_w`*heMXdb9eKgWs@t z9MR(NYJU4iGkpZ7$6Aw+;B&Y1D)=+&REOi(jE8M_UH>Th@iN5n^q>@-r78Sae%((5 z?1n#Hi`zLu8uo%{I|;nXXL!#Q3@zU%XGYI6*%?o=9kbDdvD)zMUQZ7~07F)AaZnRU zS74Z{Qwj^K??x4-Q}I!sCx$doa5#x{K?Ftf$QG7e2tj(?iZBe;GIyxXURrh}R!VD1 zk98xQ{4YsA1Y27u)Fp1bE=n^cqzTQ^4Lxk2m}E@*vyQzD7SQ8DOim~;&|=Wsf!Z$e z2#U&}JCE<2D_HGyW41mD2Yr(G0n{3VE3cXkTM51GR{AC?g50EY8v$u3@_FxOrbOel z2GU8#g33}p(^w6r3E5S!l5JcdO91_MmMM|WkUYVNk9XUQ(?^d4A&4%OK@xO(W6}Zw zcMeHBSgelu(T4E964+Uw9{X`wc!^+y9Ac#~)0=g09Q6X!fMDg)Z{lU=t^z(4gjv$? zRW!2_(PCz>Vr936t&DhwUm%)Y=%A=7m8ckcyZFyKPc^*9!z|M1X#@YX28ZMxDNu#V zEkt^e=XQsIC7{6oA>YnNtxdqwWEu*}d=q*d%51f>&?;$vsF!5s30!)3^pSGzwo)s9 z8idD`=+oSULI`|f8<-|E>pn^q!FANL+!JW;*Uylje9?ifpX9gb9I6fi(l%_q(IF&%(=T*gxRu)^*4rVOS z#bB$F<4~rlAOP!i9LL4G!C?sT_M2*8T``p8z7O2y8 V0nvqmFhJJbC7)VJZ=kc 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

    hU}wVA9HjqE1mgo~JG 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 MOr})8 str: + return ( + s.replace("&", "&") + .replace(">", ">") + .replace("<", "<") + .replace("'", "'") + .replace('"', """) + ) +# BEGIN PATCHED SECTION + + +class _HasHTML(t.Protocol): + def __html__(self, /) -> str: ... + + +class _TPEscape(t.Protocol): + def __call__(self, s: t.Any, /) -> Markup: ... + + +def escape(s: t.Any, /) -> Markup: + """Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in + the string with HTML-safe sequences. Use this if you need to display + text that might contain such characters in HTML. + + If the object has an ``__html__`` method, it is called and the + return value is assumed to already be safe for HTML. + + :param s: An object to be converted to a string and escaped. + :return: A :class:`Markup` string with the escaped text. + """ + # If the object is already a plain string, skip __html__ check and string + # conversion. This is the most common use case. + # Use type(s) instead of s.__class__ because a proxy object may be reporting + # the __class__ of the proxied value. + if type(s) is str: + return Markup(_escape_inner(s)) + + if hasattr(s, "__html__"): + return Markup(s.__html__()) + + return Markup(_escape_inner(str(s))) + + +def escape_silent(s: t.Any | None, /) -> Markup: + """Like :func:`escape` but treats ``None`` as the empty string. + Useful with optional values, as otherwise you get the string + ``'None'`` when the value is ``None``. + + >>> escape(None) + Markup('None') + >>> escape_silent(None) + Markup('') + """ + if s is None: + return Markup() + + return escape(s) + + +def soft_str(s: t.Any, /) -> str: + """Convert an object to a string if it isn't already. This preserves + a :class:`Markup` string rather than converting it back to a basic + string, so it will still be marked as safe and won't be escaped + again. + + >>> value = escape("") + >>> value + Markup('<User 1>') + >>> escape(str(value)) + Markup('&lt;User 1&gt;') + >>> escape(soft_str(value)) + Markup('<User 1>') + """ + if not isinstance(s, str): + return str(s) + + return s + + +class Markup(str): + """A string that is ready to be safely inserted into an HTML or XML + document, either because it was escaped or because it was marked + safe. + + Passing an object to the constructor converts it to text and wraps + it to mark it safe without escaping. To escape the text, use the + :meth:`escape` class method instead. + + >>> Markup("Hello, World!") + Markup('Hello, World!') + >>> Markup(42) + Markup('42') + >>> Markup.escape("Hello, World!") + Markup('Hello <em>World</em>!') + + This implements the ``__html__()`` interface that some frameworks + use. Passing an object that implements ``__html__()`` will wrap the + output of that method, marking it safe. + + >>> class Foo: + ... def __html__(self): + ... return 'foo' + ... + >>> Markup(Foo()) + Markup('foo') + + This is a subclass of :class:`str`. It has the same methods, but + escapes their arguments and returns a ``Markup`` instance. + + >>> Markup("%s") % ("foo & bar",) + Markup('foo & bar') + >>> Markup("Hello ") + "" + Markup('Hello <foo>') + """ + + __slots__ = () + + def __new__( + cls, object: t.Any = "", encoding: str | None = None, errors: str = "strict" + ) -> te.Self: + if hasattr(object, "__html__"): + object = object.__html__() + + if encoding is None: + return super().__new__(cls, object) + + return super().__new__(cls, object, encoding, errors) + + def __html__(self, /) -> te.Self: + return self + + def __add__(self, value: str | _HasHTML, /) -> te.Self: + if isinstance(value, str) or hasattr(value, "__html__"): + return self.__class__(super().__add__(self.escape(value))) + + return NotImplemented + + def __radd__(self, value: str | _HasHTML, /) -> te.Self: + if isinstance(value, str) or hasattr(value, "__html__"): + return self.escape(value).__add__(self) + + return NotImplemented + + def __mul__(self, value: t.SupportsIndex, /) -> te.Self: + return self.__class__(super().__mul__(value)) + + def __rmul__(self, value: t.SupportsIndex, /) -> te.Self: + return self.__class__(super().__mul__(value)) + + def __mod__(self, value: t.Any, /) -> te.Self: + if isinstance(value, tuple): + # a tuple of arguments, each wrapped + value = tuple(_MarkupEscapeHelper(x, self.escape) for x in value) + elif hasattr(type(value), "__getitem__") and not isinstance(value, str): + # a mapping of arguments, wrapped + value = _MarkupEscapeHelper(value, self.escape) + else: + # a single argument, wrapped with the helper and a tuple + value = (_MarkupEscapeHelper(value, self.escape),) + + return self.__class__(super().__mod__(value)) + + def __repr__(self, /) -> str: + return f"{self.__class__.__name__}({super().__repr__()})" + + def join(self, iterable: cabc.Iterable[str | _HasHTML], /) -> te.Self: + return self.__class__(super().join(map(self.escape, iterable))) + + def split( # type: ignore[override] + self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1 + ) -> list[te.Self]: + return [self.__class__(v) for v in super().split(sep, maxsplit)] + + def rsplit( # type: ignore[override] + self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1 + ) -> list[te.Self]: + return [self.__class__(v) for v in super().rsplit(sep, maxsplit)] + + def splitlines( # type: ignore[override] + self, /, keepends: bool = False + ) -> list[te.Self]: + return [self.__class__(v) for v in super().splitlines(keepends)] + + def unescape(self, /) -> str: + """Convert escaped markup back into a text string. This replaces + HTML entities with the characters they represent. + + >>> Markup("Main » About").unescape() + 'Main » About' + """ + from html import unescape + + return unescape(str(self)) + + def striptags(self, /) -> str: + """:meth:`unescape` the markup, remove tags, and normalize + whitespace to single spaces. + + >>> Markup("Main »\tAbout").striptags() + 'Main » About' + """ + value = str(self) + + # Look for comments then tags separately. Otherwise, a comment that + # contains a tag would end early, leaving some of the comment behind. + + # keep finding comment start marks + while (start := value.find("", start)) == -1: + break + + value = f"{value[:start]}{value[end + 3 :]}" + + # remove tags using the same method + while (start := value.find("<")) != -1: + if (end := value.find(">", start)) == -1: + break + + value = f"{value[:start]}{value[end + 1 :]}" + + # collapse spaces + value = " ".join(value.split()) + return self.__class__(value).unescape() + + @classmethod + def escape(cls, s: t.Any, /) -> te.Self: + """Escape a string. Calls :func:`escape` and ensures that for + subclasses the correct type is returned. + """ + rv = escape(s) + + if rv.__class__ is not cls: + return cls(rv) + + return rv # type: ignore[return-value] + + def __getitem__(self, key: t.SupportsIndex | slice, /) -> te.Self: + return self.__class__(super().__getitem__(key)) + + def capitalize(self, /) -> te.Self: + return self.__class__(super().capitalize()) + + def title(self, /) -> te.Self: + return self.__class__(super().title()) + + def lower(self, /) -> te.Self: + return self.__class__(super().lower()) + + def upper(self, /) -> te.Self: + return self.__class__(super().upper()) + + def replace(self, old: str, new: str, count: t.SupportsIndex = -1, /) -> te.Self: + return self.__class__(super().replace(old, self.escape(new), count)) + + def ljust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self: + return self.__class__(super().ljust(width, self.escape(fillchar))) + + def rjust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self: + return self.__class__(super().rjust(width, self.escape(fillchar))) + + def lstrip(self, chars: str | None = None, /) -> te.Self: + return self.__class__(super().lstrip(chars)) + + def rstrip(self, chars: str | None = None, /) -> te.Self: + return self.__class__(super().rstrip(chars)) + + def center(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self: + return self.__class__(super().center(width, self.escape(fillchar))) + + def strip(self, chars: str | None = None, /) -> te.Self: + return self.__class__(super().strip(chars)) + + def translate( + self, + table: cabc.Mapping[int, str | int | None], # type: ignore[override] + /, + ) -> str: + return self.__class__(super().translate(table)) + + def expandtabs(self, /, tabsize: t.SupportsIndex = 8) -> te.Self: + return self.__class__(super().expandtabs(tabsize)) + + def swapcase(self, /) -> te.Self: + return self.__class__(super().swapcase()) + + def zfill(self, width: t.SupportsIndex, /) -> te.Self: + return self.__class__(super().zfill(width)) + + def casefold(self, /) -> te.Self: + return self.__class__(super().casefold()) + + def removeprefix(self, prefix: str, /) -> te.Self: + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix: str) -> te.Self: + return self.__class__(super().removesuffix(suffix)) + + def partition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]: + left, sep, right = super().partition(sep) + cls = self.__class__ + return cls(left), cls(sep), cls(right) + + def rpartition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]: + left, sep, right = super().rpartition(sep) + cls = self.__class__ + return cls(left), cls(sep), cls(right) + + def format(self, *args: t.Any, **kwargs: t.Any) -> te.Self: + formatter = EscapeFormatter(self.escape) + return self.__class__(formatter.vformat(self, args, kwargs)) + + def format_map( + self, + mapping: cabc.Mapping[str, t.Any], # type: ignore[override] + /, + ) -> te.Self: + formatter = EscapeFormatter(self.escape) + return self.__class__(formatter.vformat(self, (), mapping)) + + def __html_format__(self, format_spec: str, /) -> te.Self: + if format_spec: + raise ValueError("Unsupported format specification for Markup.") + + return self + + +class EscapeFormatter(string.Formatter): + __slots__ = ("escape",) + + def __init__(self, escape: _TPEscape) -> None: + self.escape: _TPEscape = escape + super().__init__() + + def format_field(self, value: t.Any, format_spec: str) -> str: + if hasattr(value, "__html_format__"): + rv = value.__html_format__(format_spec) + elif hasattr(value, "__html__"): + if format_spec: + raise ValueError( + f"Format specifier {format_spec} given, but {type(value)} does not" + " define __html_format__. A class that defines __html__ must define" + " __html_format__ to work with format specifiers." + ) + rv = value.__html__() + else: + # We need to make sure the format spec is str here as + # otherwise the wrong callback methods are invoked. + rv = super().format_field(value, str(format_spec)) + return str(self.escape(rv)) + + +class _MarkupEscapeHelper: + """Helper for :meth:`Markup.__mod__`.""" + + __slots__ = ("obj", "escape") + + def __init__(self, obj: t.Any, escape: _TPEscape) -> None: + self.obj: t.Any = obj + self.escape: _TPEscape = escape + + def __getitem__(self, key: t.Any, /) -> te.Self: + return self.__class__(self.obj[key], self.escape) + + def __str__(self, /) -> str: + return str(self.escape(self.obj)) + + def __repr__(self, /) -> str: + return str(self.escape(repr(self.obj))) + + def __int__(self, /) -> int: + return int(self.obj) + + def __float__(self, /) -> float: + return float(self.obj) diff --git a/style/properties/vendored_python/toml-0.10.2-py2.py3-none-any.whl b/style/properties/vendored_python/toml-0.10.2-py2.py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..2cb8dcbd80a84a5a7f046c25922fa06d5b3763e8 GIT binary patch literal 16588 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 literal 0 HcmV?d00001 diff --git a/style/servo/media_queries.rs b/style/servo/media_queries.rs index da9fa7c6ff..011c7d36fb 100644 --- a/style/servo/media_queries.rs +++ b/style/servo/media_queries.rs @@ -19,7 +19,7 @@ use crate::values::computed::font::GenericFontFamily; use crate::values::computed::{ CSSPixelLength, Context, Length, LineHeight, NonNegativeLength, Resolution, }; -use crate::values::specified::color::{ColorSchemeFlags, ForcedColors}; +use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; use crate::values::specified::font::{ QueryFontMetricsFlags, FONT_MEDIUM_CAP_PX, FONT_MEDIUM_CH_PX, FONT_MEDIUM_EX_PX, FONT_MEDIUM_IC_PX, FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX, @@ -505,6 +505,122 @@ impl Device { false } + pub(crate) fn system_color( + &self, + system_color: SystemColor, + color_scheme: ColorSchemeFlags, + ) -> AbsoluteColor { + fn srgb(r: u8, g: u8, b: u8) -> AbsoluteColor { + AbsoluteColor::srgb_legacy(r, g, b, 1f32) + } + + let dark = self.is_dark_color_scheme(color_scheme); + + // Refer to spec + // + if dark { + match system_color { + SystemColor::Accentcolor => srgb(10, 132, 255), + SystemColor::Accentcolortext => srgb(255, 255, 255), + SystemColor::Activetext => srgb(255, 0, 0), + SystemColor::Linktext => srgb(158, 158, 255), + SystemColor::Visitedtext => srgb(208, 173, 240), + SystemColor::Buttonborder + // Deprecated system colors (CSS Color 4) mapped to Buttonborder. + | SystemColor::Activeborder + | SystemColor::Inactiveborder + | SystemColor::Threeddarkshadow + | SystemColor::Threedshadow + | SystemColor::Windowframe => srgb(255, 255, 255), + SystemColor::Buttonface + // Deprecated system colors (CSS Color 4) mapped to Buttonface. + | SystemColor::Buttonhighlight + | SystemColor::Buttonshadow + | SystemColor::Threedface + | SystemColor::Threedhighlight + | SystemColor::Threedlightshadow => srgb(107, 107, 107), + SystemColor::Buttontext => srgb(245, 245, 245), + SystemColor::Canvas + // Deprecated system colors (CSS Color 4) mapped to Canvas. + | SystemColor::Activecaption + | SystemColor::Appworkspace + | SystemColor::Background + | SystemColor::Inactivecaption + | SystemColor::Infobackground + | SystemColor::Menu + | SystemColor::Scrollbar + | SystemColor::Window => srgb(30, 30, 30), + SystemColor::Canvastext + // Deprecated system colors (CSS Color 4) mapped to Canvastext. + | SystemColor::Captiontext + | SystemColor::Infotext + | SystemColor::Menutext + | SystemColor::Windowtext => srgb(232, 232, 232), + SystemColor::Field => srgb(45, 45, 45), + SystemColor::Fieldtext => srgb(240, 240, 240), + SystemColor::Graytext + // Deprecated system colors (CSS Color 4) mapped to Graytext. + | SystemColor::Inactivecaptiontext => srgb(155, 155, 155), + SystemColor::Highlight => srgb(38, 79, 120), + SystemColor::Highlighttext => srgb(255, 255, 255), + SystemColor::Mark => srgb(102, 92, 0), + SystemColor::Marktext => srgb(255, 255, 255), + SystemColor::Selecteditem => srgb(153, 200, 255), + SystemColor::Selecteditemtext => srgb(59, 59, 59), + } + } else { + match system_color { + SystemColor::Accentcolor => srgb(0, 102, 204), + SystemColor::Accentcolortext => srgb(255, 255, 255), + SystemColor::Activetext => srgb(238, 0, 0), + SystemColor::Linktext => srgb(0, 0, 238), + SystemColor::Visitedtext => srgb(85, 26, 139), + SystemColor::Buttonborder + // Deprecated system colors (CSS Color 4) mapped to Buttonborder. + | SystemColor::Activeborder + | SystemColor::Inactiveborder + | SystemColor::Threeddarkshadow + | SystemColor::Threedshadow + | SystemColor::Windowframe => srgb(0, 0, 0), + SystemColor::Buttonface + // Deprecated system colors (CSS Color 4) mapped to Buttonface. + | SystemColor::Buttonhighlight + | SystemColor::Buttonshadow + | SystemColor::Threedface + | SystemColor::Threedhighlight + | SystemColor::Threedlightshadow => srgb(240, 240, 240), + SystemColor::Buttontext => srgb(0, 0, 0), + SystemColor::Canvas + // Deprecated system colors (CSS Color 4) mapped to Canvas. + | SystemColor::Activecaption + | SystemColor::Appworkspace + | SystemColor::Background + | SystemColor::Inactivecaption + | SystemColor::Infobackground + | SystemColor::Menu + | SystemColor::Scrollbar + | SystemColor::Window => srgb(255, 255, 255), + SystemColor::Canvastext + // Deprecated system colors (CSS Color 4) mapped to Canvastext. + | SystemColor::Captiontext + | SystemColor::Infotext + | SystemColor::Menutext + | SystemColor::Windowtext => srgb(0, 0, 0), + SystemColor::Field => srgb(255, 255, 255), + SystemColor::Fieldtext => srgb(0, 0, 0), + SystemColor::Graytext + // Deprecated system colors (CSS Color 4) mapped to Graytext. + | SystemColor::Inactivecaptiontext => srgb(109, 109, 109), + SystemColor::Highlight => srgb(0, 65, 198), + SystemColor::Highlighttext => srgb(0, 0, 0), + SystemColor::Mark => srgb(255, 235, 59), + SystemColor::Marktext => srgb(0, 0, 0), + SystemColor::Selecteditem => srgb(0, 102, 204), + SystemColor::Selecteditemtext => srgb(255, 255, 255), + } + } + } + /// Returns safe area insets pub fn safe_area_insets(&self) -> SideOffsets2D { SideOffsets2D::zero() diff --git a/style/servo/url.rs b/style/servo/url.rs index 95491dee1d..f2836ee52b 100644 --- a/style/servo/url.rs +++ b/style/servo/url.rs @@ -68,7 +68,12 @@ impl CssUrl { /// FIXME(emilio): Should honor CorsMode. pub fn parse_from_string(url: String, context: &ParserContext, _: CorsMode) -> Self { let serialization = Arc::new(url); - let resolved = context.url_data.0.join(&serialization).ok().map(Arc::new); + // Per https://drafts.csswg.org/css-values-4/#url-empty + // If the original url is empty, then the resolved url is considered invalid. + let resolved = (!serialization.is_empty()) + .then(|| context.url_data.0.join(&serialization)) + .and_then(Result::ok) + .map(Arc::new); CssUrl(Arc::new(CssUrlData { original: Some(serialization), resolved: resolved, @@ -120,7 +125,10 @@ impl CssUrl { pub fn new_for_testing(url: &str) -> Self { CssUrl(Arc::new(CssUrlData { original: Some(Arc::new(url.into())), - resolved: ::url::Url::parse(url).ok().map(Arc::new), + resolved: (!url.is_empty()) + .then(|| ::url::Url::parse(url)) + .and_then(Result::ok) + .map(Arc::new), })) } diff --git a/style/values/specified/box.rs b/style/values/specified/box.rs index c4332461fe..bf47e6a518 100644 --- a/style/values/specified/box.rs +++ b/style/values/specified/box.rs @@ -28,7 +28,7 @@ fn grid_enabled() -> bool { #[cfg(feature = "servo")] fn grid_enabled() -> bool { - style_config::get_bool("layout.grid.enabled") + static_prefs::pref!("layout.grid.enabled") } #[inline] diff --git a/style/values/specified/color.rs b/style/values/specified/color.rs index 7a7e308d54..a5e3de2b4f 100644 --- a/style/values/specified/color.rs +++ b/style/values/specified/color.rs @@ -167,7 +167,6 @@ pub enum Color { /// Right now this is only the case for relative colors with `currentColor` as the origin. ColorFunction(Box>), /// A system color. - #[cfg(feature = "gecko")] System(SystemColor), /// A color mix. ColorMix(Box), @@ -439,6 +438,79 @@ impl SystemColor { } } +/// System colors. A bunch of these are ad-hoc, others come from Windows: +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor +/// +/// Others are HTML/CSS specific. Spec is: +/// +/// https://drafts.csswg.org/css-color/#css-system-colors +/// https://drafts.csswg.org/css-color/#deprecated-system-colors +#[allow(missing_docs)] +#[cfg(feature = "servo")] +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum SystemColor { + Accentcolor, + Accentcolortext, + Activetext, + Linktext, + Visitedtext, + Buttonborder, + Buttonface, + Buttontext, + Canvas, + Canvastext, + Field, + Fieldtext, + Graytext, + Highlight, + Highlighttext, + Mark, + Marktext, + Selecteditem, + Selecteditemtext, + + // Deprecated system colors. + Activeborder, + Inactiveborder, + Threeddarkshadow, + Threedhighlight, + Threedlightshadow, + Threedshadow, + Windowframe, + Buttonhighlight, + Buttonshadow, + Threedface, + Activecaption, + Appworkspace, + Background, + Inactivecaption, + Infobackground, + Menu, + Scrollbar, + Window, + Captiontext, + Infotext, + Menutext, + Windowtext, + Inactivecaptiontext, +} + +#[cfg(feature = "servo")] +impl SystemColor { + #[inline] + fn compute(&self, cx: &Context) -> ComputedColor { + if cx.for_non_inherited_property { + cx.rule_cache_conditions + .borrow_mut() + .set_color_scheme_dependency(cx.builder.color_scheme); + } + + ComputedColor::Absolute(cx.device().system_color(*self, cx.builder.color_scheme)) + } +} + /// Whether to preserve authored colors during parsing. That's useful only if we /// plan to serialize the color back. #[derive(Copy, Clone)] @@ -485,13 +557,16 @@ impl Color { Ok(color) }, Err(e) => { - #[cfg(feature = "gecko")] { + #[cfg(feature = "gecko")] if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { return Ok(Color::System(system)); } + #[cfg(feature = "servo")] + if let Ok(system) = input.try_parse(SystemColor::parse) { + return Ok(Color::System(system)); + } } - if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored)) { return Ok(Color::ColorMix(Box::new(mix))); @@ -595,7 +670,6 @@ impl ToCss for Color { c.to_css(dest)?; dest.write_char(')') }, - #[cfg(feature = "gecko")] Color::System(system) => system.to_css(dest), #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => dest.write_str("-moz-inherit-from-body-quirk"), @@ -610,7 +684,6 @@ impl Color { #[cfg(feature = "gecko")] Self::InheritFromBodyQuirk => false, Self::CurrentColor => true, - #[cfg(feature = "gecko")] Self::System(..) => true, Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(), Self::ColorFunction(ref color_function) => { @@ -841,7 +914,6 @@ impl Color { Color::ContrastColor(ref c) => { ComputedColor::ContrastColor(Box::new(c.to_computed_color(context)?)) }, - #[cfg(feature = "gecko")] Color::System(system) => system.compute(context?), #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => { diff --git a/style/values/specified/font.rs b/style/values/specified/font.rs index fe91aa2470..610fb29517 100644 --- a/style/values/specified/font.rs +++ b/style/values/specified/font.rs @@ -1773,7 +1773,7 @@ impl XTextScale { ToShmem, ToTyped, )] -#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "servo", derive(Deserialize, Eq, Hash, Serialize))] /// Internal property that reflects the lang attribute pub struct XLang(#[css(skip)] pub Atom); diff --git a/style_derive/Cargo.toml b/style_derive/Cargo.toml index 8d8a85f62d..b1c84ad073 100644 --- a/style_derive/Cargo.toml +++ b/style_derive/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "style_derive" -version = "0.0.1" +name = "stylo_derive" +version.workspace = true authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" edition = "2021" description = "Derive crate for Stylo CSS engine" +readme = "../README.md" [lib] path = "lib.rs" diff --git a/style_traits/Cargo.toml b/style_traits/Cargo.toml index 9ba394fe91..323d6f0b85 100644 --- a/style_traits/Cargo.toml +++ b/style_traits/Cargo.toml @@ -1,33 +1,34 @@ [package] -name = "style_traits" -version = "0.0.1" +name = "stylo_traits" +version.workspace = true authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" edition = "2021" description = "Types used by the Stylo CSS engine" +readme = "../README.md" [lib] name = "style_traits" path = "lib.rs" [features] +default = ["servo"] servo = ["stylo_atoms", "cssparser/serde", "url", "euclid/serde"] -gecko = ["nsstring"] +gecko = [] [dependencies] app_units = "0.7" bitflags = "2" cssparser = "0.36" euclid = "0.22" -malloc_size_of = { path = "../malloc_size_of" } -malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" } -nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true} -selectors = { path = "../selectors" } +malloc_size_of = { workspace = true} +malloc_size_of_derive = "0.1" +selectors = { workspace = true} serde = "1.0" -servo_arc = { path = "../servo_arc" } -stylo_atoms = { path = "../atoms", optional = true } +servo_arc = { workspace = true} +stylo_atoms = { workspace = true, optional = true } thin-vec = "0.2" -to_shmem = { path = "../to_shmem" } -to_shmem_derive = { path = "../to_shmem_derive" } +to_shmem = { workspace = true} +to_shmem_derive = { workspace = true} url = { version = "2.5", optional = true } diff --git a/stylo_atoms/Cargo.toml b/stylo_atoms/Cargo.toml new file mode 100644 index 0000000000..e39f28f9b1 --- /dev/null +++ b/stylo_atoms/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "stylo_atoms" +version.workspace = true +authors = ["The Servo Project Developers"] +documentation = "https://docs.rs/stylo_atoms/" +description = "Interned string type for the Servo and Stylo projects" +repository = "https://github.com/servo/stylo" +license = "MPL-2.0" +edition = "2018" +build = "build.rs" +readme = "../README.md" + +[lib] +path = "lib.rs" + +[dependencies] +string_cache = "0.9" + +[build-dependencies] +string_cache_codegen = "0.6.1" diff --git a/stylo_atoms/build.rs b/stylo_atoms/build.rs new file mode 100644 index 0000000000..b5f6775724 --- /dev/null +++ b/stylo_atoms/build.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; + +fn main() { + let static_atoms = + Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("static_atoms.txt"); + let static_atoms = BufReader::new(File::open(&static_atoms).unwrap()); + let mut atom_type = string_cache_codegen::AtomType::new("Atom", "atom!"); + + macro_rules! predefined { + ($($name: expr,)+) => { + { + $( + atom_type.atom($name); + )+ + } + } + } + include!("./predefined_counter_styles.rs"); + + atom_type + .atoms(static_atoms.lines().map(Result::unwrap)) + .write_to_file(&Path::new(&env::var_os("OUT_DIR").unwrap()).join("atom.rs")) + .unwrap(); +} diff --git a/stylo_atoms/lib.rs b/stylo_atoms/lib.rs new file mode 100644 index 0000000000..03560a40c0 --- /dev/null +++ b/stylo_atoms/lib.rs @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +include!(concat!(env!("OUT_DIR"), "/atom.rs")); diff --git a/stylo_atoms/predefined_counter_styles.rs b/stylo_atoms/predefined_counter_styles.rs new file mode 100644 index 0000000000..f376981e32 --- /dev/null +++ b/stylo_atoms/predefined_counter_styles.rs @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + + // THIS FILE IS DUPLICATED FROM style/counter_style/predefined.rs. + // TO UPDATE IT: + // - Run `python style/counter_style/updated_predefined.py` + // - Re-copy style/counter_style/predefined.rs to this location + +predefined! { + "decimal", + "decimal-leading-zero", + "arabic-indic", + "armenian", + "upper-armenian", + "lower-armenian", + "bengali", + "cambodian", + "khmer", + "cjk-decimal", + "devanagari", + "georgian", + "gujarati", + "gurmukhi", + "hebrew", + "kannada", + "lao", + "malayalam", + "mongolian", + "myanmar", + "oriya", + "persian", + "lower-roman", + "upper-roman", + "tamil", + "telugu", + "thai", + "tibetan", + "lower-alpha", + "lower-latin", + "upper-alpha", + "upper-latin", + "cjk-earthly-branch", + "cjk-heavenly-stem", + "lower-greek", + "hiragana", + "hiragana-iroha", + "katakana", + "katakana-iroha", + "disc", + "circle", + "square", + "disclosure-open", + "disclosure-closed", + "japanese-informal", + "japanese-formal", + "korean-hangul-formal", + "korean-hanja-informal", + "korean-hanja-formal", + "simp-chinese-informal", + "simp-chinese-formal", + "trad-chinese-informal", + "trad-chinese-formal", + "cjk-ideographic", + "ethiopic-numeric", +} diff --git a/stylo_atoms/static_atoms.txt b/stylo_atoms/static_atoms.txt new file mode 100644 index 0000000000..f9b3eecb11 --- /dev/null +++ b/stylo_atoms/static_atoms.txt @@ -0,0 +1,186 @@ +-moz-content-preferred-color-scheme +-moz-device-pixel-ratio +-moz-fixed-pos-containing-block +-moz-gtk-csd-close-button-position +-moz-gtk-csd-maximize-button-position +-moz-gtk-csd-menu-radius +-moz-gtk-csd-minimize-button-position +-moz-gtk-csd-titlebar-button-spacing +-moz-gtk-csd-titlebar-radius +-moz-gtk-csd-tooltip-radius +-moz-gtk-menu-radius +-moz-mac-titlebar-height +-moz-overlay-scrollbar-fade-duration +DOMContentLoaded +abort +activate +addtrack +animationcancel +animationend +animationiteration +animationstart +aspect-ratio +beforetoggle +beforeunload +block-size +button +canplay +canplaythrough +center +change +characteristicvaluechanged +checkbox +cancel +click +close +closing +color +command +complete +compositionend +compositionstart +compositionupdate +controllerchange +cursive +dark +datachannel +date +datetime-local +dir +device-pixel-ratio +durationchange +email +emptied +end +ended +error +fantasy +fetch +file +fill +fill-opacity +formdata +fullscreenchange +fullscreenerror +gattserverdisconnected +hairline +hashchange +height +hidden +icecandidate +iceconnectionstatechange +icegatheringstatechange +image +inline-size +input +inputsourceschange +invalid +keydown +keypress +kind +left +light +ltr +load +loadeddata +loadedmetadata +loadend +loadstart +message +message +messageerror +monospace +month +mousedown +mousemove +mouseover +mouseup +negotiationneeded +none +normal +number +onchange +open +orientation +pagehide +pageshow +password +pause +play +playing +popstate +postershown +prefers-color-scheme +print +progress +radio +range +ratechange +readystatechange +referrer +reftest-wait +rejectionhandled +removetrack +reset +resize +resolution +resourcetimingbufferfull +right +rtl +sans-serif +safe-area-inset-top +safe-area-inset-bottom +safe-area-inset-left +safe-area-inset-right +scan +screen +scroll-position +scrollbar-inline-size +search +seeked +seeking +select +selectend +selectionchange +selectstart +serif +sessionavailable +show +signalingstatechange +slotchange +squeeze +squeezeend +squeezestart +srclang +statechange +stroke +stroke-opacity +storage +submit +suspend +system-ui +tel +text +time +timeupdate +toggle +track +transitioncancel +transitionend +transitionrun +transitionstart +uncapturederror +unhandledrejection +unload +url +visibilitychange +volumechange +waiting +webglcontextcreationerror +webkitAnimationEnd +webkitAnimationIteration +webkitAnimationStart +webkitTransitionEnd +webkitTransitionRun +week +width diff --git a/stylo_dom/Cargo.toml b/stylo_dom/Cargo.toml new file mode 100644 index 0000000000..5d25f57c50 --- /dev/null +++ b/stylo_dom/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stylo_dom" +version.workspace = true +authors = ["The Servo Project Developers"] +documentation = "https://docs.rs/stylo_dom/" +description = "DOM state types for Stylo" +repository = "https://github.com/servo/stylo" +keywords = ["css", "style"] +license = "MPL-2.0" +edition = "2021" +readme = "../README.md" + +[lib] +path = "lib.rs" + +[dependencies] +bitflags = "2" +malloc_size_of = { workspace = true } diff --git a/stylo_dom/lib.rs b/stylo_dom/lib.rs new file mode 100644 index 0000000000..98a0330cf4 --- /dev/null +++ b/stylo_dom/lib.rs @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use bitflags::bitflags; +use malloc_size_of::malloc_size_of_is_0; + +pub const HEADING_LEVEL_OFFSET: usize = 52; + +// DOM types to be shared between Rust and C++. +bitflags! { + /// Event-based element states. + #[repr(C)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ElementState: u64 { + /// The mouse is down on this element. + /// + /// FIXME(#7333): set/unset this when appropriate + const ACTIVE = 1 << 0; + /// This element has focus. + /// + const FOCUS = 1 << 1; + /// The mouse is hovering over this element. + /// + const HOVER = 1 << 2; + /// Content is enabled (and can be disabled). + /// + const ENABLED = 1 << 3; + /// Content is disabled. + /// + const DISABLED = 1 << 4; + /// Content is checked. + /// + const CHECKED = 1 << 5; + /// + const INDETERMINATE = 1 << 6; + /// + const PLACEHOLDER_SHOWN = 1 << 7; + /// + const URLTARGET = 1 << 8; + /// + const FULLSCREEN = 1 << 9; + /// + const VALID = 1 << 10; + /// + const INVALID = 1 << 11; + /// + const USER_VALID = 1 << 12; + /// + const USER_INVALID = 1 << 13; + /// All the validity bits at once. + const VALIDITY_STATES = Self::VALID.bits() | Self::INVALID.bits() | Self::USER_VALID.bits() | Self::USER_INVALID.bits(); + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-broken + const BROKEN = 1 << 14; + /// + const REQUIRED = 1 << 15; + /// + /// We use an underscore to workaround a silly windows.h define. + const OPTIONAL_ = 1 << 16; + /// + const DEFINED = 1 << 17; + /// + const VISITED = 1 << 18; + /// + const UNVISITED = 1 << 19; + /// + const VISITED_OR_UNVISITED = Self::VISITED.bits() | Self::UNVISITED.bits(); + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-drag-over + const DRAGOVER = 1 << 20; + /// + const INRANGE = 1 << 21; + /// + const OUTOFRANGE = 1 << 22; + /// + const READONLY = 1 << 23; + /// + const READWRITE = 1 << 24; + /// + const DEFAULT = 1 << 25; + /// Non-standard & undocumented. + const OPTIMUM = 1 << 26; + /// Non-standard & undocumented. + const SUB_OPTIMUM = 1 << 27; + /// Non-standard & undocumented. + const SUB_SUB_OPTIMUM = 1 << 28; + /// All the above bits in one place. + const METER_OPTIMUM_STATES = Self::OPTIMUM.bits() | Self::SUB_OPTIMUM.bits() | Self::SUB_SUB_OPTIMUM.bits(); + /// Non-standard & undocumented. + const INCREMENT_SCRIPT_LEVEL = 1 << 29; + /// + const FOCUSRING = 1 << 30; + /// + const FOCUS_WITHIN = 1u64 << 31; + /// :dir matching; the states are used for dynamic change detection. + /// State that elements that match :dir(ltr) are in. + const LTR = 1u64 << 32; + /// State that elements that match :dir(rtl) are in. + const RTL = 1u64 << 33; + /// State that HTML elements that have a "dir" attr are in. + const HAS_DIR_ATTR = 1u64 << 34; + /// State that HTML elements with dir="ltr" (or something + /// case-insensitively equal to "ltr") are in. + const HAS_DIR_ATTR_LTR = 1u64 << 35; + /// State that HTML elements with dir="rtl" (or something + /// case-insensitively equal to "rtl") are in. + const HAS_DIR_ATTR_RTL = 1u64 << 36; + /// State that HTML elements without a valid-valued "dir" attr or + /// any HTML elements (including ) with dir="auto" (or something + /// case-insensitively equal to "auto") are in. + const HAS_DIR_ATTR_LIKE_AUTO = 1u64 << 37; + /// Non-standard & undocumented. + const AUTOFILL = 1u64 << 38; + /// Non-standard & undocumented. + const AUTOFILL_PREVIEW = 1u64 << 39; + /// State for modal elements: + /// + const MODAL = 1u64 << 40; + /// + const INERT = 1u64 << 41; + /// State for the topmost modal element in top layer + const TOPMOST_MODAL = 1u64 << 42; + /// Initially used for the devtools highlighter, but now somehow only + /// used for the devtools accessibility inspector. + const DEVTOOLS_HIGHLIGHTED = 1u64 << 43; + /// Used for the devtools style editor. Probably should go away. + const STYLEEDITOR_TRANSITIONING = 1u64 << 44; + /// For :-moz-value-empty (to show widgets like the reveal password + /// button or the clear button). + const VALUE_EMPTY = 1u64 << 45; + /// For :-moz-revealed. + const REVEALED = 1u64 << 46; + /// https://html.spec.whatwg.org/#selector-popover-open + /// Match element's popover visibility state of showing + const POPOVER_OPEN = 1u64 << 47; + /// https://drafts.csswg.org/css-scoping-1/#the-has-slotted-pseudo + /// Match whether a slot element has assigned nodes + const HAS_SLOTTED = 1u64 << 48; + /// https://drafts.csswg.org/selectors-4/#open-state + /// Match whether an openable element is currently open + const OPEN = 1u64 << 49; + /// For :active-view-transition. + /// + const ACTIVE_VIEW_TRANSITION = 1u64 << 50; + /// For :-moz-suppress-for-print-selection. + const SUPPRESS_FOR_PRINT_SELECTION = 1u64 << 51; + /// https://drafts.csswg.org/selectors-5/#headings + /// These 4 bits are used to pack the elements heading level into the element state + /// Heading levels can be from 1-9 so 4 bits allows us to express the full range. + const HEADING_LEVEL_BITS = 0b1111u64 << HEADING_LEVEL_OFFSET; + + /// Some convenience unions. + const DIR_STATES = Self::LTR.bits() | Self::RTL.bits(); + + const DIR_ATTR_STATES = Self::HAS_DIR_ATTR.bits() | + Self::HAS_DIR_ATTR_LTR.bits() | + Self::HAS_DIR_ATTR_RTL.bits() | + Self::HAS_DIR_ATTR_LIKE_AUTO.bits(); + + const DISABLED_STATES = Self::DISABLED.bits() | Self::ENABLED.bits(); + + const REQUIRED_STATES = Self::REQUIRED.bits() | Self::OPTIONAL_.bits(); + } +} + +bitflags! { + /// Event-based document states. + #[repr(C)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct DocumentState: u64 { + /// Window activation status + const WINDOW_INACTIVE = 1 << 0; + /// RTL locale: specific to the XUL localedir attribute + const RTL_LOCALE = 1 << 1; + /// LTR locale: specific to the XUL localedir attribute + const LTR_LOCALE = 1 << 2; + + const ALL_LOCALEDIR_BITS = Self::LTR_LOCALE.bits() | Self::RTL_LOCALE.bits(); + } +} + +malloc_size_of_is_0!(ElementState, DocumentState); diff --git a/stylo_static_prefs/Cargo.toml b/stylo_static_prefs/Cargo.toml new file mode 100644 index 0000000000..3274bf47fe --- /dev/null +++ b/stylo_static_prefs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stylo_static_prefs" +version.workspace = true +authors = ["The Servo Project Developers"] +documentation = "https://docs.rs/stylo_static_prefs/" +description = "Configuration for Stylo" +repository = "https://github.com/servo/stylo" +keywords = ["css", "style"] +license = "MPL-2.0" +edition = "2021" +readme = "../README.md" diff --git a/stylo_static_prefs/src/lib.rs b/stylo_static_prefs/src/lib.rs new file mode 100644 index 0000000000..1f6203b23d --- /dev/null +++ b/stylo_static_prefs/src/lib.rs @@ -0,0 +1,253 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; +use std::sync::{LazyLock, RwLock}; + +/// Returns the default value for a preference exposed to the style crate. +/// This is what will be used if the embedder has not set the preference. +#[macro_export] +macro_rules! default_value { + ("layout.columns.enabled") => { + false + }; + ("layout.container-queries.enabled") => { + false + }; + ("layout.css.anchor-positioning.enabled") => { + false + }; + ("layout.css.at-scope.enabled") => { + false + }; + ("layout.css.attr.enabled") => { + false + }; + ("layout.css.basic-shape-shape.enabled") => { + false + }; + ("layout.css.color-mix-multi-color.enabled") => { + false + }; + ("layout.css.content.alt-text.enabled") => { + false + }; + ("layout.css.contrast-color.enabled") => { + true + }; + ("layout.css.custom-media.enabled") => { + false + }; + ("layout.css.fit-content-function.enabled") => { + true + }; + ("layout.css.font-palette.enabled") => { + false + }; + ("layout.css.font-tech.enabled") => { + false + }; + ("layout.css.font-variations.enabled") => { + true + }; + ("layout.css.gradient-color-interpolation-method.enabled") => { + true + }; + ("layout.css.light-dark.images.enabled") => { + false + }; + ("layout.css.margin-rules.enabled") => { + false + }; + ("layout.css.motion-path-url.enabled") => { + false + }; + ("layout.css.outline-offset.snapping") => { + 1 + }; + ("layout.css.properties-and-values.enabled") => { + true + }; + ("layout.css.relative-color-syntax.enabled") => { + true + }; + ("layout.css.scroll-driven-animations.enabled") => { + false + }; + ("layout.css.scroll-state.enabled") => { + false + }; + ("layout.css.starting-style-at-rules.enabled") => { + false + }; + ("layout.css.stretch-size-keyword.enabled") => { + true + }; + ("layout.css.style-queries.enabled") => { + false + }; + ("layout.css.stylo-local-work-queue.in-main-thread") => { + 32 + }; + ("layout.css.stylo-local-work-queue.in-worker") => { + 0 + }; + ("layout.css.stylo-work-unit-size") => { + 16 + }; + ("layout.css.system-ui.enabled") => { + true + }; + ("layout.css.webkit-fill-available.all-size-properties.enabled") => { + true + }; + ("layout.css.webkit-fill-available.enabled") => { + true + }; + ("layout.grid.enabled") => { + false + }; + ("layout.threads") => { + // Negative means auto, 0 disables the thread-pool (main-thread styling), + // other numbers override as specified. + -1 + }; + ("layout.unimplemented") => { + false + }; + ("layout.variable_fonts.enabled") => { + false + }; + ("layout.writing-mode.enabled") => { + false + }; +} + +/// Returns the value of a preference exposed to the style crate. If the embedder +/// has not set a value for it, this returns the default value of the preference. +#[macro_export] +macro_rules! pref { + ($string:tt) => { + $crate::Preference::get($string, $crate::default_value!($string)) + }; +} + +/// Sets a preference to the provided value. +#[macro_export] +macro_rules! set_pref { + ($string:tt, $($value:expr)+) => { + let value = $($value)+; + // This comparison ensures that the provided value belongs to the expected type. + let _ = $crate::default_value!($string) == value; + $crate::Preference::set($string, value) + }; +} + +static PREFS: LazyLock = LazyLock::new(Preferences::default); + +#[derive(Debug, Default)] +struct Preferences { + bool_prefs: RwLock>, + i32_prefs: RwLock>, +} + +impl Preferences { + pub fn get_bool(&self, key: &str, default: bool) -> bool { + let prefs = self.bool_prefs.read().expect("RwLock is poisoned"); + *prefs.get(key).unwrap_or(&default) + } + + pub fn get_i32(&self, key: &str, default: i32) -> i32 { + let prefs = self.i32_prefs.read().expect("RwLock is poisoned"); + *prefs.get(key).unwrap_or(&default) + } + + pub fn set_bool(&self, key: &str, value: bool) { + let mut prefs = self.bool_prefs.write().expect("RwLock is poisoned"); + + // Avoid cloning the key if it exists. + if let Some(pref) = prefs.get_mut(key) { + *pref = value; + } else { + prefs.insert(key.to_owned(), value); + } + } + + pub fn set_i32(&self, key: &str, value: i32) { + let mut prefs = self.i32_prefs.write().expect("RwLock is poisoned"); + + // Avoid cloning the key if it exists. + if let Some(pref) = prefs.get_mut(key) { + *pref = value; + } else { + prefs.insert(key.to_owned(), value); + } + } +} + +pub trait Preference: Sized { + /// Gets the value of a preference, falling back to the provided default. + /// Prefer using [`pref!()`] instead, since it ensures that the preference + /// exists and uses the correct default automatically. + fn get(key: &str, default: Self) -> Self; + + /// Sets a preference to the provided value. Prefer using [`set_pref!()`] + /// instead, since it ensures that the preference exists and the value + /// belongs to the expected type. + fn set(key: &str, value: Self); +} + +impl Preference for bool { + fn get(key: &str, default: Self) -> Self { + PREFS.get_bool(key, default) + } + fn set(key: &str, value: Self) { + PREFS.set_bool(key, value) + } +} + +impl Preference for i32 { + fn get(key: &str, default: Self) -> Self { + PREFS.get_i32(key, default) + } + fn set(key: &str, value: Self) { + PREFS.set_i32(key, value) + } +} + +#[test] +fn test() { + let prefs = Preferences::default(); + + // We get the default value when the pref is not set. + assert_eq!(prefs.get_bool("foo", false), false); + assert_eq!(prefs.get_bool("foo", true), true); + assert_eq!(prefs.get_i32("bar", 0), 0); + assert_eq!(prefs.get_i32("bar", 1), 1); + assert_eq!(prefs.get_i32("bar", 2), 2); + + // Prefs can be set and retrieved. + prefs.set_bool("foo", true); + prefs.set_i32("bar", 1); + assert_eq!(prefs.get_bool("foo", false), true); + assert_eq!(prefs.get_bool("foo", true), true); + assert_eq!(prefs.get_i32("bar", 0), 1); + assert_eq!(prefs.get_i32("bar", 1), 1); + assert_eq!(prefs.get_i32("bar", 2), 1); + prefs.set_bool("foo", false); + prefs.set_i32("bar", 2); + assert_eq!(prefs.get_bool("foo", false), false); + assert_eq!(prefs.get_bool("foo", true), false); + assert_eq!(prefs.get_i32("bar", 0), 2); + assert_eq!(prefs.get_i32("bar", 1), 2); + assert_eq!(prefs.get_i32("bar", 2), 2); + + // Each value type currently has an independent namespace. + prefs.set_i32("foo", 3); + prefs.set_bool("bar", true); + assert_eq!(prefs.get_i32("foo", 0), 3); + assert_eq!(prefs.get_bool("foo", false), false); + assert_eq!(prefs.get_bool("bar", false), true); + assert_eq!(prefs.get_i32("bar", 0), 2); +} diff --git a/sync.sh b/sync.sh index e92182c746..68c8689c9c 100755 --- a/sync.sh +++ b/sync.sh @@ -30,7 +30,7 @@ fi step Cloning upstream if needed if ! [ -e upstream ]; then - git clone --bare --single-branch --progress https://github.com/mozilla/gecko-dev.git upstream + git clone --bare --single-branch --branch main --progress https://github.com/mozilla-firefox/firefox.git upstream fi step Updating upstream diff --git a/to_shmem/Cargo.toml b/to_shmem/Cargo.toml index 60bcd524f6..81f7fed5d5 100644 --- a/to_shmem/Cargo.toml +++ b/to_shmem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "to_shmem" -version = "0.1.0" +version = "0.3.0" authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" @@ -24,8 +24,8 @@ thin-vec = ["dep:thin-vec"] [dependencies] cssparser = { version = "0.36", optional = true } -servo_arc = { version = "0.4.0", path = "../servo_arc", optional = true } +servo_arc = { workspace = true, optional = true } smallbitvec = { version = "2.3.0", optional = true } smallvec = { version = "1.13", optional = true } -string_cache = { version = "0.8", optional = true } +string_cache = { version = "0.9", optional = true } thin-vec = { version = "0.2.1", optional = true } From b9eb42d528bb5b3117b5b9af3e26594f913df3c3 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 12:57:52 +0100 Subject: [PATCH 03/30] Fixup for https://phabricator.services.mozilla.com/D281332 Signed-off-by: Oriol Brufau --- style/properties/shorthands.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/style/properties/shorthands.rs b/style/properties/shorthands.rs index 141c8af862..9557655763 100644 --- a/style/properties/shorthands.rs +++ b/style/properties/shorthands.rs @@ -3141,6 +3141,7 @@ pub mod font_synthesis { } } +#[cfg(feature = "gecko")] pub mod text_box { pub use crate::properties::generated::shorthands::text_box::*; From 025b43a4d649a94cef33f13d54f45c1948e16a7b Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 12:47:38 +0100 Subject: [PATCH 04/30] Fixup for https://phabricator.services.mozilla.com/D280938 Signed-off-by: Oriol Brufau --- style/properties/properties.mako.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index ef2b2432aa..fc8b713d2a 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -1952,6 +1952,14 @@ impl ComputedValues { } } } + + /// Calls the given function for each cached lazy pseudo-element style. + pub fn each_cached_lazy_pseudo(&self, mut _f: F) + where + F: FnMut(&Self), + { + // Servo doesn't currently cache lazy pseudo-element styles. + } } #[cfg(feature = "servo")] From cfae27b84990f1cd07c49738e9131edd5d3ac643 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 13:00:36 +0100 Subject: [PATCH 05/30] Fixup for https://phabricator.services.mozilla.com/D280243 Signed-off-by: Oriol Brufau --- style/servo/selector_parser.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/style/servo/selector_parser.rs b/style/servo/selector_parser.rs index cdb4794efe..4452cec1e7 100644 --- a/style/servo/selector_parser.rs +++ b/style/servo/selector_parser.rs @@ -290,6 +290,25 @@ impl PseudoElement { true } + + /// Whether this pseudo-element is the ::highlight pseudo. + pub fn is_highlight(&self) -> bool { + false + } + + /// Whether this pseudo-element is the ::target-text pseudo. + #[inline] + pub fn is_target_text(&self) -> bool { + false + } + + /// Whether this is a highlight pseudo-element that is styled lazily during + /// painting rather than during the restyle traversal. These pseudos need + /// explicit repaint triggering when their styles change. + #[inline] + pub fn is_lazy_painted_highlight_pseudo(&self) -> bool { + self.is_selection() || self.is_highlight() || self.is_target_text() + } } /// The type used for storing `:lang` arguments. From 4724d935e5275b4b85567c4c54eb4a3015801528 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 13:07:30 +0100 Subject: [PATCH 06/30] Fixup for https://phabricator.services.mozilla.com/D282398 Signed-off-by: Oriol Brufau --- stylo_static_prefs/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stylo_static_prefs/src/lib.rs b/stylo_static_prefs/src/lib.rs index 1f6203b23d..d1f4cd54f8 100644 --- a/stylo_static_prefs/src/lib.rs +++ b/stylo_static_prefs/src/lib.rs @@ -18,6 +18,9 @@ macro_rules! default_value { ("layout.css.anchor-positioning.enabled") => { false }; + ("layout.css.appearance-base-select.enabled") => { + false + }; ("layout.css.at-scope.enabled") => { false }; From a79e2751444e30bc2d7e270a72f2ec935c4c1c95 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 13:07:57 +0100 Subject: [PATCH 07/30] Fixup for https://phabricator.services.mozilla.com/D284054 Signed-off-by: Oriol Brufau --- stylo_static_prefs/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stylo_static_prefs/src/lib.rs b/stylo_static_prefs/src/lib.rs index d1f4cd54f8..abf337ccd9 100644 --- a/stylo_static_prefs/src/lib.rs +++ b/stylo_static_prefs/src/lib.rs @@ -9,6 +9,9 @@ use std::sync::{LazyLock, RwLock}; /// This is what will be used if the embedder has not set the preference. #[macro_export] macro_rules! default_value { + ("dom.select.customizable_select.enabled") => { + false + }; ("layout.columns.enabled") => { false }; @@ -18,9 +21,6 @@ macro_rules! default_value { ("layout.css.anchor-positioning.enabled") => { false }; - ("layout.css.appearance-base-select.enabled") => { - false - }; ("layout.css.at-scope.enabled") => { false }; From 9202ae54c6f7601d20163b8ef2ad4a023ca2f08a Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 13:34:39 +0100 Subject: [PATCH 08/30] Fixup for https://phabricator.services.mozilla.com/D284378 Signed-off-by: Oriol Brufau --- style/properties/properties.mako.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index fc8b713d2a..d0d4533d77 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -1384,6 +1384,7 @@ pub mod style_structs { self.${longhand.ident}.clone() } + /// Whether `self` and `other` have the same computed value for ${longhand.name}. #[allow(non_snake_case)] #[inline] pub fn ${longhand.ident}_equals(&self, other: &Self) -> bool { From 58f696563c8097e2bed9dab16cf420fd81155227 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 14:30:18 +0100 Subject: [PATCH 09/30] Fixup for https://phabricator.services.mozilla.com/D284172 Signed-off-by: Oriol Brufau --- style/queries/feature_expression.rs | 2 +- style/values/mod.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/style/queries/feature_expression.rs b/style/queries/feature_expression.rs index fb8801afd9..b2916eb060 100644 --- a/style/queries/feature_expression.rs +++ b/style/queries/feature_expression.rs @@ -941,7 +941,7 @@ impl QueryStyleRange { QueryExpressionValue::Custom(ident) => { // `ident` is the dashed ident, but we need the name // without "--" for custom-property lookup. - let name = Atom::from(&ident.0.as_slice()[2..]); + let name = ident.undashed(); let stylist = context .builder .stylist diff --git a/style/values/mod.rs b/style/values/mod.rs index 455d646a77..d511b01dc4 100644 --- a/style/values/mod.rs +++ b/style/values/mod.rs @@ -646,6 +646,20 @@ impl DashedIdent { pub fn is_empty(&self) -> bool { self.0 == atom!("") } + + /// Returns an atom with the same value, but without the starting "--". + /// + /// # Panics + /// + /// Panics when used on the special `DashedIdent::empty()`. + pub(crate) fn undashed(&self) -> Atom { + assert!(!self.is_empty(), "Can't undash the empty DashedIdent"); + #[cfg(feature = "gecko")] + let name = &self.0.as_slice()[2..]; + #[cfg(feature = "servo")] + let name = &self.0[2..]; + Atom::from(name) + } } impl Parse for DashedIdent { From 4ddfe54d06bfca34761802a0412a57c111b00429 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 14:41:48 +0100 Subject: [PATCH 10/30] Fixup for https://phabricator.services.mozilla.com/D284297 Signed-off-by: Oriol Brufau --- style/values/specified/image.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/style/values/specified/image.rs b/style/values/specified/image.rs index 657e706bc6..7be7a1c87d 100644 --- a/style/values/specified/image.rs +++ b/style/values/specified/image.rs @@ -1291,7 +1291,12 @@ impl PaintWorklet { .try_parse(|input| { input.expect_comma()?; input.parse_comma_separated(|input| { - SpecifiedValue::parse(input, &context.url_data).map(Arc::new) + SpecifiedValue::parse( + input, + Some(&context.namespaces.prefixes), + &context.url_data, + ) + .map(Arc::new) }) }) .unwrap_or_default(); From ecf17b54251b2f380a186780811f3c234308f57d Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 5 Mar 2026 21:16:37 +0100 Subject: [PATCH 11/30] Bump versions - Bump stylo_* to 0.13.0 - Bump selectors to 0.36.0 Signed-off-by: Oriol Brufau --- Cargo.toml | 18 +++++++++--------- selectors/Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 93acb338d2..d009fb5de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,20 +17,20 @@ members = [ default-members = ["style"] [workspace.package] -version = "0.12.0" +version = "0.13.0" [workspace.dependencies] # in-repo dependencies (separately versioned) servo_arc = { version = "0.4.3", path = "./servo_arc" } -selectors = { version = "0.35.0", path = "./selectors" } +selectors = { version = "0.36.0", path = "./selectors" } to_shmem = { version = "0.3.0", path = "./to_shmem", features = ["servo"] } to_shmem_derive = { version = "0.1.0", path = "./to_shmem_derive" } # in-repo dependencies (main version) -malloc_size_of = { version = "0.12.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } -static_prefs = { version = "0.12.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } -stylo_atoms = { version = "0.12.0", path = "./stylo_atoms" } -dom = { version = "0.12.0", path = "./stylo_dom", package = "stylo_dom" } -style_traits = { version = "0.12.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} -style_derive = { version = "0.12.0", path = "./style_derive", package = "stylo_derive"} -stylo = { version = "0.12.0", path = "./style" } +malloc_size_of = { version = "0.13.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } +static_prefs = { version = "0.13.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } +stylo_atoms = { version = "0.13.0", path = "./stylo_atoms" } +dom = { version = "0.13.0", path = "./stylo_dom", package = "stylo_dom" } +style_traits = { version = "0.13.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} +style_derive = { version = "0.13.0", path = "./style_derive", package = "stylo_derive"} +stylo = { version = "0.13.0", path = "./style" } diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index d566b36fe8..c67d028258 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selectors" -version = "0.35.0" +version = "0.36.0" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/selectors/" description = "CSS Selectors matching for Rust" From f34b80b82842191fa7680d7524931f6ded07c756 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Sun, 8 Mar 2026 17:16:14 +0100 Subject: [PATCH 12/30] Add beforeinput as atom (#326) Required for firing events for contenteditable Signed-off-by: Tim van der Lippe --- stylo_atoms/static_atoms.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/stylo_atoms/static_atoms.txt b/stylo_atoms/static_atoms.txt index f9b3eecb11..bff093bb35 100644 --- a/stylo_atoms/static_atoms.txt +++ b/stylo_atoms/static_atoms.txt @@ -20,6 +20,7 @@ animationend animationiteration animationstart aspect-ratio +beforeinput beforetoggle beforeunload block-size From 1d2177035f79863fa42fd09cd18122b3b2322c28 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Sun, 8 Mar 2026 22:53:41 +0100 Subject: [PATCH 13/30] Add logic for registering custom properties from script (#325) This way it can be shared among Gecko and Servo. Stylo PR: https://github.com/servo/servo/pull/43085 Upstream: https://bugzilla.mozilla.org/show_bug.cgi?id=2021743 Signed-off-by: Oriol Brufau --- style/stylist.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/style/stylist.rs b/style/stylist.rs index fa143b2a16..be2e08e2c4 100644 --- a/style/stylist.rs +++ b/style/stylist.rs @@ -10,6 +10,7 @@ use crate::applicable_declarations::{ use crate::computed_value_flags::ComputedValueFlags; use crate::context::{CascadeInputs, QuirksMode}; use crate::custom_properties::ComputedCustomProperties; +use crate::custom_properties::{parse_name, SpecifiedValue}; use crate::derives::*; use crate::dom::TElement; #[cfg(feature = "gecko")] @@ -32,6 +33,8 @@ use crate::properties::{ use crate::properties_and_values::registry::{ PropertyRegistration, PropertyRegistrationData, ScriptRegistry as CustomPropertyScriptRegistry, }; +use crate::properties_and_values::rule::{Inherits, PropertyRegistrationError, PropertyRuleName}; +use crate::properties_and_values::syntax::Descriptor; use crate::rule_cache::{RuleCache, RuleCacheConditions}; use crate::rule_collector::RuleCollector; use crate::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource}; @@ -49,6 +52,7 @@ use crate::stylesheets::scope_rule::{ collect_scope_roots, element_is_outside_of_scope, scope_selector_list_is_trivial, ImplicitScopeRoot, ScopeRootCandidate, ScopeSubjectMap, ScopeTarget, }; +use crate::stylesheets::UrlExtraData; use crate::stylesheets::{ CounterStyleRule, CssRule, CssRuleRef, EffectiveRulesIterator, FontFaceRule, FontFeatureValuesRule, FontPaletteValuesRule, Origin, OriginSet, PagePseudoClassFlags, @@ -58,9 +62,10 @@ use crate::stylesheets::{CustomMediaEvaluator, CustomMediaMap}; #[cfg(feature = "gecko")] use crate::values::specified::position::PositionTryFallbacksItem; use crate::values::specified::position::PositionTryFallbacksTryTactic; -use crate::values::{computed, AtomIdent}; +use crate::values::{computed, AtomIdent, Parser, SourceLocation}; use crate::AllocErr; use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded, WeakAtom}; +use cssparser::ParserInput; use dom::{DocumentState, ElementState}; #[cfg(feature = "gecko")] use malloc_size_of::MallocUnconditionalShallowSizeOf; @@ -2020,6 +2025,97 @@ impl Stylist { } } +#[allow(missing_docs)] +#[repr(u8)] +pub enum RegisterCustomPropertyResult { + SuccessfullyRegistered, + InvalidName, + AlreadyRegistered, + InvalidSyntax, + NoInitialValue, + InvalidInitialValue, + InitialValueNotComputationallyIndependent, +} + +impl Stylist { + /// + pub fn register_custom_property( + &mut self, + url_data: &UrlExtraData, + name: &str, + syntax: &str, + inherits: bool, + initial_value: Option<&str>, + ) -> RegisterCustomPropertyResult { + use RegisterCustomPropertyResult::*; + + // If name is not a custom property name string, throw a SyntaxError and exit this algorithm. + let Ok(name) = parse_name(name).map(Atom::from) else { + return InvalidName; + }; + + // If property set already contains an entry with name as its property name (compared + // codepoint-wise), throw an InvalidModificationError and exit this algorithm. + if self.custom_property_script_registry().get(&name).is_some() { + return AlreadyRegistered; + } + // Attempt to consume a syntax definition from syntax. If it returns failure, throw a + // SyntaxError. Otherwise, let syntax definition be the returned syntax definition. + let Ok(syntax) = Descriptor::from_str(syntax, /* preserve_specified = */ false) else { + return InvalidSyntax; + }; + + let initial_value = match initial_value { + Some(value) => { + let mut input = ParserInput::new(value); + let parsed = Parser::new(&mut input) + .parse_entirely(|input| { + input.skip_whitespace(); + SpecifiedValue::parse(input, None, url_data).map(Arc::new) + }) + .ok(); + if parsed.is_none() { + return InvalidInitialValue; + } + parsed + }, + None => None, + }; + + if let Err(error) = + PropertyRegistration::validate_initial_value(&syntax, initial_value.as_deref()) + { + return match error { + PropertyRegistrationError::InitialValueNotComputationallyIndependent => { + InitialValueNotComputationallyIndependent + }, + PropertyRegistrationError::InvalidInitialValue => InvalidInitialValue, + PropertyRegistrationError::NoInitialValue => NoInitialValue, + }; + } + + let property_registration = PropertyRegistration { + name: PropertyRuleName(name), + data: PropertyRegistrationData { + syntax, + inherits: if inherits { + Inherits::True + } else { + Inherits::False + }, + initial_value, + }, + url_data: url_data.clone(), + source_location: SourceLocation { line: 0, column: 0 }, + }; + self.custom_property_script_registry_mut() + .register(property_registration); + self.rebuild_initial_values_for_custom_properties(); + + SuccessfullyRegistered + } +} + /// A vector that is sorted in layer order. #[derive(Clone, Debug, Deref, MallocSizeOf)] pub struct LayerOrderedVec(Vec<(T, LayerId)>); From 07364a6c67c08276a9ed0a9bba031ea99f808502 Mon Sep 17 00:00:00 2001 From: Steven Novaryo <65610990+stevennovaryo@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:04:32 +0800 Subject: [PATCH 14/30] Make `::placeholder` public and add property restriction for `::placeholder` and `::marker` (#322) The `::placeholder` pseudo element should be a public pseudo element. And, both `::placeholder` and `::marker` should have a property restriction as defined in the spec. In stylo, the property restriction has been computed in `PropertyFlags`. Servo PR: https://github.com/servo/servo/pull/43053 Signed-off-by: stevennovaryo --- style/servo/selector_parser.rs | 21 +++++++++++---------- stylo_static_prefs/src/lib.rs | 3 +++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/style/servo/selector_parser.rs b/style/servo/selector_parser.rs index 4452cec1e7..2b2a0d4202 100644 --- a/style/servo/selector_parser.rs +++ b/style/servo/selector_parser.rs @@ -49,9 +49,9 @@ pub enum PseudoElement { // If/when :first-line is added, update is_first_line accordingly. - // If/when ::first-letter, ::first-line, or ::placeholder are added, adjust - // our property_restriction implementation to do property filtering for - // them. Also, make sure the UA sheet has the !important rules some of the + // If/when ::first-letter or ::first-line are added, adjust our + // property_restriction implementation to do property filtering for them. + // Also, make sure the UA sheet has the !important rules some of the // APPLIES_TO_PLACEHOLDER properties expect! // Non-eager pseudos. @@ -274,7 +274,13 @@ impl PseudoElement { /// Property flag that properties must have to apply to this pseudo-element. #[inline] pub fn property_restriction(&self) -> Option { - None + Some(match self { + PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => { + PropertyFlags::APPLIES_TO_MARKER + }, + PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER, + _ => return None, + }) } /// Whether this pseudo-element should actually exist if it has @@ -664,12 +670,7 @@ impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { }, "details-content" => DetailsContent, "color-swatch" => ColorSwatch, - "placeholder" => { - if !self.in_user_agent_stylesheet() { - return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) - } - Placeholder - }, + "placeholder" => Placeholder, "-servo-text-control-inner-container" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) diff --git a/stylo_static_prefs/src/lib.rs b/stylo_static_prefs/src/lib.rs index abf337ccd9..bf07907305 100644 --- a/stylo_static_prefs/src/lib.rs +++ b/stylo_static_prefs/src/lib.rs @@ -63,6 +63,9 @@ macro_rules! default_value { ("layout.css.margin-rules.enabled") => { false }; + ("layout.css.marker.restricted") => { + true + }; ("layout.css.motion-path-url.enabled") => { false }; From 51593b0258309bfbbee767b3c116bc7ce6bc1b80 Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Mon, 9 Mar 2026 17:56:13 +0800 Subject: [PATCH 15/30] Remove parse condition `step_position_jump_enabled` (#323) This enables `jump-start`, `jump-end`, `jump-none`, `jump-both`. The feature is left here 7 years ago. As tested with examples, all `step-easing-function`s work as expected. Servo PR: https://github.com/servo/servo/pull/43061 --------- Signed-off-by: Euclid Ye Signed-off-by: Euclid Ye > --- style/values/generics/easing.rs | 15 --------------- style/values/specified/easing.rs | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/style/values/generics/easing.rs b/style/values/generics/easing.rs index 1a8eaa229f..ee03c705b2 100644 --- a/style/values/generics/easing.rs +++ b/style/values/generics/easing.rs @@ -6,7 +6,6 @@ //! https://drafts.csswg.org/css-easing/#timing-functions use crate::derives::*; -use crate::parser::ParserContext; /// A generic easing function. #[derive( @@ -82,16 +81,6 @@ pub enum BeforeFlag { Set, } -#[cfg(feature = "gecko")] -fn step_position_jump_enabled(_context: &ParserContext) -> bool { - true -} - -#[cfg(feature = "servo")] -fn step_position_jump_enabled(_context: &ParserContext) -> bool { - false -} - #[allow(missing_docs)] #[derive( Clone, @@ -110,13 +99,9 @@ fn step_position_jump_enabled(_context: &ParserContext) -> bool { )] #[repr(u8)] pub enum StepPosition { - #[parse(condition = "step_position_jump_enabled")] JumpStart, - #[parse(condition = "step_position_jump_enabled")] JumpEnd, - #[parse(condition = "step_position_jump_enabled")] JumpNone, - #[parse(condition = "step_position_jump_enabled")] JumpBoth, Start, End, diff --git a/style/values/specified/easing.rs b/style/values/specified/easing.rs index 8c4f29ef49..ac5c9cf2c8 100644 --- a/style/values/specified/easing.rs +++ b/style/values/specified/easing.rs @@ -78,7 +78,7 @@ impl TimingFunction { let position = input .try_parse(|i| { i.expect_comma()?; - StepPosition::parse(context, i) + StepPosition::parse(i) }) .unwrap_or(StepPosition::End); From 2373b812bbfdf0d01168fd613a0e772e94d32012 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 10 Mar 2026 07:41:43 +0100 Subject: [PATCH 16/30] Restore Servo's original form control theme colors (#327) This change does two things: 1. Restores Servo's original theme colors from before support for system color was added. 2. Eliminates the selection of default system color based on the color scheme of the system. This code was dead code since `is_dark_color_scheme` always returns `false`. Servo PR: servo/servo#43107 Signed-off-by: Martin Robinson --- style/servo/media_queries.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/style/servo/media_queries.rs b/style/servo/media_queries.rs index 011c7d36fb..2b4b1a2831 100644 --- a/style/servo/media_queries.rs +++ b/style/servo/media_queries.rs @@ -508,17 +508,16 @@ impl Device { pub(crate) fn system_color( &self, system_color: SystemColor, - color_scheme: ColorSchemeFlags, + color_scheme_flags: ColorSchemeFlags, ) -> AbsoluteColor { fn srgb(r: u8, g: u8, b: u8) -> AbsoluteColor { AbsoluteColor::srgb_legacy(r, g, b, 1f32) } - let dark = self.is_dark_color_scheme(color_scheme); - // Refer to spec // - if dark { + if self.is_dark_color_scheme(color_scheme_flags) { + // Note: is_dark_color_scheme always returns true, so this code is dead code. match system_color { SystemColor::Accentcolor => srgb(10, 132, 255), SystemColor::Accentcolortext => srgb(255, 255, 255), @@ -581,14 +580,14 @@ impl Device { | SystemColor::Inactiveborder | SystemColor::Threeddarkshadow | SystemColor::Threedshadow - | SystemColor::Windowframe => srgb(0, 0, 0), + | SystemColor::Windowframe => srgb(169, 169, 169), SystemColor::Buttonface // Deprecated system colors (CSS Color 4) mapped to Buttonface. | SystemColor::Buttonhighlight | SystemColor::Buttonshadow | SystemColor::Threedface | SystemColor::Threedhighlight - | SystemColor::Threedlightshadow => srgb(240, 240, 240), + | SystemColor::Threedlightshadow => srgb(220, 220, 220), SystemColor::Buttontext => srgb(0, 0, 0), SystemColor::Canvas // Deprecated system colors (CSS Color 4) mapped to Canvas. From f00c109074832b7fa7be52bbbf8f4987d24e323b Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Tue, 10 Mar 2026 21:47:42 +0100 Subject: [PATCH 17/30] Share `Device` logic among Gecko and Servo (#330) Adds a new file `style/device/mod.rs` with the `Device` logic that is common for Gecko and Servo. The Gecko-specific logic for `Device` is moved into `style/device/gecko.rs`, and the previous `style/gecko/media_queries.rs` is removed. The Servo-specific logic for `Device` is moved into `style/device/servo.rs`. The previous `servo/media_queries.rs` also had logic for media features, which is now moved into `style/servo/media_features.rs` for consistency with Gecko. Servo PR: https://github.com/servo/servo/pull/43146 Signed-off-by: Oriol Brufau --- style/custom_properties.rs | 2 +- .../media_queries.rs => device/gecko.rs} | 288 ++----------- style/device/mod.rs | 283 +++++++++++++ .../media_queries.rs => device/servo.rs} | 379 ++---------------- style/dom.rs | 2 +- style/gecko/wrapper.rs | 2 +- style/invalidation/media_queries.rs | 2 +- style/invalidation/stylesheets.rs | 2 +- style/lib.rs | 1 + style/media_queries/media_list.rs | 3 +- style/media_queries/mod.rs | 5 - style/properties/gecko.mako.rs | 2 +- style/properties/properties.mako.rs | 2 +- style/queries/feature_expression.rs | 2 +- style/servo/media_features.rs | 87 ++++ style/servo/mod.rs | 2 +- style/stylesheet_set.rs | 2 +- style/stylesheets/document_rule.rs | 2 +- style/stylesheets/rules_iterator.rs | 2 +- style/stylesheets/stylesheet.rs | 3 +- style/stylist.rs | 2 +- style/values/computed/mod.rs | 2 +- style/values/resolved/mod.rs | 2 +- style/values/specified/color.rs | 2 +- style/values/specified/source_size_list.rs | 2 +- 25 files changed, 458 insertions(+), 625 deletions(-) rename style/{gecko/media_queries.rs => device/gecko.rs} (64%) create mode 100644 style/device/mod.rs rename style/{servo/media_queries.rs => device/servo.rs} (53%) create mode 100644 style/servo/media_features.rs diff --git a/style/custom_properties.rs b/style/custom_properties.rs index 8a4a409cc0..18df89d77c 100644 --- a/style/custom_properties.rs +++ b/style/custom_properties.rs @@ -8,8 +8,8 @@ use crate::applicable_declarations::CascadePriority; use crate::custom_properties_map::CustomPropertiesMap; +use crate::device::Device; use crate::dom::AttributeTracker; -use crate::media_queries::Device; use crate::properties::{ CSSWideKeyword, CustomDeclaration, CustomDeclarationValue, LonghandId, LonghandIdSet, PropertyDeclaration, diff --git a/style/gecko/media_queries.rs b/style/device/gecko.rs similarity index 64% rename from style/gecko/media_queries.rs rename to style/device/gecko.rs index 1066dd51dd..3391d67874 100644 --- a/style/gecko/media_queries.rs +++ b/style/device/gecko.rs @@ -2,13 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Gecko's media-query device and expression representation. +//! Gecko-specific logic for [`Device`]. use crate::color::AbsoluteColor; use crate::context::QuirksMode; use crate::custom_properties::CssEnvironment; +use crate::device::Device; use crate::font_metrics::FontMetrics; use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color}; +use crate::gecko::wrapper::GeckoElement; use crate::gecko_bindings::bindings; use crate::gecko_bindings::structs; use crate::logical_geometry::WritingMode; @@ -30,77 +32,18 @@ use euclid::{Scale, SideOffsets2D}; use parking_lot::RwLock; use servo_arc::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; -use std::{cmp, fmt, mem}; +use std::{cmp, fmt}; use style_traits::{CSSPixel, DevicePixel}; -/// The `Device` in Gecko wraps a pres context, has a default values computed, -/// and contains all the viewport rule state. -/// -/// This structure also contains atomics used for computing root font-relative -/// units. These atomics use relaxed ordering, since when computing the style -/// of the root element, there can't be any other style being computed at the -/// same time (given we need the style of the parent to compute everything else). -pub struct Device { +pub(super) struct ExtraDeviceData { /// NB: The document owns the styleset, who owns the stylist, and thus the /// `Device`, so having a raw document pointer here is fine. document: *const structs::Document, - default_values: Arc, - /// Current computed style of the root element, used for calculations of - /// root font-relative units. - root_style: RwLock>, - /// Font size of the root element, used for rem units in other elements. - root_font_size: AtomicU32, - /// Line height of the root element, used for rlh units in other elements. - root_line_height: AtomicU32, - /// X-height of the root element, used for rex units in other elements. - root_font_metrics_ex: AtomicU32, - /// Cap-height of the root element, used for rcap units in other elements. - root_font_metrics_cap: AtomicU32, - /// Advance measure (ch) of the root element, used for rch units in other elements. - root_font_metrics_ch: AtomicU32, - /// Ideographic advance measure of the root element, used for ric units in other elements. - root_font_metrics_ic: AtomicU32, /// The body text color, stored as an `nscolor`, used for the "tables /// inherit from body" quirk. /// /// body_text_color: AtomicUsize, - /// Whether any styles computed in the document relied on the root font-size - /// by using rem units. - used_root_font_size: AtomicBool, - /// Whether any styles computed in the document relied on the root line-height - /// by using rlh units. - used_root_line_height: AtomicBool, - /// Whether any styles computed in the document relied on the root font metrics - /// by using rcap, rch, rex, or ric units. This is a lock instead of an atomic - /// in order to prevent concurrent writes to the root font metric values. - used_root_font_metrics: RwLock, - /// Whether any styles computed in the document relied on font metrics. - used_font_metrics: AtomicBool, - /// Whether any styles computed in the document relied on the viewport size - /// by using vw/vh/vmin/vmax units. - used_viewport_size: AtomicBool, - /// Whether any styles computed in the document relied on the viewport size - /// by using dvw/dvh/dvmin/dvmax units. - used_dynamic_viewport_size: AtomicBool, - /// The CssEnvironment object responsible of getting CSS environment - /// variables. - environment: CssEnvironment, -} - -impl fmt::Debug for Device { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use nsstring::nsCString; - - let mut doc_uri = nsCString::new(); - unsafe { - bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) - }; - - f.debug_struct("Device") - .field("document_url", &doc_uri) - .finish() - } } unsafe impl Sync for Device {} @@ -115,7 +58,6 @@ impl Device { let default_values = ComputedValues::default_values(doc); let root_style = RwLock::new(Arc::clone(&default_values)); Device { - document, default_values: default_values, root_style: root_style, root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), @@ -124,10 +66,6 @@ impl Device { root_font_metrics_cap: AtomicU32::new(FONT_MEDIUM_CAP_PX.to_bits()), root_font_metrics_ch: AtomicU32::new(FONT_MEDIUM_CH_PX.to_bits()), root_font_metrics_ic: AtomicU32::new(FONT_MEDIUM_IC_PX.to_bits()), - - // This gets updated when we see the , so it doesn't really - // matter which color-scheme we look at here. - body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), used_root_font_size: AtomicBool::new(false), used_root_line_height: AtomicBool::new(false), used_root_font_metrics: RwLock::new(false), @@ -135,15 +73,15 @@ impl Device { used_viewport_size: AtomicBool::new(false), used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, + extra: ExtraDeviceData { + document, + // This gets updated when we see the , so it doesn't really + // matter which color-scheme we look at here. + body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), + }, } } - /// Get the relevant environment to resolve `env()` functions. - #[inline] - pub fn environment(&self) -> &CssEnvironment { - &self.environment - } - /// Returns the computed line-height for the font in a given computed values instance. /// /// If you pass down an element, then the used line-height is returned. @@ -151,7 +89,7 @@ impl Device { &self, font: &crate::properties::style_structs::Font, writing_mode: WritingMode, - element: Option, + element: Option, ) -> NonNegativeLength { let pres_context = self.pres_context(); let line_height = font.clone_line_height(); @@ -180,108 +118,6 @@ impl Device { } } - /// Returns the default computed values as a reference, in order to match - /// Servo. - pub fn default_computed_values(&self) -> &ComputedValues { - &self.default_values - } - - /// Returns the default computed values as an `Arc`. - pub fn default_computed_values_arc(&self) -> &Arc { - &self.default_values - } - - /// Store a pointer to the root element's computed style, for use in - /// calculation of root font-relative metrics. - pub fn set_root_style(&self, style: &Arc) { - *self.root_style.write() = style.clone(); - } - - /// Get the font size of the root element (for rem) - pub fn root_font_size(&self) -> Length { - self.used_root_font_size.store(true, Ordering::Relaxed); - Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) - } - - /// Set the font size of the root element (for rem), in zoom-independent CSS pixels. - pub fn set_root_font_size(&self, size: f32) { - self.root_font_size.store(size.to_bits(), Ordering::Relaxed) - } - - /// Get the line height of the root element (for rlh) - pub fn root_line_height(&self) -> Length { - self.used_root_line_height.store(true, Ordering::Relaxed); - Length::new(f32::from_bits( - self.root_line_height.load(Ordering::Relaxed), - )) - } - - /// Set the line height of the root element (for rlh), in zoom-independent CSS pixels. - pub fn set_root_line_height(&self, size: f32) { - self.root_line_height - .store(size.to_bits(), Ordering::Relaxed); - } - - /// Get the x-height of the root element (for rex) - pub fn root_font_metrics_ex(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ex.load(Ordering::Relaxed), - )) - } - - /// Set the x-height of the root element (for rex), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ex(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ex.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the cap-height of the root element (for rcap) - pub fn root_font_metrics_cap(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_cap.load(Ordering::Relaxed), - )) - } - - /// Set the cap-height of the root element (for rcap), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_cap(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_cap.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the advance measure of the root element (for rch) - pub fn root_font_metrics_ch(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ch.load(Ordering::Relaxed), - )) - } - - /// Set the advance measure of the root element (for rch), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ch(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ch.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the ideographic advance measure of the root element (for ric) - pub fn root_font_metrics_ic(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ic.load(Ordering::Relaxed), - )) - } - - /// Set the ideographic advance measure of the root element (for ric), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ic(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ic.swap(size, Ordering::Relaxed); - previous != size - } - /// The quirks mode of the document. pub fn quirks_mode(&self) -> QuirksMode { self.document().mCompatMode.into() @@ -291,7 +127,7 @@ impl Device { /// /// pub fn set_body_text_color(&self, color: AbsoluteColor) { - self.body_text_color.store( + self.extra.body_text_color.store( convert_absolute_color_to_nscolor(&color) as usize, Ordering::Relaxed, ) @@ -361,64 +197,15 @@ impl Device { } } - fn ensure_root_font_metrics_updated(&self) { - let mut guard = self.used_root_font_metrics.write(); - let previously_computed = mem::replace(&mut *guard, true); - if !previously_computed { - self.update_root_font_metrics(); - } - } - - /// Compute the root element's font metrics, and returns a bool indicating whether - /// the font metrics have changed since the previous restyle. - pub fn update_root_font_metrics(&self) -> bool { - let root_style = self.root_style.read(); - let root_effective_zoom = (*root_style).effective_zoom; - let root_font_size = (*root_style).get_font().clone_font_size().computed_size(); - - let root_font_metrics = self.query_font_metrics( - (*root_style).writing_mode.is_upright(), - &(*root_style).get_font(), - root_font_size, - QueryFontMetricsFlags::USE_USER_FONT_SET - | QueryFontMetricsFlags::NEEDS_CH - | QueryFontMetricsFlags::NEEDS_IC, - /* track_usage = */ false, - ); - - let mut root_font_metrics_changed = false; - root_font_metrics_changed |= self.set_root_font_metrics_ex( - root_effective_zoom.unzoom(root_font_metrics.x_height_or_default(root_font_size).px()), - ); - root_font_metrics_changed |= self.set_root_font_metrics_ch( - root_effective_zoom.unzoom( - root_font_metrics - .zero_advance_measure_or_default( - root_font_size, - (*root_style).writing_mode.is_upright(), - ) - .px(), - ), - ); - root_font_metrics_changed |= self.set_root_font_metrics_cap( - root_effective_zoom.unzoom(root_font_metrics.cap_height_or_default().px()), - ); - root_font_metrics_changed |= self.set_root_font_metrics_ic( - root_effective_zoom.unzoom(root_font_metrics.ic_width_or_default(root_font_size).px()), - ); - - root_font_metrics_changed - } - /// Returns the body text color. pub fn body_text_color(&self) -> AbsoluteColor { - convert_nscolor_to_absolute_color(self.body_text_color.load(Ordering::Relaxed) as u32) + convert_nscolor_to_absolute_color(self.extra.body_text_color.load(Ordering::Relaxed) as u32) } /// Gets the document pointer. #[inline] pub fn document(&self) -> &structs::Document { - unsafe { &*self.document } + unsafe { &*self.extra.document } } /// Gets the pres context associated with this document. @@ -457,21 +244,6 @@ impl Device { .store(false, Ordering::Relaxed); } - /// Returns whether we ever looked up the root font size of the device. - pub fn used_root_font_size(&self) -> bool { - self.used_root_font_size.load(Ordering::Relaxed) - } - - /// Returns whether we ever looked up the root line-height of the device. - pub fn used_root_line_height(&self) -> bool { - self.used_root_line_height.load(Ordering::Relaxed) - } - - /// Returns whether we ever looked up the root font metrics of the device. - pub fn used_root_font_metrics(&self) -> bool { - *self.used_root_font_metrics.read() - } - /// Recreates all the temporary state that the `Device` stores. /// /// This includes the viewport override from `@viewport` rules, and also the @@ -590,21 +362,6 @@ impl Device { } } - /// Returns whether we ever looked up the viewport size of the Device. - pub fn used_viewport_size(&self) -> bool { - self.used_viewport_size.load(Ordering::Relaxed) - } - - /// Returns whether we ever looked up the dynamic viewport size of the Device. - pub fn used_dynamic_viewport_size(&self) -> bool { - self.used_dynamic_viewport_size.load(Ordering::Relaxed) - } - - /// Returns whether font metrics have been queried. - pub fn used_font_metrics(&self) -> bool { - self.used_font_metrics.load(Ordering::Relaxed) - } - /// Returns whether visited styles are enabled. pub fn visited_styles_enabled(&self) -> bool { unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) } @@ -742,3 +499,18 @@ impl Device { self.document().mChromeRulesEnabled() } } + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + let mut doc_uri = nsCString::new(); + unsafe { + bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) + }; + + f.debug_struct("Device") + .field("document_url", &doc_uri) + .finish() + } +} diff --git a/style/device/mod.rs b/style/device/mod.rs new file mode 100644 index 0000000000..7eb29dbf72 --- /dev/null +++ b/style/device/mod.rs @@ -0,0 +1,283 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Media-query device and expression representation. + +use crate::custom_properties::CssEnvironment; +#[cfg(feature = "servo")] +use crate::derives::*; +use crate::properties::ComputedValues; +use crate::values::computed::font::QueryFontMetricsFlags; +use crate::values::computed::Length; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::mem; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +#[cfg(feature = "gecko")] +use crate::device::gecko::ExtraDeviceData; +#[cfg(feature = "servo")] +use crate::device::servo::ExtraDeviceData; + +#[cfg(feature = "gecko")] +pub mod gecko; +#[cfg(feature = "servo")] +pub mod servo; + +/// A device is a structure that represents the current media a given document +/// is displayed in. +/// +/// This is the struct against which media queries are evaluated, has a default +/// values computed, and contains all the viewport rule state. +/// +/// This structure also contains atomics used for computing root font-relative +/// units. These atomics use relaxed ordering, since when computing the style +/// of the root element, there can't be any other style being computed at the +/// same time (given we need the style of the parent to compute everything else). +/// +/// In Gecko, it wraps a pres context. +#[cfg_attr(feature = "servo", derive(Debug, MallocSizeOf))] +pub struct Device { + /// The default computed values for this Device. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc is shared")] + default_values: Arc, + /// Current computed style of the root element, used for calculations of + /// root font-relative units. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + root_style: RwLock>, + /// Font size of the root element, used for rem units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_font_size: AtomicU32, + /// Line height of the root element, used for rlh units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_line_height: AtomicU32, + /// X-height of the root element, used for rex units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_font_metrics_ex: AtomicU32, + /// Cap-height of the root element, used for rcap units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_font_metrics_cap: AtomicU32, + /// Advance measure (ch) of the root element, used for rch units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_font_metrics_ch: AtomicU32, + /// Ideographic advance measure of the root element, used for ric units in other elements. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + root_font_metrics_ic: AtomicU32, + /// Whether any styles computed in the document relied on the root font-size + /// by using rem units. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + used_root_font_size: AtomicBool, + /// Whether any styles computed in the document relied on the root line-height + /// by using rlh units. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + used_root_line_height: AtomicBool, + /// Whether any styles computed in the document relied on the root font metrics + /// by using rcap, rch, rex, or ric units. This is a lock instead of an atomic + /// in order to prevent concurrent writes to the root font metric values. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + used_root_font_metrics: RwLock, + /// Whether any styles computed in the document relied on font metrics. + used_font_metrics: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using vw/vh/vmin/vmax units. + used_viewport_size: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using dvw/dvh/dvmin/dvmax units. + used_dynamic_viewport_size: AtomicBool, + /// The CssEnvironment object responsible of getting CSS environment + /// variables. + environment: CssEnvironment, + + /// Extra Gecko-specific or Servo-specific data. + extra: ExtraDeviceData, +} + +impl Device { + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// Returns the default computed values as a reference, in order to match + /// Servo. + pub fn default_computed_values(&self) -> &ComputedValues { + &self.default_values + } + + /// Returns the default computed values as an `Arc`. + pub fn default_computed_values_arc(&self) -> &Arc { + &self.default_values + } + + /// Store a pointer to the root element's computed style, for use in + /// calculation of root font-relative metrics. + pub fn set_root_style(&self, style: &Arc) { + *self.root_style.write() = style.clone(); + } + + /// Get the font size of the root element (for rem) + pub fn root_font_size(&self) -> Length { + self.used_root_font_size.store(true, Ordering::Relaxed); + Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) + } + + /// Set the font size of the root element (for rem), in zoom-independent CSS pixels. + pub fn set_root_font_size(&self, size: f32) { + self.root_font_size.store(size.to_bits(), Ordering::Relaxed) + } + + /// Get the line height of the root element (for rlh) + pub fn root_line_height(&self) -> Length { + self.used_root_line_height.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_line_height.load(Ordering::Relaxed), + )) + } + + /// Set the line height of the root element (for rlh), in zoom-independent CSS pixels. + pub fn set_root_line_height(&self, size: f32) { + self.root_line_height + .store(size.to_bits(), Ordering::Relaxed); + } + + /// Get the x-height of the root element (for rex) + pub fn root_font_metrics_ex(&self) -> Length { + self.ensure_root_font_metrics_updated(); + Length::new(f32::from_bits( + self.root_font_metrics_ex.load(Ordering::Relaxed), + )) + } + + /// Set the x-height of the root element (for rex), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ex(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ex.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the cap-height of the root element (for rcap) + pub fn root_font_metrics_cap(&self) -> Length { + self.ensure_root_font_metrics_updated(); + Length::new(f32::from_bits( + self.root_font_metrics_cap.load(Ordering::Relaxed), + )) + } + + /// Set the cap-height of the root element (for rcap), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_cap(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_cap.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the advance measure of the root element (for rch) + pub fn root_font_metrics_ch(&self) -> Length { + self.ensure_root_font_metrics_updated(); + Length::new(f32::from_bits( + self.root_font_metrics_ch.load(Ordering::Relaxed), + )) + } + + /// Set the advance measure of the root element (for rch), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ch(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ch.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the ideographic advance measure of the root element (for ric) + pub fn root_font_metrics_ic(&self) -> Length { + self.ensure_root_font_metrics_updated(); + Length::new(f32::from_bits( + self.root_font_metrics_ic.load(Ordering::Relaxed), + )) + } + + /// Set the ideographic advance measure of the root element (for ric), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ic(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ic.swap(size, Ordering::Relaxed); + previous != size + } + + fn ensure_root_font_metrics_updated(&self) { + let mut guard = self.used_root_font_metrics.write(); + let previously_computed = mem::replace(&mut *guard, true); + if !previously_computed { + self.update_root_font_metrics(); + } + } + + /// Compute the root element's font metrics, and returns a bool indicating whether + /// the font metrics have changed since the previous restyle. + pub fn update_root_font_metrics(&self) -> bool { + let root_style = self.root_style.read(); + let root_effective_zoom = (*root_style).effective_zoom; + let root_font_size = (*root_style).get_font().clone_font_size().computed_size(); + + let root_font_metrics = self.query_font_metrics( + (*root_style).writing_mode.is_upright(), + &(*root_style).get_font(), + root_font_size, + QueryFontMetricsFlags::USE_USER_FONT_SET + | QueryFontMetricsFlags::NEEDS_CH + | QueryFontMetricsFlags::NEEDS_IC, + /* track_usage = */ false, + ); + + let mut root_font_metrics_changed = false; + root_font_metrics_changed |= self.set_root_font_metrics_ex( + root_effective_zoom.unzoom(root_font_metrics.x_height_or_default(root_font_size).px()), + ); + root_font_metrics_changed |= self.set_root_font_metrics_ch( + root_effective_zoom.unzoom( + root_font_metrics + .zero_advance_measure_or_default( + root_font_size, + (*root_style).writing_mode.is_upright(), + ) + .px(), + ), + ); + root_font_metrics_changed |= self.set_root_font_metrics_cap( + root_effective_zoom.unzoom(root_font_metrics.cap_height_or_default().px()), + ); + root_font_metrics_changed |= self.set_root_font_metrics_ic( + root_effective_zoom.unzoom(root_font_metrics.ic_width_or_default(root_font_size).px()), + ); + + root_font_metrics_changed + } + + /// Returns whether we ever looked up the root font size of the Device. + pub fn used_root_font_size(&self) -> bool { + self.used_root_font_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the root line-height of the device. + pub fn used_root_line_height(&self) -> bool { + self.used_root_line_height.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the root font metrics of the device. + pub fn used_root_font_metrics(&self) -> bool { + *self.used_root_font_metrics.read() + } + + /// Returns whether we ever looked up the viewport size of the Device. + pub fn used_viewport_size(&self) -> bool { + self.used_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the dynamic viewport size of the Device. + pub fn used_dynamic_viewport_size(&self) -> bool { + self.used_dynamic_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether font metrics have been queried. + pub fn used_font_metrics(&self) -> bool { + self.used_font_metrics.load(Ordering::Relaxed) + } +} diff --git a/style/servo/media_queries.rs b/style/device/servo.rs similarity index 53% rename from style/servo/media_queries.rs rename to style/device/servo.rs index 2b4b1a2831..e7dd693ff6 100644 --- a/style/servo/media_queries.rs +++ b/style/device/servo.rs @@ -2,23 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Servo's media-query device and expression representation. +//! Servo-specific logic for [`Device`]. use crate::color::AbsoluteColor; use crate::context::QuirksMode; use crate::custom_properties::CssEnvironment; -use crate::derives::*; use crate::font_metrics::FontMetrics; use crate::logical_geometry::WritingMode; use crate::media_queries::MediaType; use crate::properties::style_structs::Font; use crate::properties::ComputedValues; -use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; use crate::queries::values::PrefersColorScheme; use crate::values::computed::font::GenericFontFamily; -use crate::values::computed::{ - CSSPixelLength, Context, Length, LineHeight, NonNegativeLength, Resolution, -}; +use crate::values::computed::{CSSPixelLength, Length, LineHeight, NonNegativeLength}; use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; use crate::values::specified::font::{ QueryFontMetricsFlags, FONT_MEDIUM_CAP_PX, FONT_MEDIUM_CH_PX, FONT_MEDIUM_EX_PX, @@ -29,14 +25,16 @@ use crate::values::KeyframesName; use app_units::{Au, AU_PER_PX}; use euclid::default::Size2D as UntypedSize2D; use euclid::{Scale, SideOffsets2D, Size2D}; +use malloc_size_of_derive::MallocSizeOf; use mime::Mime; use parking_lot::RwLock; use servo_arc::Arc; use std::fmt::Debug; -use std::mem; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use style_traits::{CSSPixel, DevicePixel}; +use crate::device::Device; + /// A trait used to query font metrics in clients of Stylo. This is used by Device to /// query font metrics in a way that is specific to the client using Stylo. pub trait FontMetricsProvider: Debug + Sync { @@ -52,18 +50,8 @@ pub trait FontMetricsProvider: Debug + Sync { fn base_size_for_generic(&self, generic: GenericFontFamily) -> Length; } -/// A device is a structure that represents the current media a given document -/// is displayed in. -/// -/// This is the struct against which media queries are evaluated. -/// and contains all the viewport rule state. -/// -/// This structure also contains atomics used for computing root font-relative -/// units. These atomics use relaxed ordering, since when computing the style -/// of the root element, there can't be any other style being computed at the -/// same time (given we need the style of the parent to compute everything else). #[derive(Debug, MallocSizeOf)] -pub struct Device { +pub(super) struct ExtraDeviceData { /// The current media type used by de device. media_type: MediaType, /// The current viewport size, in CSS pixels. @@ -73,59 +61,12 @@ pub struct Device { /// The current quirks mode. #[ignore_malloc_size_of = "Pure stack type"] quirks_mode: QuirksMode, - - /// Current computed style of the root element, used for calculations of - /// root font-relative units. - #[ignore_malloc_size_of = "Arc"] - root_style: RwLock>, - /// Font size of the root element, used for rem units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_font_size: AtomicU32, - /// Line height of the root element, used for rlh units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_line_height: AtomicU32, - /// X-height of the root element, used for rex units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_font_metrics_ex: AtomicU32, - /// Cap-height of the root element, used for rcap units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_font_metrics_cap: AtomicU32, - /// Advance measure (ch) of the root element, used for rch units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_font_metrics_ch: AtomicU32, - /// Ideographic advance measure of the root element, used for ric units in other elements. - #[ignore_malloc_size_of = "Pure stack type"] - root_font_metrics_ic: AtomicU32, - /// Whether any styles computed in the document relied on the root font-size - /// by using rem units. - #[ignore_malloc_size_of = "Pure stack type"] - used_root_font_size: AtomicBool, - /// Whether any styles computed in the document relied on the root line-height - /// by using rlh units. - #[ignore_malloc_size_of = "Pure stack type"] - used_root_line_height: AtomicBool, - /// Whether any styles computed in the document relied on the root font metrics - /// by using rcap, rch, rex, or ric units. This is a lock instead of an atomic - /// in order to prevent concurrent writes to the root font metric values. - #[ignore_malloc_size_of = "Pure stack type"] - used_root_font_metrics: RwLock, - /// Whether any styles computed in the document relied on font metrics. - used_font_metrics: AtomicBool, - /// Whether any styles computed in the document relied on the viewport size. - #[ignore_malloc_size_of = "Pure stack type"] - used_viewport_units: AtomicBool, /// Whether the user prefers light mode or dark mode #[ignore_malloc_size_of = "Pure stack type"] prefers_color_scheme: PrefersColorScheme, - /// The CssEnvironment object responsible of getting CSS environment - /// variables. - environment: CssEnvironment, /// An implementation of a trait which implements support for querying font metrics. #[ignore_malloc_size_of = "Owned by embedder"] font_metrics_provider: Box, - /// The default computed values for this Device. - #[ignore_malloc_size_of = "Arc is shared"] - default_computed_values: Arc, } impl Device { @@ -136,17 +77,11 @@ impl Device { viewport_size: Size2D, device_pixel_ratio: Scale, font_metrics_provider: Box, - default_computed_values: Arc, + default_values: Arc, prefers_color_scheme: PrefersColorScheme, ) -> Device { - let default_values = - ComputedValues::initial_values_with_font_override(Font::initial_values()); let root_style = RwLock::new(Arc::clone(&default_values)); Device { - media_type, - viewport_size, - device_pixel_ratio, - quirks_mode, root_style, root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), @@ -158,116 +93,21 @@ impl Device { used_root_line_height: AtomicBool::new(false), used_root_font_metrics: RwLock::new(false), used_font_metrics: AtomicBool::new(false), - used_viewport_units: AtomicBool::new(false), - prefers_color_scheme, + used_viewport_size: AtomicBool::new(false), + used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, - font_metrics_provider, - default_computed_values, + default_values, + extra: ExtraDeviceData { + media_type, + viewport_size, + device_pixel_ratio, + quirks_mode, + prefers_color_scheme, + font_metrics_provider, + }, } } - /// Get the relevant environment to resolve `env()` functions. - #[inline] - pub fn environment(&self) -> &CssEnvironment { - &self.environment - } - - /// Return the default computed values for this device. - pub fn default_computed_values(&self) -> &ComputedValues { - &self.default_computed_values - } - - /// Store a pointer to the root element's computed style, for use in - /// calculation of root font-relative metrics. - pub fn set_root_style(&self, style: &Arc) { - *self.root_style.write() = style.clone(); - } - - /// Get the font size of the root element (for rem) - pub fn root_font_size(&self) -> CSSPixelLength { - self.used_root_font_size.store(true, Ordering::Relaxed); - CSSPixelLength::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) - } - - /// Set the font size of the root element (for rem), in zoom-independent CSS pixels. - pub fn set_root_font_size(&self, size: f32) { - self.root_font_size.store(size.to_bits(), Ordering::Relaxed) - } - - /// Get the line height of the root element (for rlh) - pub fn root_line_height(&self) -> CSSPixelLength { - self.used_root_line_height.store(true, Ordering::Relaxed); - CSSPixelLength::new(f32::from_bits( - self.root_line_height.load(Ordering::Relaxed), - )) - } - - /// Set the line height of the root element (for rlh), in zoom-independent CSS pixels. - pub fn set_root_line_height(&self, size: f32) { - self.root_line_height - .store(size.to_bits(), Ordering::Relaxed); - } - - /// Get the x-height of the root element (for rex) - pub fn root_font_metrics_ex(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ex.load(Ordering::Relaxed), - )) - } - - /// Set the x-height of the root element (for rex), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ex(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ex.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the cap-height of the root element (for rcap) - pub fn root_font_metrics_cap(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_cap.load(Ordering::Relaxed), - )) - } - - /// Set the cap-height of the root element (for rcap), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_cap(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_cap.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the advance measure of the root element (for rch) - pub fn root_font_metrics_ch(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ch.load(Ordering::Relaxed), - )) - } - - /// Set the advance measure of the root element (for rch), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ch(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ch.swap(size, Ordering::Relaxed); - previous != size - } - - /// Get the ideographic advance measure of the root element (for ric) - pub fn root_font_metrics_ic(&self) -> Length { - self.ensure_root_font_metrics_updated(); - Length::new(f32::from_bits( - self.root_font_metrics_ic.load(Ordering::Relaxed), - )) - } - - /// Set the ideographic advance measure of the root element (for ric), in zoom-independent CSS pixels. - pub fn set_root_font_metrics_ic(&self, size: f32) -> bool { - let size = size.to_bits(); - let previous = self.root_font_metrics_ic.swap(size, Ordering::Relaxed); - previous != size - } - /// Returns the computed line-height for the font in a given computed values instance. /// /// If you pass down an element, then the used line-height is returned. @@ -288,7 +128,7 @@ impl Device { /// Get the quirks mode of the current device. pub fn quirks_mode(&self) -> QuirksMode { - self.quirks_mode + self.extra.quirks_mode } /// Sets the body text color for the "inherit color from body" quirk. @@ -300,7 +140,9 @@ impl Device { /// Gets the base size given a generic font family. pub fn base_size_for_generic(&self, generic: GenericFontFamily) -> Length { - self.font_metrics_provider.base_size_for_generic(generic) + self.extra + .font_metrics_provider + .base_size_for_generic(generic) } /// Whether a given animation name may be referenced from style. @@ -309,29 +151,9 @@ impl Device { true } - /// Returns whether we ever looked up the root font size of the Device. - pub fn used_root_font_size(&self) -> bool { - self.used_root_font_size.load(Ordering::Relaxed) - } - - /// Returns whether we ever looked up the root line-height of the device. - pub fn used_root_line_height(&self) -> bool { - self.used_root_line_height.load(Ordering::Relaxed) - } - - /// Returns whether we ever looked up the root font metrics of the device. - pub fn used_root_font_metrics(&self) -> bool { - *self.used_root_font_metrics.read() - } - - /// Returns whether font metrics have been queried. - pub fn used_font_metrics(&self) -> bool { - self.used_font_metrics.load(Ordering::Relaxed) - } - /// Get the viewport size on this [`Device`]. pub fn viewport_size(&self) -> Size2D { - self.viewport_size + self.extra.viewport_size } /// Set the viewport size on this [`Device`]. @@ -340,7 +162,7 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_viewport_size(&mut self, viewport_size: Size2D) { - self.viewport_size = viewport_size; + self.extra.viewport_size = viewport_size; } /// Returns the viewport size of the current device in app units, needed, @@ -348,8 +170,8 @@ impl Device { #[inline] pub fn au_viewport_size(&self) -> UntypedSize2D { Size2D::new( - Au::from_f32_px(self.viewport_size.width), - Au::from_f32_px(self.viewport_size.height), + Au::from_f32_px(self.extra.viewport_size.width), + Au::from_f32_px(self.extra.viewport_size.height), ) } @@ -358,30 +180,25 @@ impl Device { &self, _: ViewportVariant, ) -> UntypedSize2D { - self.used_viewport_units.store(true, Ordering::Relaxed); + self.used_viewport_size.store(true, Ordering::Relaxed); // Servo doesn't have dynamic UA interfaces that affect the viewport, // so we can just ignore the ViewportVariant. self.au_viewport_size() } - /// Whether viewport units were used since the last device change. - pub fn used_viewport_units(&self) -> bool { - self.used_viewport_units.load(Ordering::Relaxed) - } - /// Returns the number of app units per device pixel we're using currently. pub fn app_units_per_device_pixel(&self) -> i32 { - (AU_PER_PX as f32 / self.device_pixel_ratio.0) as i32 + (AU_PER_PX as f32 / self.extra.device_pixel_ratio.0) as i32 } /// Returns the device pixel ratio, ignoring the full zoom factor. pub fn device_pixel_ratio_ignoring_full_zoom(&self) -> Scale { - self.device_pixel_ratio + self.extra.device_pixel_ratio } /// Returns the device pixel ratio. pub fn device_pixel_ratio(&self) -> Scale { - self.device_pixel_ratio + self.extra.device_pixel_ratio } /// Set a new device pixel ratio on this [`Device`]. @@ -393,7 +210,7 @@ impl Device { &mut self, device_pixel_ratio: Scale, ) { - self.device_pixel_ratio = device_pixel_ratio; + self.extra.device_pixel_ratio = device_pixel_ratio; } /// Gets the size of the scrollbar in CSS pixels. @@ -414,62 +231,14 @@ impl Device { if track_usage { self.used_font_metrics.store(true, Ordering::Relaxed); } - self.font_metrics_provider + self.extra + .font_metrics_provider .query_font_metrics(vertical, font, base_size, flags) } - fn ensure_root_font_metrics_updated(&self) { - let mut guard = self.used_root_font_metrics.write(); - let previously_computed = mem::replace(&mut *guard, true); - if !previously_computed { - self.update_root_font_metrics(); - } - } - - /// Compute the root element's font metrics, and returns a bool indicating whether - /// the font metrics have changed since the previous restyle. - pub fn update_root_font_metrics(&self) -> bool { - let root_style = self.root_style.read(); - let root_effective_zoom = (*root_style).effective_zoom; - let root_font_size = (*root_style).get_font().clone_font_size().computed_size(); - - let root_font_metrics = self.query_font_metrics( - (*root_style).writing_mode.is_upright(), - &(*root_style).get_font(), - root_font_size, - QueryFontMetricsFlags::USE_USER_FONT_SET - | QueryFontMetricsFlags::NEEDS_CH - | QueryFontMetricsFlags::NEEDS_IC, - /* track_usage = */ false, - ); - - let mut root_font_metrics_changed = false; - root_font_metrics_changed |= self.set_root_font_metrics_ex( - root_effective_zoom.unzoom(root_font_metrics.x_height_or_default(root_font_size).px()), - ); - root_font_metrics_changed |= self.set_root_font_metrics_ch( - root_effective_zoom.unzoom( - root_font_metrics - .zero_advance_measure_or_default( - root_font_size, - (*root_style).writing_mode.is_upright(), - ) - .px(), - ), - ); - root_font_metrics_changed |= self.set_root_font_metrics_cap( - root_effective_zoom.unzoom(root_font_metrics.cap_height_or_default().px()), - ); - root_font_metrics_changed |= self.set_root_font_metrics_ic( - root_effective_zoom.unzoom(root_font_metrics.ic_width_or_default(root_font_size).px()), - ); - - root_font_metrics_changed - } - /// Return the media type of the current device. pub fn media_type(&self) -> MediaType { - self.media_type.clone() + self.extra.media_type.clone() } /// Returns whether document colors are enabled. @@ -493,12 +262,12 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_color_scheme(&mut self, new_color_scheme: PrefersColorScheme) { - self.prefers_color_scheme = new_color_scheme; + self.extra.prefers_color_scheme = new_color_scheme; } /// Returns the color scheme of this [`Device`]. pub fn color_scheme(&self) -> PrefersColorScheme { - self.prefers_color_scheme + self.extra.prefers_color_scheme } pub(crate) fn is_dark_color_scheme(&self, _: ColorSchemeFlags) -> bool { @@ -648,79 +417,3 @@ impl Device { false } } - -/// https://drafts.csswg.org/mediaqueries-4/#width -fn eval_width(context: &Context) -> CSSPixelLength { - CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) -} - -#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] -#[repr(u8)] -enum Scan { - Progressive, - Interlace, -} - -/// https://drafts.csswg.org/mediaqueries-4/#scan -fn eval_scan(_: &Context, _: Option) -> bool { - // Since we doesn't support the 'tv' media type, the 'scan' feature never - // matches. - false -} - -/// https://drafts.csswg.org/mediaqueries-4/#resolution -fn eval_resolution(context: &Context) -> Resolution { - Resolution::from_dppx(context.device().device_pixel_ratio.0) -} - -/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio -fn eval_device_pixel_ratio(context: &Context) -> f32 { - eval_resolution(context).dppx() -} - -fn eval_prefers_color_scheme(context: &Context, query_value: Option) -> bool { - match query_value { - Some(v) => context.device().prefers_color_scheme == v, - None => true, - } -} - -/// A list with all the media features that Servo supports. -pub static MEDIA_FEATURES: [QueryFeatureDescription; 6] = [ - feature!( - atom!("width"), - AllowsRanges::Yes, - Evaluator::Length(eval_width), - FeatureFlags::empty(), - ), - feature!( - atom!("scan"), - AllowsRanges::No, - keyword_evaluator!(eval_scan, Scan), - FeatureFlags::empty(), - ), - feature!( - atom!("resolution"), - AllowsRanges::Yes, - Evaluator::Resolution(eval_resolution), - FeatureFlags::empty(), - ), - feature!( - atom!("device-pixel-ratio"), - AllowsRanges::Yes, - Evaluator::Float(eval_device_pixel_ratio), - FeatureFlags::WEBKIT_PREFIX, - ), - feature!( - atom!("-moz-device-pixel-ratio"), - AllowsRanges::Yes, - Evaluator::Float(eval_device_pixel_ratio), - FeatureFlags::empty(), - ), - feature!( - atom!("prefers-color-scheme"), - AllowsRanges::No, - keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), - FeatureFlags::empty(), - ), -]; diff --git a/style/dom.rs b/style/dom.rs index b6cea79f6b..5cf30d202c 100644 --- a/style/dom.rs +++ b/style/dom.rs @@ -12,7 +12,7 @@ use crate::context::SharedStyleContext; #[cfg(feature = "gecko")] use crate::context::UpdateAnimationsTasks; use crate::data::{ElementData, ElementDataMut, ElementDataRef}; -use crate::media_queries::Device; +use crate::device::Device; use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock}; use crate::selector_map::PrecomputedHashMap; use crate::selector_parser::{AttrValue, Lang, PseudoElement, RestyleDamage, SelectorImpl}; diff --git a/style/gecko/wrapper.rs b/style/gecko/wrapper.rs index d76f1060b3..aabe596ac4 100644 --- a/style/gecko/wrapper.rs +++ b/style/gecko/wrapper.rs @@ -18,6 +18,7 @@ use crate::applicable_declarations::ApplicableDeclarationBlock; use crate::bloom::each_relevant_element_hash; use crate::context::{QuirksMode, SharedStyleContext, UpdateAnimationsTasks}; use crate::data::{ElementDataMut, ElementDataRef, ElementDataWrapper}; +use crate::device::Device; use crate::dom::{ AttributeProvider, LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot, @@ -54,7 +55,6 @@ use crate::gecko_bindings::structs::{nsAtom, nsIContent, nsINode_BooleanFlag}; use crate::gecko_bindings::structs::{nsINode as RawGeckoNode, Element as RawGeckoElement}; use crate::global_style_data::GLOBAL_STYLE_DATA; use crate::invalidation::element::restyle_hints::RestyleHint; -use crate::media_queries::Device; use crate::properties::{ animated_properties::{AnimationValue, AnimationValueMap}, ComputedValues, Importance, OwnedPropertyDeclarationId, PropertyDeclaration, diff --git a/style/invalidation/media_queries.rs b/style/invalidation/media_queries.rs index 1aabac4c68..1f9a61a886 100644 --- a/style/invalidation/media_queries.rs +++ b/style/invalidation/media_queries.rs @@ -6,7 +6,7 @@ use crate::context::QuirksMode; use crate::derives::*; -use crate::media_queries::Device; +use crate::device::Device; use crate::shared_lock::SharedRwLockReadGuard; use crate::stylesheets::{CustomMediaMap, DocumentRule, ImportRule, MediaRule}; use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule}; diff --git a/style/invalidation/stylesheets.rs b/style/invalidation/stylesheets.rs index a6753f51c1..d043d613b8 100644 --- a/style/invalidation/stylesheets.rs +++ b/style/invalidation/stylesheets.rs @@ -10,10 +10,10 @@ use crate::context::QuirksMode; use crate::data::ElementData; use crate::derives::*; +use crate::device::Device; use crate::dom::{TDocument, TElement, TNode}; use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; use crate::invalidation::element::restyle_hints::RestyleHint; -use crate::media_queries::Device; use crate::selector_map::PrecomputedHashSet; use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; use crate::shared_lock::SharedRwLockReadGuard; diff --git a/style/lib.rs b/style/lib.rs index e22a0ff239..ad12ef5157 100644 --- a/style/lib.rs +++ b/style/lib.rs @@ -74,6 +74,7 @@ pub mod counter_style; pub mod custom_properties; pub mod custom_properties_map; pub mod data; +pub mod device; pub mod dom; pub mod dom_apis; pub mod driver; diff --git a/style/media_queries/media_list.rs b/style/media_queries/media_list.rs index 63733c6f54..b644a4e3e6 100644 --- a/style/media_queries/media_list.rs +++ b/style/media_queries/media_list.rs @@ -6,9 +6,10 @@ //! //! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list -use super::{Device, MediaQuery, Qualifier}; +use super::{MediaQuery, Qualifier}; use crate::context::QuirksMode; use crate::derives::*; +use crate::device::Device; use crate::error_reporting::ContextualParseError; use crate::parser::ParserContext; use crate::stylesheets::CustomMediaEvaluator; diff --git a/style/media_queries/mod.rs b/style/media_queries/mod.rs index 833f6f53cb..823ceca175 100644 --- a/style/media_queries/mod.rs +++ b/style/media_queries/mod.rs @@ -11,8 +11,3 @@ mod media_query; pub use self::media_list::MediaList; pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; - -#[cfg(feature = "gecko")] -pub use crate::gecko::media_queries::Device; -#[cfg(feature = "servo")] -pub use crate::servo::media_queries::Device; diff --git a/style/properties/gecko.mako.rs b/style/properties/gecko.mako.rs index 379b956302..35d5ad36a4 100644 --- a/style/properties/gecko.mako.rs +++ b/style/properties/gecko.mako.rs @@ -11,6 +11,7 @@ use crate::Atom; use crate::logical_geometry::PhysicalSide; use crate::computed_value_flags::*; use crate::custom_properties::ComputedCustomProperties; +use crate::device::Device; use crate::gecko_bindings::bindings; % for style_struct in data.style_structs: use crate::gecko_bindings::bindings::Gecko_Construct_Default_${style_struct.gecko_ffi_name}; @@ -24,7 +25,6 @@ use crate::gecko_bindings::structs; use crate::gecko_bindings::structs::mozilla::PseudoStyleType; use crate::gecko::data::PerDocumentStyleData; use crate::logical_geometry::WritingMode; -use crate::media_queries::Device; use crate::properties::longhands; use crate::rule_tree::StrongRuleNode; use crate::selector_parser::PseudoElement; diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index d0d4533d77..caabaa7c47 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -20,7 +20,7 @@ use crate::logical_geometry::WritingMode; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use crate::computed_value_flags::*; use cssparser::Parser; -use crate::media_queries::Device; +use crate::device::Device; use crate::parser::ParserContext; use crate::selector_parser::PseudoElement; use crate::stylist::Stylist; diff --git a/style/queries/feature_expression.rs b/style/queries/feature_expression.rs index b2916eb060..0298148348 100644 --- a/style/queries/feature_expression.rs +++ b/style/queries/feature_expression.rs @@ -39,7 +39,7 @@ impl FeatureType { #[cfg(feature = "gecko")] use crate::gecko::media_features::MEDIA_FEATURES; #[cfg(feature = "servo")] - use crate::servo::media_queries::MEDIA_FEATURES; + use crate::servo::media_features::MEDIA_FEATURES; use crate::stylesheets::container_rule::CONTAINER_FEATURES; diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs new file mode 100644 index 0000000000..a6a56039df --- /dev/null +++ b/style/servo/media_features.rs @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Servo's media feature list and evaluator. + +use crate::derives::*; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::PrefersColorScheme; +use crate::values::computed::{CSSPixelLength, Context, Resolution}; +use std::fmt::Debug; + +/// https://drafts.csswg.org/mediaqueries-4/#width +fn eval_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Context, _: Option) -> bool { + // Since we doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +/// https://drafts.csswg.org/mediaqueries-4/#resolution +fn eval_resolution(context: &Context) -> Resolution { + Resolution::from_dppx(context.device().device_pixel_ratio().0) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio +fn eval_device_pixel_ratio(context: &Context) -> f32 { + eval_resolution(context).dppx() +} + +fn eval_prefers_color_scheme(context: &Context, query_value: Option) -> bool { + match query_value { + Some(v) => context.device().color_scheme() == v, + None => true, + } +} + +/// A list with all the media features that Servo supports. +pub static MEDIA_FEATURES: [QueryFeatureDescription; 6] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + FeatureFlags::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + FeatureFlags::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + FeatureFlags::empty(), + ), + feature!( + atom!("device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::empty(), + ), +]; diff --git a/style/servo/mod.rs b/style/servo/mod.rs index 753e8cc6dd..b7bff0fc28 100644 --- a/style/servo/mod.rs +++ b/style/servo/mod.rs @@ -10,7 +10,7 @@ pub mod animation; #[allow(missing_docs)] // TODO. pub mod attr; mod encoding_support; -pub mod media_queries; +pub mod media_features; pub mod restyle_damage; pub mod selector_parser; mod shadow_parts; diff --git a/style/stylesheet_set.rs b/style/stylesheet_set.rs index 327048ef7c..14e465436f 100644 --- a/style/stylesheet_set.rs +++ b/style/stylesheet_set.rs @@ -5,8 +5,8 @@ //! A centralized set of stylesheets for a document. use crate::derives::*; +use crate::device::Device; use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet}; -use crate::media_queries::Device; use crate::shared_lock::SharedRwLockReadGuard; use crate::stylesheets::{ CssRule, CssRuleRef, CustomMediaMap, Origin, OriginSet, PerOrigin, StylesheetInDocument, diff --git a/style/stylesheets/document_rule.rs b/style/stylesheets/document_rule.rs index 4477aeedb3..a64cfdd9af 100644 --- a/style/stylesheets/document_rule.rs +++ b/style/stylesheets/document_rule.rs @@ -7,7 +7,7 @@ //! We implement the prefixed `@-moz-document`. use crate::derives::*; -use crate::media_queries::Device; +use crate::device::Device; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{DeepCloneWithLock, Locked}; use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; diff --git a/style/stylesheets/rules_iterator.rs b/style/stylesheets/rules_iterator.rs index 9db1759529..a60d36e960 100644 --- a/style/stylesheets/rules_iterator.rs +++ b/style/stylesheets/rules_iterator.rs @@ -5,7 +5,7 @@ //! An iterator over a list of rules. use crate::context::QuirksMode; -use crate::media_queries::Device; +use crate::device::Device; use crate::shared_lock::SharedRwLockReadGuard; use crate::stylesheets::{ CssRule, CssRuleRef, CustomMediaEvaluator, CustomMediaMap, DocumentRule, ImportRule, MediaRule, diff --git a/style/stylesheets/stylesheet.rs b/style/stylesheets/stylesheet.rs index 7caa7d2a06..8e23370634 100644 --- a/style/stylesheets/stylesheet.rs +++ b/style/stylesheets/stylesheet.rs @@ -4,8 +4,9 @@ use crate::context::QuirksMode; use crate::derives::*; +use crate::device::Device; use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; -use crate::media_queries::{Device, MediaList}; +use crate::media_queries::MediaList; use crate::parser::ParserContext; use crate::shared_lock::{DeepCloneWithLock, Locked}; use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard}; diff --git a/style/stylist.rs b/style/stylist.rs index be2e08e2c4..f4679f0597 100644 --- a/style/stylist.rs +++ b/style/stylist.rs @@ -12,6 +12,7 @@ use crate::context::{CascadeInputs, QuirksMode}; use crate::custom_properties::ComputedCustomProperties; use crate::custom_properties::{parse_name, SpecifiedValue}; use crate::derives::*; +use crate::device::Device; use crate::dom::TElement; #[cfg(feature = "gecko")] use crate::gecko_bindings::structs::{ServoStyleSetSizes, StyleRuleInclusion}; @@ -23,7 +24,6 @@ use crate::invalidation::media_queries::{ EffectiveMediaQueryResults, MediaListKey, ToMediaListKey, }; use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet}; -use crate::media_queries::Device; #[cfg(feature = "gecko")] use crate::properties::StyleBuilder; use crate::properties::{ diff --git a/style/values/computed/mod.rs b/style/values/computed/mod.rs index 536bb81386..d2bca245ca 100644 --- a/style/values/computed/mod.rs +++ b/style/values/computed/mod.rs @@ -18,8 +18,8 @@ use crate::computed_value_flags::ComputedValueFlags; use crate::context::QuirksMode; use crate::custom_properties::ComputedCustomProperties; use crate::derives::*; +use crate::device::Device; use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; -use crate::media_queries::Device; #[cfg(feature = "gecko")] use crate::properties; use crate::properties::{ComputedValues, StyleBuilder}; diff --git a/style/values/resolved/mod.rs b/style/values/resolved/mod.rs index 3261382c5a..86a7a8fe3e 100644 --- a/style/values/resolved/mod.rs +++ b/style/values/resolved/mod.rs @@ -6,7 +6,7 @@ //! there are used values. #[cfg(feature = "gecko")] -use crate::media_queries::Device; +use crate::device::Device; use crate::properties::{ComputedValues, LonghandId, NonCustomPropertyId}; use crate::ArcSlice; use app_units::Au; diff --git a/style/values/specified/color.rs b/style/values/specified/color.rs index a5e3de2b4f..16417e9a8b 100644 --- a/style/values/specified/color.rs +++ b/style/values/specified/color.rs @@ -8,7 +8,7 @@ use super::AllowQuirks; use crate::color::mix::ColorInterpolationMethod; use crate::color::{parsing, AbsoluteColor, ColorFunction, ColorMixItemList, ColorSpace}; use crate::derives::*; -use crate::media_queries::Device; +use crate::device::Device; use crate::parser::{Parse, ParserContext}; use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; use crate::values::generics::color::{ diff --git a/style/values/specified/source_size_list.rs b/style/values/specified/source_size_list.rs index 08399af713..f3abe1f829 100644 --- a/style/values/specified/source_size_list.rs +++ b/style/values/specified/source_size_list.rs @@ -4,7 +4,7 @@ //! https://html.spec.whatwg.org/multipage/#source-size-list -use crate::media_queries::Device; +use crate::device::Device; use crate::parser::{Parse, ParserContext}; use crate::queries::{FeatureType, QueryCondition}; use crate::stylesheets::CustomMediaEvaluator; From 2a6d7c006a88bc2f4ec9e1bcf6e06f2f5083a628 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Wed, 11 Mar 2026 14:05:50 +0100 Subject: [PATCH 18/30] Unify `ListStyleType` among Gecko and Servo (#329) This makes Servo use the same `ListStyleType` as Gecko. In particular: - `counter()` and `counters()` will no longer accept a `none` style. - `list-style-type` will now accept a string. - Both `list-style-type` and counters will now accept arbitrary keywords. However, Servo's flavor of Stylo will still reject `@counter-style` rules. So non-predefined keywords will just default to `decimal`. - Both `list-style-type` and counters will now accept `symbols()`. Signed-off-by: Oriol Brufau --- style/values/generics/counters.rs | 21 +---- style/values/specified/counters.rs | 14 --- style/values/specified/list.rs | 141 +---------------------------- 3 files changed, 4 insertions(+), 172 deletions(-) diff --git a/style/values/generics/counters.rs b/style/values/generics/counters.rs index e8ffbeb879..24a7360269 100644 --- a/style/values/generics/counters.rs +++ b/style/values/generics/counters.rs @@ -4,9 +4,6 @@ //! Generic types for counters-related CSS values. -#[cfg(feature = "servo")] -use crate::computed_values::list_style_type::T as ListStyleType; -#[cfg(feature = "gecko")] use crate::counter_style::CounterStyle; use crate::derives::*; use crate::values::specified::Attr; @@ -189,21 +186,9 @@ pub struct GenericCounters( ); pub use self::GenericCounters as Counters; -#[cfg(feature = "servo")] -type CounterStyleType = ListStyleType; -#[cfg(feature = "gecko")] -type CounterStyleType = CounterStyle; - -#[cfg(feature = "servo")] -#[inline] -fn is_decimal(counter_type: &CounterStyleType) -> bool { - *counter_type == ListStyleType::Decimal -} - -#[cfg(feature = "gecko")] #[inline] -fn is_decimal(counter_type: &CounterStyleType) -> bool { +fn is_decimal(counter_type: &CounterStyle) -> bool { *counter_type == CounterStyle::decimal() } @@ -302,13 +287,13 @@ pub enum GenericContentItem { String(crate::OwnedStr), /// `counter(name, style)`. #[css(comma, function)] - Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType), + Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyle), /// `counters(name, separator, style)`. #[css(comma, function)] Counters( CustomIdent, crate::OwnedStr, - #[css(skip_if = "is_decimal")] CounterStyleType, + #[css(skip_if = "is_decimal")] CounterStyle, ), /// `open-quote`. OpenQuote, diff --git a/style/values/specified/counters.rs b/style/values/specified/counters.rs index b89d53358b..3d19b6a778 100644 --- a/style/values/specified/counters.rs +++ b/style/values/specified/counters.rs @@ -4,9 +4,6 @@ //! Specified types for counter properties. -#[cfg(feature = "servo")] -use crate::computed_values::list_style_type::T as ListStyleType; -#[cfg(feature = "gecko")] use crate::counter_style::CounterStyle; use crate::parser::{Parse, ParserContext}; use crate::values::generics::counters as generics; @@ -149,17 +146,6 @@ pub type Content = generics::GenericContent; pub type ContentItem = generics::GenericContentItem; impl Content { - #[cfg(feature = "servo")] - fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType { - input - .try_parse(|input| { - input.expect_comma()?; - ListStyleType::parse(input) - }) - .unwrap_or(ListStyleType::Decimal) - } - - #[cfg(feature = "gecko")] fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle { use crate::counter_style::CounterStyleParsingFlags; input diff --git a/style/values/specified/list.rs b/style/values/specified/list.rs index f28211082d..6632a9db1d 100644 --- a/style/values/specified/list.rs +++ b/style/values/specified/list.rs @@ -4,7 +4,6 @@ //! `list` specified values. -#[cfg(feature = "gecko")] use crate::counter_style::{CounterStyle, CounterStyleParsingFlags}; use crate::derives::*; use crate::parser::{Parse, ParserContext}; @@ -12,7 +11,6 @@ use cssparser::{Parser, Token}; use style_traits::{ParseError, StyleParseErrorKind}; /// Specified and computed `list-style-type` property. -#[cfg(feature = "gecko")] #[derive( Clone, Debug, @@ -29,7 +27,6 @@ use style_traits::{ParseError, StyleParseErrorKind}; #[repr(transparent)] pub struct ListStyleType(pub CounterStyle); -#[cfg(feature = "gecko")] impl ListStyleType { /// Initial specified value for `list-style-type`. #[inline] @@ -47,6 +44,7 @@ impl ListStyleType { /// /// This should only be used for mapping type attribute to list-style-type, and thus only /// values possible in that attribute is considered here. + #[cfg(feature = "gecko")] pub fn from_gecko_keyword(value: u32) -> Self { use crate::gecko_bindings::structs; use crate::values::CustomIdent; @@ -75,7 +73,6 @@ impl ListStyleType { } } -#[cfg(feature = "gecko")] impl Parse for ListStyleType { fn parse<'i, 't>( context: &ParserContext, @@ -86,142 +83,6 @@ impl Parse for ListStyleType { } } -/// Specified and computed `list-style-type` property. -#[cfg(feature = "servo")] -#[derive( - Clone, - Debug, - Eq, - MallocSizeOf, - Parse, - PartialEq, - SpecifiedValueInfo, - ToCss, - ToComputedValue, - ToResolvedValue, - ToShmem, - ToTyped, -)] -pub enum ListStyleType { - /// A filled circle, similar to • U+2022 BULLET. - /// - Disc, - /// The element has no marker string. - /// - None, - /// A hollow circle, similar to ◦ U+25E6 WHITE BULLET. - /// - Circle, - /// A filled square, similar to ▪ U+25AA BLACK SMALL SQUARE. - /// - Square, - /// Symbol for indicating an open disclosure widget, such as the HTML `

    ` element. - /// - DisclosureOpen, - /// Symbol for indicating a closed disclosure widget, such as the HTML `
    ` element. - /// - DisclosureClosed, - /// Western decimal numbers (e.g., 1, 2, 3, ..., 98, 99, 100). - /// - Decimal, - /// Lowercase ASCII letters (e.g., a, b, c, ..., z, aa, ab). - /// - LowerAlpha, - /// Uppercase ASCII letters (e.g., A, B, C, ..., Z, AA, AB). - /// - UpperAlpha, - /// Arabic-indic numbering (e.g.، ١، ٢، ٣، ٤، ...، ٩٨، ٩٩، ١٠٠). - /// - ArabicIndic, - /// Bengali numbering (e.g., ১, ২, ৩, ..., ৯৮, ৯৯, ১০০). - /// - Bengali, - /// Cambodian/Khmer numbering (e.g., ១, ២, ៣, ..., ៩៨, ៩៩, ១០០). - /// - Cambodian, - /// Han decimal numbers (e.g., 一, 二, 三, ..., 九八, 九九, 一〇〇). - /// - CjkDecimal, - /// devanagari numbering (e.g., १, २, ३, ..., ९८, ९९, १००). - /// - Devanagari, - /// Gujarati numbering (e.g., ૧, ૨, ૩, ..., ૯૮, ૯૯, ૧૦૦). - /// - Gujarati, - /// Gurmukhi numbering (e.g., ੧, ੨, ੩, ..., ੯੮, ੯੯, ੧੦੦). - /// - Gurmukhi, - /// Kannada numbering (e.g., ೧, ೨, ೩, ..., ೯೮, ೯೯, ೧೦೦). - /// - Kannada, - /// Cambodian/Khmer numbering (e.g., ១, ២, ៣, ..., ៩៨, ៩៩, ១០០). - /// - Khmer, - /// Laotian numbering (e.g., ໑, ໒, ໓, ..., ໙໘, ໙໙, ໑໐໐). - /// - Malayalam, - /// Mongolian numbering (e.g., ᠑, ᠒, ᠓, ..., ᠙᠘, ᠙᠙, ᠑᠐᠐). - /// - Mongolian, - /// Myanmar (Burmese) numbering (e.g., ၁, ၂, ၃, ..., ၉၈, ၉၉, ၁၀၀). - /// - Myanmar, - /// Oriya numbering (e.g., ୧, ୨, ୩, ..., ୯୮, ୯୯, ୧୦୦). - /// - Oriya, - /// Persian numbering (e.g., ۱, ۲, ۳, ۴, ..., ۹۸, ۹۹, ۱۰۰). - /// - Persian, - /// Telugu numbering (e.g., ౧, ౨, ౩, ..., ౯౮, ౯౯, ౧౦౦). - /// - Telugu, - /// Thai (Siamese) numbering (e.g., ๑, ๒, ๓, ..., ๙๘, ๙๙, ๑๐๐). - /// - Thai, - /// Tibetan numbering (e.g., ༡, ༢, ༣, ..., ༩༨, ༩༩, ༡༠༠). - /// - Tibetan, - /// Han "Earthly Branch" ordinals (e.g., 子, 丑, 寅, ..., 亥). - /// - CjkEarthlyBranch, - /// Han "Heavenly Stem" ordinals (e.g., 甲, 乙, 丙, ..., 癸) - /// - CjkHeavenlyStem, - /// Lowercase classical Greek (e.g., α, β, γ, ..., ω, αα, αβ). - /// - LowerGreek, - /// Dictionary-order hiragana lettering (e.g., あ, い, う, ..., ん, ああ, あい). - /// - Hiragana, - /// Iroha-order hiragana lettering (e.g., い, ろ, は, ..., す, いい, いろ). - /// - HiraganaIroha, - /// Dictionary-order katakana lettering (e.g., ア, イ, ウ, ..., ン, アア, アイ). - /// - Katakana, - /// Iroha-order katakana lettering (e.g., イ, ロ, ハ, ..., ス, イイ, イロ) - /// - KatakanaIroha, -} - -#[cfg(feature = "servo")] -impl ListStyleType { - /// Initial specified value for `list-style-type`. - #[inline] - pub fn disc() -> Self { - Self::Disc - } - - /// none value. - #[inline] - pub fn none() -> Self { - Self::None - } -} - /// A quote pair. #[derive( Clone, From dca3934667dae76c49bb579b268c5eb142d09c6a Mon Sep 17 00:00:00 2001 From: minghuaw Date: Wed, 11 Mar 2026 22:23:44 +0800 Subject: [PATCH 19/30] Enabling parsing `first-letter` pseudo element for servo (#320) This PR enables parsing `first-letter` pseudo element for servo. An accompanying servo PR (https://github.com/servo/servo/pull/43027) is open --------- Signed-off-by: Minghua Wu Signed-off-by: minghuaw Co-authored-by: Oriol Brufau --- style/data.rs | 3 --- style/servo/selector_parser.rs | 15 ++++++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/style/data.rs b/style/data.rs index 33afb8eae2..db93ea3778 100644 --- a/style/data.rs +++ b/style/data.rs @@ -112,10 +112,7 @@ impl fmt::Debug for EagerPseudoArray { // Can't use [None; EAGER_PSEUDO_COUNT] here because it complains // about Copy not being implemented for our Arc type. -#[cfg(feature = "gecko")] const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None]; -#[cfg(feature = "servo")] -const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None]; impl EagerPseudoStyles { /// Returns whether there are any pseudo styles. diff --git a/style/servo/selector_parser.rs b/style/servo/selector_parser.rs index 2b2a0d4202..4e94dade56 100644 --- a/style/servo/selector_parser.rs +++ b/style/servo/selector_parser.rs @@ -53,6 +53,7 @@ pub enum PseudoElement { // property_restriction implementation to do property filtering for them. // Also, make sure the UA sheet has the !important rules some of the // APPLIES_TO_PLACEHOLDER properties expect! + FirstLetter, // Non-eager pseudos. Backdrop, @@ -92,6 +93,7 @@ impl ToCss for PseudoElement { After => "::after", Before => "::before", Selection => "::selection", + FirstLetter => "::first-letter", Backdrop => "::backdrop", DetailsSummary => "::-servo-details-summary", DetailsContent => "::details-content", @@ -115,7 +117,7 @@ impl ::selectors::parser::PseudoElement for PseudoElement { } /// The number of eager pseudo-elements. Keep this in sync with cascade_type. -pub const EAGER_PSEUDO_COUNT: usize = 3; +pub const EAGER_PSEUDO_COUNT: usize = 4; impl PseudoElement { /// Gets the canonical index of this eagerly-cascaded pseudo-element. @@ -184,7 +186,7 @@ impl PseudoElement { /// Whether the current pseudo element is :first-letter #[inline] pub fn is_first_letter(&self) -> bool { - false + *self == PseudoElement::FirstLetter } /// Whether the current pseudo element is :first-line @@ -240,9 +242,10 @@ impl PseudoElement { #[inline] pub fn cascade_type(&self) -> PseudoElementCascadeType { match *self { - PseudoElement::After | PseudoElement::Before | PseudoElement::Selection => { - PseudoElementCascadeType::Eager - }, + PseudoElement::After + | PseudoElement::Before + | PseudoElement::FirstLetter + | PseudoElement::Selection => PseudoElementCascadeType::Eager, PseudoElement::Backdrop | PseudoElement::ColorSwatch | PseudoElement::DetailsSummary @@ -275,6 +278,7 @@ impl PseudoElement { #[inline] pub fn property_restriction(&self) -> Option { Some(match self { + PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER, PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => { PropertyFlags::APPLIES_TO_MARKER }, @@ -661,6 +665,7 @@ impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { "after" => After, "backdrop" => Backdrop, "selection" => Selection, + "first-letter" => FirstLetter, "marker" => Marker, "-servo-details-summary" => { if !self.in_user_agent_stylesheet() { From d38712e51b52c4f205a7f51e2f1db75451449f51 Mon Sep 17 00:00:00 2001 From: Alex Touchet <26315797+atouchet@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:28:41 -0700 Subject: [PATCH 20/30] Replace Travis CI badge with GitHub workflow (#332) Signed-off-by: Alex Touchet <26315797+atouchet@users.noreply.github.com> --- selectors/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/selectors/README.md b/selectors/README.md index dac4a7ff91..3ce269fa3c 100644 --- a/selectors/README.md +++ b/selectors/README.md @@ -1,9 +1,8 @@ rust-selectors ============== -* [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)]( - https://travis-ci.com/servo/rust-selectors) -* [Documentation](https://docs.rs/selectors/) +* [![Build Status](https://github.com/servo/stylo/actions/workflows/main.yml/badge.svg)](https://github.com/servo/stylo/actions) +* [Documentation](https://docs.rs/selectors) * [crates.io](https://crates.io/crates/selectors) CSS Selectors library for Rust. From d91c7cf6a077ef3badb9ebe0ec98e4ea104d10d7 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 17 Mar 2026 14:43:11 +0100 Subject: [PATCH 21/30] Remove the Servo non-incremental layout feature (#331) This isn't really used. Nowadays we always test incremental layout as it's considered an essential feature of the functioning of Servo. After servo/servo#43207, Servo will no longer use this feature. Signed-off-by: Martin Robinson --- style/traversal.rs | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/style/traversal.rs b/style/traversal.rs index b80fd1dc73..d0ac0294b1 100644 --- a/style/traversal.rs +++ b/style/traversal.rs @@ -52,27 +52,6 @@ impl PreTraverseToken { } } -/// A global variable holding the state of -/// `is_servo_nonincremental_layout()`. -/// See [#22854](https://github.com/servo/servo/issues/22854). -#[cfg(feature = "servo")] -pub static IS_SERVO_NONINCREMENTAL_LAYOUT: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -#[cfg(feature = "servo")] -#[inline] -fn is_servo_nonincremental_layout() -> bool { - use std::sync::atomic::Ordering; - - IS_SERVO_NONINCREMENTAL_LAYOUT.load(Ordering::Relaxed) -} - -#[cfg(not(feature = "servo"))] -#[inline] -fn is_servo_nonincremental_layout() -> bool { - false -} - /// A DOM Traversal trait, that is used to generically implement styling for /// Gecko and Servo. pub trait DomTraversal: Sync { @@ -219,11 +198,6 @@ pub trait DomTraversal: Sync { el, traversal_flags, data ); - // Non-incremental layout visits every node. - if is_servo_nonincremental_layout() { - return true; - } - // Unwrap the data. let data = match data { Some(d) if d.has_styles() => d, @@ -513,8 +487,7 @@ pub fn recalc_style_at( // We only do this if we're not a display: none root, since in that case // it's useless to style children. let mut traverse_children = has_dirty_descendants_for_this_restyle - || !propagated_hint.is_empty() - || is_servo_nonincremental_layout(); + || !propagated_hint.is_empty(); traverse_children = traverse_children && !data.styles.is_display_none(); @@ -759,9 +732,7 @@ fn note_children( let child = match child_node.as_element() { Some(el) => el, None => { - if is_servo_nonincremental_layout() - || D::text_node_needs_traversal(child_node, data) - { + if D::text_node_needs_traversal(child_node, data) { note_child(child_node); } continue; From 8278e5ad35b2483c4968470d808d50cca1aa4573 Mon Sep 17 00:00:00 2001 From: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:21:46 +0100 Subject: [PATCH 22/30] Bump the workspace version (#334) The independently versioned crates in this workspace haven't had any changes since the last release according to git. --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d009fb5de7..aa134b81b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ default-members = ["style"] [workspace.package] -version = "0.13.0" +version = "0.14.0" [workspace.dependencies] # in-repo dependencies (separately versioned) @@ -27,10 +27,10 @@ to_shmem = { version = "0.3.0", path = "./to_shmem", features = ["servo" to_shmem_derive = { version = "0.1.0", path = "./to_shmem_derive" } # in-repo dependencies (main version) -malloc_size_of = { version = "0.13.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } -static_prefs = { version = "0.13.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } -stylo_atoms = { version = "0.13.0", path = "./stylo_atoms" } -dom = { version = "0.13.0", path = "./stylo_dom", package = "stylo_dom" } -style_traits = { version = "0.13.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} -style_derive = { version = "0.13.0", path = "./style_derive", package = "stylo_derive"} -stylo = { version = "0.13.0", path = "./style" } +malloc_size_of = { version = "0.14.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } +static_prefs = { version = "0.14.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } +stylo_atoms = { version = "0.14.0", path = "./stylo_atoms" } +dom = { version = "0.14.0", path = "./stylo_dom", package = "stylo_dom" } +style_traits = { version = "0.14.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} +style_derive = { version = "0.14.0", path = "./style_derive", package = "stylo_derive"} +stylo = { version = "0.14.0", path = "./style" } From 635e1a19d02960588a00e189bd4bd5bdb150ec3d Mon Sep 17 00:00:00 2001 From: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:00:11 +0100 Subject: [PATCH 23/30] Bump selectors for readme update. (#335) --- Cargo.toml | 2 +- selectors/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa134b81b1..5dbc3d8b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ version = "0.14.0" [workspace.dependencies] # in-repo dependencies (separately versioned) servo_arc = { version = "0.4.3", path = "./servo_arc" } -selectors = { version = "0.36.0", path = "./selectors" } +selectors = { version = "0.36.1", path = "./selectors" } to_shmem = { version = "0.3.0", path = "./to_shmem", features = ["servo"] } to_shmem_derive = { version = "0.1.0", path = "./to_shmem_derive" } diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index c67d028258..f08bbaff2c 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selectors" -version = "0.36.0" +version = "0.36.1" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/selectors/" description = "CSS Selectors matching for Rust" From f9d940629a7fd0389cc4ecb58c12927b09286478 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 19 Mar 2026 14:40:05 +0100 Subject: [PATCH 24/30] Adopt "tables inherit color from body quirk" from Gecko (#333) Already landed upstream: https://bugzilla.mozilla.org/show_bug.cgi?id=2023380 Servo PR: https://github.com/servo/servo/pull/43368 Signed-off-by: Oriol Brufau --- style/color/mod.rs | 24 +++++++++++++++++++++ style/device/gecko.rs | 37 +++++++-------------------------- style/device/mod.rs | 20 ++++++++++++++++++ style/device/servo.rs | 8 +------ style/gecko/mod.rs | 2 -- style/gecko/values.rs | 36 -------------------------------- style/values/specified/color.rs | 7 +------ 7 files changed, 53 insertions(+), 81 deletions(-) delete mode 100644 style/gecko/values.rs diff --git a/style/color/mod.rs b/style/color/mod.rs index 93918b0a11..9d90b92b73 100644 --- a/style/color/mod.rs +++ b/style/color/mod.rs @@ -638,6 +638,30 @@ impl AbsoluteColor { self.alpha(), ) } + + /// Convert a color value to `nscolor`. + pub fn to_nscolor(&self) -> u32 { + let srgb = self.to_color_space(ColorSpace::Srgb); + u32::from_le_bytes([ + (srgb.components.0 * 255.0).round() as u8, + (srgb.components.1 * 255.0).round() as u8, + (srgb.components.2 * 255.0).round() as u8, + (srgb.alpha * 255.0).round() as u8, + ]) + } + + /// Convert a given `nscolor` to a Servo AbsoluteColor value. + pub fn from_nscolor(color: u32) -> Self { + let [r, g, b, a] = color.to_le_bytes(); + Self::srgb_legacy(r, g, b, a as f32 / 255.0) + } +} + +#[test] +fn from_nscolor_should_be_in_legacy_syntax() { + let result = AbsoluteColor::from_nscolor(0x336699CC); + assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB)); + assert!(result.is_legacy_syntax()); } impl From for ColorSpace { diff --git a/style/device/gecko.rs b/style/device/gecko.rs index 3391d67874..7ed8c2c7a1 100644 --- a/style/device/gecko.rs +++ b/style/device/gecko.rs @@ -9,7 +9,6 @@ use crate::context::QuirksMode; use crate::custom_properties::CssEnvironment; use crate::device::Device; use crate::font_metrics::FontMetrics; -use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color}; use crate::gecko::wrapper::GeckoElement; use crate::gecko_bindings::bindings; use crate::gecko_bindings::structs; @@ -31,7 +30,7 @@ use euclid::default::Size2D; use euclid::{Scale, SideOffsets2D}; use parking_lot::RwLock; use servo_arc::Arc; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::{cmp, fmt}; use style_traits::{CSSPixel, DevicePixel}; @@ -39,11 +38,6 @@ pub(super) struct ExtraDeviceData { /// NB: The document owns the styleset, who owns the stylist, and thus the /// `Device`, so having a raw document pointer here is fine. document: *const structs::Document, - /// The body text color, stored as an `nscolor`, used for the "tables - /// inherit from body" quirk. - /// - /// - body_text_color: AtomicUsize, } unsafe impl Sync for Device {} @@ -73,12 +67,10 @@ impl Device { used_viewport_size: AtomicBool::new(false), used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, - extra: ExtraDeviceData { - document, - // This gets updated when we see the , so it doesn't really - // matter which color-scheme we look at here. - body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), - }, + // This gets updated when we see the , so it doesn't really + // matter which color-scheme we look at here. + body_text_color: AtomicU32::new(prefs.mLightColors.mDefault), + extra: ExtraDeviceData { document }, } } @@ -123,16 +115,6 @@ impl Device { self.document().mCompatMode.into() } - /// Sets the body text color for the "inherit color from body" quirk. - /// - /// - pub fn set_body_text_color(&self, color: AbsoluteColor) { - self.extra.body_text_color.store( - convert_absolute_color_to_nscolor(&color) as usize, - Ordering::Relaxed, - ) - } - /// Gets the base size given a generic font family and a language. pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length { unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) } @@ -197,11 +179,6 @@ impl Device { } } - /// Returns the body text color. - pub fn body_text_color(&self) -> AbsoluteColor { - convert_nscolor_to_absolute_color(self.extra.body_text_color.load(Ordering::Relaxed) as u32) - } - /// Gets the document pointer. #[inline] pub fn document(&self) -> &structs::Document { @@ -431,7 +408,7 @@ impl Device { /// This is only for forced-colors/high-contrast, so looking at light colors /// is ok. pub fn default_background_color(&self) -> AbsoluteColor { - convert_nscolor_to_absolute_color( + AbsoluteColor::from_nscolor( self.system_nscolor(SystemColor::Canvas, ColorScheme::normal().bits), ) } @@ -440,7 +417,7 @@ impl Device { /// /// See above for looking at light colors only. pub fn default_color(&self) -> AbsoluteColor { - convert_nscolor_to_absolute_color( + AbsoluteColor::from_nscolor( self.system_nscolor(SystemColor::Canvastext, ColorScheme::normal().bits), ) } diff --git a/style/device/mod.rs b/style/device/mod.rs index 7eb29dbf72..08a7502f8d 100644 --- a/style/device/mod.rs +++ b/style/device/mod.rs @@ -4,6 +4,7 @@ //! Media-query device and expression representation. +use crate::color::AbsoluteColor; use crate::custom_properties::CssEnvironment; #[cfg(feature = "servo")] use crate::derives::*; @@ -88,6 +89,12 @@ pub struct Device { /// The CssEnvironment object responsible of getting CSS environment /// variables. environment: CssEnvironment, + /// The body text color, stored as an `nscolor`, used for the "tables + /// inherit from body" quirk. + /// + /// + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] + body_text_color: AtomicU32, /// Extra Gecko-specific or Servo-specific data. extra: ExtraDeviceData, @@ -280,4 +287,17 @@ impl Device { pub fn used_font_metrics(&self) -> bool { self.used_font_metrics.load(Ordering::Relaxed) } + + /// Returns the body text color. + pub fn body_text_color(&self) -> AbsoluteColor { + AbsoluteColor::from_nscolor(self.body_text_color.load(Ordering::Relaxed)) + } + + /// Sets the body text color for the "inherit color from body" quirk. + /// + /// + pub fn set_body_text_color(&self, color: AbsoluteColor) { + self.body_text_color + .store(color.to_nscolor(), Ordering::Relaxed) + } } diff --git a/style/device/servo.rs b/style/device/servo.rs index e7dd693ff6..2d14029f91 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -97,6 +97,7 @@ impl Device { used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, default_values, + body_text_color: AtomicU32::new(AbsoluteColor::BLACK.to_nscolor()), extra: ExtraDeviceData { media_type, viewport_size, @@ -131,13 +132,6 @@ impl Device { self.extra.quirks_mode } - /// Sets the body text color for the "inherit color from body" quirk. - /// - /// - pub fn set_body_text_color(&self, _color: AbsoluteColor) { - // Servo doesn't implement this quirk (yet) - } - /// Gets the base size given a generic font family. pub fn base_size_for_generic(&self, generic: GenericFontFamily) -> Length { self.extra diff --git a/style/gecko/mod.rs b/style/gecko/mod.rs index c32ded14f3..5428fdf6b4 100644 --- a/style/gecko/mod.rs +++ b/style/gecko/mod.rs @@ -11,7 +11,6 @@ pub mod arc_types; pub mod conversions; pub mod data; pub mod media_features; -pub mod media_queries; pub mod pseudo_element; pub mod restyle_damage; pub mod selector_parser; @@ -19,5 +18,4 @@ pub mod snapshot; pub mod snapshot_helpers; pub mod traversal; pub mod url; -pub mod values; pub mod wrapper; diff --git a/style/gecko/values.rs b/style/gecko/values.rs deleted file mode 100644 index 1b4f5e453a..0000000000 --- a/style/gecko/values.rs +++ /dev/null @@ -1,36 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -#![allow(unsafe_code)] - -//! Different kind of helpers to interact with Gecko values. - -use crate::color::{AbsoluteColor, ColorSpace}; - -/// Convert a color value to `nscolor`. -pub fn convert_absolute_color_to_nscolor(color: &AbsoluteColor) -> u32 { - let srgb = color.to_color_space(ColorSpace::Srgb); - u32::from_le_bytes([ - (srgb.components.0 * 255.0).round() as u8, - (srgb.components.1 * 255.0).round() as u8, - (srgb.components.2 * 255.0).round() as u8, - (srgb.alpha * 255.0).round() as u8, - ]) -} - -/// Convert a given `nscolor` to a Servo AbsoluteColor value. -pub fn convert_nscolor_to_absolute_color(color: u32) -> AbsoluteColor { - let [r, g, b, a] = color.to_le_bytes(); - AbsoluteColor::srgb_legacy(r, g, b, a as f32 / 255.0) -} - -#[test] -fn convert_ns_color_to_absolute_color_should_be_in_legacy_syntax() { - use crate::color::ColorFlags; - - let result = convert_nscolor_to_absolute_color(0x336699CC); - assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB)); - - assert!(result.is_legacy_syntax()); -} diff --git a/style/values/specified/color.rs b/style/values/specified/color.rs index 16417e9a8b..dc46c5261f 100644 --- a/style/values/specified/color.rs +++ b/style/values/specified/color.rs @@ -175,7 +175,6 @@ pub enum Color { /// The contrast-color function. ContrastColor(Box), /// Quirksmode-only rule for inheriting color from the body - #[cfg(feature = "gecko")] InheritFromBodyQuirk, } @@ -422,7 +421,6 @@ pub enum SystemColor { impl SystemColor { #[inline] fn compute(&self, cx: &Context) -> ComputedColor { - use crate::gecko::values::convert_nscolor_to_absolute_color; use crate::gecko_bindings::bindings; let color = cx.device().system_nscolor(*self, cx.builder.color_scheme); @@ -434,7 +432,7 @@ impl SystemColor { if color == bindings::NS_SAME_AS_FOREGROUND_COLOR { return ComputedColor::currentcolor(); } - ComputedColor::Absolute(convert_nscolor_to_absolute_color(color)) + ComputedColor::Absolute(AbsoluteColor::from_nscolor(color)) } } @@ -671,7 +669,6 @@ impl ToCss for Color { dest.write_char(')') }, Color::System(system) => system.to_css(dest), - #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => dest.write_str("-moz-inherit-from-body-quirk"), } } @@ -681,7 +678,6 @@ impl Color { /// Returns whether this color is allowed in forced-colors mode. pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool { match *self { - #[cfg(feature = "gecko")] Self::InheritFromBodyQuirk => false, Self::CurrentColor => true, Self::System(..) => true, @@ -915,7 +911,6 @@ impl Color { ComputedColor::ContrastColor(Box::new(c.to_computed_color(context)?)) }, Color::System(system) => system.compute(context?), - #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => { ComputedColor::Absolute(context?.device().body_text_color()) }, From 2052fb516d115872aa03909120064a55cce6c5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=BClker?= Date: Thu, 19 Mar 2026 15:54:45 +0100 Subject: [PATCH 25/30] Remove declaration of gecko's `media_queries` module (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/servo/stylo/pull/330/ moved the module into `style/device` but forgot to remove the old declaration. That's why `cargo fmt` currently fails on `main`: ``` Error writing files: failed to resolve mod `media_queries`: /home/alaska/stylo/style/gecko/media_queries.rs does not exist ``` Signed-off-by: Simon Wülker From a4f1d69e3d239b73687ee5d6eca59d8e4056da41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=BClker?= Date: Fri, 20 Mar 2026 19:13:14 +0100 Subject: [PATCH 26/30] Don't skip animations without keyframes (#336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We currently never start animations that have no keyframes. That is not correct. While such an animation will never change any styles, it should still fire `animationstart` and `animationend` events at the correct time, and that requires the animation to start first. Servo companion PR: https://github.com/servo/servo/pull/43454 Signed-off-by: Simon Wülker --- style/servo/animation.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index f270c49977..2f47def2b7 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -152,6 +152,10 @@ impl IntermediateComputedKeyframe { context: &SharedStyleContext, base_style: &ComputedValues, ) -> Vec { + if animation.steps.is_empty() { + return vec![]; + } + let mut intermediate_steps: Vec = Vec::with_capacity(animation.steps.len()); let mut current_step = IntermediateComputedKeyframe::new(0.); for step in animation.steps.iter() { @@ -582,7 +586,10 @@ impl Animation { /// Fill in an `AnimationValueMap` with values calculated from this animation at /// the given time value. fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) { - debug_assert!(!self.computed_steps.is_empty()); + if self.computed_steps.is_empty() { + // Nothing to do. + return; + } let total_progress = match self.state { AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { @@ -1496,21 +1503,12 @@ pub fn maybe_start_animations( continue; } - let keyframe_animation = match context.stylist.lookup_keyframes(name, element) { - Some(animation) => animation, - None => continue, + let Some(keyframe_animation) = context.stylist.lookup_keyframes(name, element) else { + continue; }; debug!("maybe_start_animations: animation {} found", name); - // If this animation doesn't have any keyframe, we can just continue - // without submitting it to the compositor, since both the first and - // the second keyframes would be synthetised from the computed - // values. - if keyframe_animation.steps.is_empty() { - continue; - } - // NB: This delay may be negative, meaning that the animation may be created // in a state where we have advanced one or more iterations or even that the // animation begins in a finished state. From 4ecadfcfd3365f05ee692d5b60f62c418d65fa1e Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Mon, 23 Mar 2026 22:12:54 +0800 Subject: [PATCH 27/30] overhaul Signed-off-by: Euclid Ye --- style/servo/animation.rs | 54 +++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 2f47def2b7..74f29fb7aa 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -27,7 +27,7 @@ use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, Keyf use crate::stylesheets::layer_rule::LayerOrder; use crate::values::animated::{Animate, Procedure}; use crate::values::computed::TimingFunction; -use crate::values::generics::easing::BeforeFlag; +use crate::values::generics::easing::{BeforeFlag, StepPosition}; use crate::values::specified::TransitionBehavior; use crate::Atom; use parking_lot::RwLock; @@ -615,6 +615,19 @@ impl Animation { .min(self.current_iteration_end_progress()) .max(0.0); + // If we only need to take into account one keyframe, then exit early + // in order to avoid doing more work. + let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { + for value in keyframe.values.iter() { + map.insert(value.id().to_owned(), value.clone()); + } + }; + + if total_progress >= 1.0 { + add_declarations_to_map(self.computed_steps.last().unwrap()); + return; + } + // Get the indices of the previous (from) keyframe and the next (to) keyframe. let next_keyframe_index; let prev_keyframe_index; @@ -624,7 +637,7 @@ impl Animation { next_keyframe_index = self .computed_steps .iter() - .position(|step| total_progress as f32 <= step.start_percentage); + .position(|step| (total_progress as f32) < step.start_percentage); prev_keyframe_index = next_keyframe_index .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) .unwrap_or(0); @@ -657,23 +670,34 @@ impl Animation { let prev_keyframe = &self.computed_steps[prev_keyframe_index]; let next_keyframe = match next_keyframe_index { Some(index) => &self.computed_steps[index], - None => return, - }; - - // If we only need to take into account one keyframe, then exit early - // in order to avoid doing more work. - let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { - for value in keyframe.values.iter() { - map.insert(value.id().to_owned(), value.clone()); - } + None => { + return; + }, }; - if total_progress <= 0.0 { + if total_progress < 0.0 { add_declarations_to_map(&prev_keyframe); return; } - if total_progress >= 1.0 { - add_declarations_to_map(&next_keyframe); - return; + + // At progress 0, we need to handle step functions specially + // for "jump-both, jump-start, start" step functions. + if total_progress == 0.0 { + if let TimingFunction::Steps(_steps, pos) = &prev_keyframe.timing_function { + if *pos == StepPosition::JumpBoth + || *pos == StepPosition::JumpStart + || *pos == StepPosition::Start + { + // Continue to interpolation. + } else { + // Others use start value + add_declarations_to_map(&prev_keyframe); + return; + } + } else { + // Not a step function, use start value + add_declarations_to_map(&prev_keyframe); + return; + } } let percentage_between_keyframes = From ecb0a952d2f5e672977a66cd74677d71e14391f7 Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Tue, 24 Mar 2026 00:02:36 +0800 Subject: [PATCH 28/30] raw Signed-off-by: Euclid Ye --- style/servo/animation.rs | 43 +++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 74f29fb7aa..628afe9c05 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -591,7 +591,8 @@ impl Animation { return; } - let total_progress = match self.state { + // Raw progress ratio of the animation: can be negative (before start) or >1.0 (after end). + let progress = match self.state { AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { (now - self.started_at) / self.duration }, @@ -599,7 +600,7 @@ impl Animation { AnimationState::Canceled => return, }; - if total_progress < 0. + if progress < 0. && self.fill_mode != AnimationFillMode::Backwards && self.fill_mode != AnimationFillMode::Both { @@ -611,9 +612,6 @@ impl Animation { { return; } - let total_progress = total_progress - .min(self.current_iteration_end_progress()) - .max(0.0); // If we only need to take into account one keyframe, then exit early // in order to avoid doing more work. @@ -623,8 +621,27 @@ impl Animation { } }; + // Handle negative progress (before animation start) with backwards/both fill mode + if progress < 0.0 { + let keyframe = match self.current_direction { + AnimationDirection::Normal => self.computed_steps.first().unwrap(), + AnimationDirection::Reverse => self.computed_steps.last().unwrap(), + _ => unreachable!(), + }; + add_declarations_to_map(keyframe); + return; + } + + // Progress clamped to the current iteration (0.0 to 1.0). + let total_progress = progress.min(self.current_iteration_end_progress()).max(0.0); + if total_progress >= 1.0 { - add_declarations_to_map(self.computed_steps.last().unwrap()); + let keyframe = match self.current_direction { + AnimationDirection::Normal => self.computed_steps.last().unwrap(), + AnimationDirection::Reverse => self.computed_steps.first().unwrap(), + _ => unreachable!(), + }; + add_declarations_to_map(keyframe); return; } @@ -662,26 +679,24 @@ impl Animation { _ => unreachable!(), } - debug!( - "Animation::get_property_declaration_at_time: keyframe from {:?} to {:?}", - prev_keyframe_index, next_keyframe_index - ); - let prev_keyframe = &self.computed_steps[prev_keyframe_index]; let next_keyframe = match next_keyframe_index { Some(index) => &self.computed_steps[index], None => { + add_declarations_to_map(&prev_keyframe); return; }, }; - if total_progress < 0.0 { + + // Detect zero interval. Prevent division by zero from percentage_between_keyframes. + if Some(prev_keyframe_index) == next_keyframe_index { add_declarations_to_map(&prev_keyframe); return; } - // At progress 0, we need to handle step functions specially + // At progress 0 (start of normal direction), we need to handle step functions specially // for "jump-both, jump-start, start" step functions. - if total_progress == 0.0 { + if total_progress == 0.0 && self.current_direction == AnimationDirection::Normal { if let TimingFunction::Steps(_steps, pos) = &prev_keyframe.timing_function { if *pos == StepPosition::JumpBoth || *pos == StepPosition::JumpStart From 6d1e8dd4b9dcc850b180bdc347b7196551e8124d Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Tue, 24 Mar 2026 13:47:20 +0800 Subject: [PATCH 29/30] use debug_assert Signed-off-by: Euclid Ye --- style/servo/animation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 628afe9c05..3c5928cb81 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -683,7 +683,7 @@ impl Animation { let next_keyframe = match next_keyframe_index { Some(index) => &self.computed_steps[index], None => { - add_declarations_to_map(&prev_keyframe); + debug_assert!(false, "next_keyframe_index should always be Some"); return; }, }; From 1a41cbe49b6e9320e9a3cd9c7d50383dc74df17e Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Tue, 24 Mar 2026 17:15:30 +0800 Subject: [PATCH 30/30] Simon's comment Signed-off-by: Euclid Ye --- style/servo/animation.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 3c5928cb81..75879d47f4 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -591,7 +591,8 @@ impl Animation { return; } - // Raw progress ratio of the animation: can be negative (before start) or >1.0 (after end). + // Raw progress ratio of the animation: can be negative (before start) or + // >1.0 (after end or during multiple iterations). let progress = match self.state { AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { (now - self.started_at) / self.duration @@ -688,7 +689,8 @@ impl Animation { }, }; - // Detect zero interval. Prevent division by zero from percentage_between_keyframes. + // Prevent division by zero from percentage_between_keyframes. + // This can happen for reverse direction at total_progress == 0.0. if Some(prev_keyframe_index) == next_keyframe_index { add_declarations_to_map(&prev_keyframe); return;