From 6d39a2a88169a7b4c0de545d308f235f621381bf Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Wed, 7 Feb 2024 17:52:11 +0800 Subject: [PATCH 01/29] 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 75382fdd662c18810a7478e414620574def964e1 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Thu, 15 Feb 2024 18:30:14 +0800 Subject: [PATCH 02/29] Commit our changes on top of upstream Stylo This is a rebase of 2453c41cbecb675a0689463ab38b3d75caad8881 Signed-off-by: Oriol Brufau --- .github/workflows/main.yml | 30 +- .../workflows/mirror-to-release-branch.yml | 2 +- .github/workflows/sync-upstream.yml | 4 +- Cargo.toml | 36 ++ README.md | 134 +++--- SYNCING.md | 63 +++ malloc_size_of/Cargo.toml | 14 +- malloc_size_of/lib.rs | 8 + selectors/Cargo.toml | 8 +- servo_arc/Cargo.toml | 4 +- style/Cargo.toml | 48 +-- style/build.rs | 8 +- style/properties/build.py | 3 + style/properties/longhands.toml | 23 +- style/properties/properties.mako.rs | 58 ++- style/properties/shorthands.rs | 78 ++-- style/properties/shorthands.toml | 22 +- .../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/animation.rs | 216 ++++++---- style/servo/attr.rs | 189 ++++++--- style/shared_lock.rs | 125 +----- style/values/generics/font.rs | 20 +- style/values/specified/font.rs | 3 + 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 | 189 +++++++++ stylo_dom/Cargo.toml | 18 + stylo_dom/lib.rs | 181 +++++++++ stylo_static_prefs/Cargo.toml | 15 + stylo_static_prefs/build.rs | 131 ++++++ stylo_static_prefs/preferences.toml | 44 ++ stylo_static_prefs/src/lib.rs | 18 + sync.sh | 2 +- to_shmem/Cargo.toml | 6 +- 41 files changed, 1790 insertions(+), 472 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/build.rs create mode 100644 stylo_static_prefs/preferences.toml create mode 100644 stylo_static_prefs/src/lib.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aafc8d0e84..a4aabbe5d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: name: Linux (Debug) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests @@ -26,7 +26,7 @@ jobs: name: Linux (Release) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests @@ -34,6 +34,30 @@ jobs: env: RUST_BACKTRACE: 1 + macos-debug: + name: macOS (Debug) + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - 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@v6 + - 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 @@ -41,6 +65,8 @@ jobs: needs: - linux-debug - linux-release + - macos-debug + - windows-debug steps: - name: Success if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} diff --git a/.github/workflows/mirror-to-release-branch.yml b/.github/workflows/mirror-to-release-branch.yml index c8593195da..44fd4c3124 100644 --- a/.github/workflows/mirror-to-release-branch.yml +++ b/.github/workflows/mirror-to-release-branch.yml @@ -9,7 +9,7 @@ jobs: name: Mirror runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get branch name diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 992fe6d2ce..7df440da84 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -10,7 +10,7 @@ jobs: name: Sync runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 1 - uses: actions/cache@v3 @@ -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..5302b7e107 --- /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.18.0" + +[workspace.dependencies] +# in-repo dependencies (separately versioned) +servo_arc = { version = "0.4.3", path = "./servo_arc" } +selectors = { version = "0.38.0", path = "./selectors" } +to_shmem = { version = "0.4.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.18.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } +static_prefs = { version = "0.18.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } +stylo_atoms = { version = "0.18.0", path = "./stylo_atoms" } +dom = { version = "0.18.0", path = "./stylo_dom", package = "stylo_dom" } +style_traits = { version = "0.18.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} +style_derive = { version = "0.18.0", path = "./style_derive", package = "stylo_derive"} +stylo = { version = "0.18.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 7e2366d327..6bf6102158 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.37" 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/malloc_size_of/lib.rs b/malloc_size_of/lib.rs index 412ef88710..8a80bf3ea0 100644 --- a/malloc_size_of/lib.rs +++ b/malloc_size_of/lib.rs @@ -277,6 +277,14 @@ impl MallocSizeOf for std::cell::RefCell { } } +impl MallocSizeOf for std::sync::OnceLock { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.get() + .map(|value| value.size_of(ops)) + .unwrap_or_default() + } +} + impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B> where B::Owned: MallocSizeOf, diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index c919bcdc4e..de27b2e60e 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selectors" -version = "0.26.0" +version = "0.38.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 1d9dc4a1e2..6176a9a355 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.37" 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/properties/build.py b/style/properties/build.py index c09727cc20..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 diff --git a/style/properties/longhands.toml b/style/properties/longhands.toml index 1393e3491e..58ce039323 100644 --- a/style/properties/longhands.toml +++ b/style/properties/longhands.toml @@ -863,7 +863,6 @@ affects = "layout" type = "FontFeatureSettings" initial = "computed::FontFeatureSettings::normal()" struct = "font" -engine = "gecko" extra_prefixes = ["moz:layout.css.prefixes.font-features", "webkit"] animation_type = "discrete" spec = "https://drafts.csswg.org/css-fonts/#propdef-font-feature-settings" @@ -981,7 +980,6 @@ affects = "layout" type = "FontVariantEastAsian" initial = "computed::FontVariantEastAsian::empty()" struct = "font" -engine = "gecko" animation_type = "discrete" gecko_ffi_name = "mFont.variantEastAsian" spec = "https://drafts.csswg.org/css-fonts/#propdef-font-variant-east-asian" @@ -991,7 +989,6 @@ affects = "layout" type = "FontVariantLigatures" initial = "computed::FontVariantLigatures::empty()" struct = "font" -engine = "gecko" animation_type = "discrete" gecko_ffi_name = "mFont.variantLigatures" spec = "https://drafts.csswg.org/css-fonts/#propdef-font-variant-ligatures" @@ -1001,7 +998,6 @@ affects = "layout" type = "FontVariantNumeric" initial = "computed::FontVariantNumeric::empty()" struct = "font" -engine = "gecko" animation_type = "discrete" gecko_ffi_name = "mFont.variantNumeric" spec = "https://drafts.csswg.org/css-fonts/#propdef-font-variant-numeric" @@ -2367,7 +2363,7 @@ affects = "paint" type = "position::HorizontalPosition" initial = "computed::LengthPercentage::zero_percent()" struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" extra_prefixes = ["webkit"] spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-position" vector = { animation_type = "repeatable_list" } @@ -2377,7 +2373,7 @@ affects = "paint" type = "position::VerticalPosition" initial = "computed::LengthPercentage::zero_percent()" struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" extra_prefixes = ["webkit"] spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-position" vector = { animation_type = "repeatable_list" } @@ -2387,7 +2383,7 @@ affects = "paint" type = "BackgroundRepeat" initial = "computed::BackgroundRepeat::repeat()" struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" extra_prefixes = ["webkit"] animation_type = "discrete" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat" @@ -2398,7 +2394,7 @@ affects = "paint" type = "background::BackgroundSize" initial = "computed::BackgroundSize::auto()" struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" extra_prefixes = ["webkit"] spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-size" vector = { animation_type = "repeatable_list" } @@ -3759,7 +3755,6 @@ keyword = { values = ["normal", "text", "emoji", "unicode"] } [font-variant-position] struct = "font" -engine = "gecko" spec = "https://drafts.csswg.org/css-fonts/#propdef-font-variant-position" animation_type = "discrete" affects = "layout" @@ -4020,7 +4015,7 @@ keyword = { values = ["fill", "contain", "cover", "none", "scale-down"] } [mask-type] struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-type" animation_type = "discrete" affects = "paint" @@ -4028,7 +4023,7 @@ keyword = { values = ["luminance", "alpha"] } [mask-mode] struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode" animation_type = "discrete" affects = "paint" @@ -4037,7 +4032,7 @@ keyword = { values = ["match-source", "alpha", "luminance"] } [mask-clip] struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip" animation_type = "discrete" affects = "paint" @@ -4047,7 +4042,7 @@ keyword = { values = ["border-box", "content-box", "padding-box"], extra_gecko_v [mask-origin] struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin" animation_type = "discrete" affects = "paint" @@ -4057,7 +4052,7 @@ keyword = { values = ["border-box", "content-box", "padding-box"], extra_gecko_v [mask-composite] struct = "svg" -engine = "gecko" +servo_pref = "layout.unimplemented" spec = "https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite" animation_type = "discrete" affects = "paint" diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index b3ea47e9ce..76e880b8f8 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -32,8 +32,8 @@ use crate::typed_om::{ToTyped, TypedValueList}; use crate::use_counters::UseCounters; use crate::rule_tree::StrongRuleNode; use crate::values::{ - computed, - resolved, + computed::{self, ToComputedValue}, + resolved::{self, ToResolvedValue}, specified::{font::SystemFont, length::LineHeightBase, color::ColorSchemeFlags}, }; use std::cell::Cell; @@ -384,30 +384,15 @@ impl NonCustomPropertyId { % if engine == "gecko": unsafe { structs::nsCSSProps_gPropertyEnabled[self.0 as usize] } % else: - static PREF_NAME: [Option<&str>; ${ - len(data.longhands) + len(data.shorthands) + len(data.all_aliases()) - }] = [ - % for property in data.longhands + data.shorthands + data.all_aliases(): - <% - pref = getattr(property, "servo_pref") - %> - % if pref: - { - const_assert!(!static_prefs::default_value!("${pref}")); - Some("${pref}") - }, - % else: - None, - % endif - % endfor - ]; - let pref = match PREF_NAME[self.0 as usize] { - None => return true, - Some(pref) => pref, - }; - - // The assertions above guarantee that the pref defaults to false. - static_prefs::Preference::get(pref, false) + match self.0 { + % for (index, property) in enumerate(data.longhands + data.shorthands + data.all_aliases()): + <% preference = getattr(property, "servo_pref") %> + % if preference: + ${index} => static_prefs::pref!("${preference}"), + % endif % + % endfor + _ => true, + } % endif }; @@ -1710,7 +1695,6 @@ impl ComputedValues { context: Option<&mut resolved::Context>, dest: &mut CssStringWriter, ) -> fmt::Result { - use crate::values::resolved::ToResolvedValue; let mut dest = CssWriter::new(dest); let property_id = property_id.to_physical(self.writing_mode); match property_id { @@ -1767,8 +1751,6 @@ impl ComputedValues { property_id: LonghandId, context: Option<&mut resolved::Context>, ) -> PropertyDeclaration { - use crate::values::resolved::ToResolvedValue; - use crate::values::computed::ToComputedValue; let physical_property_id = property_id.to_physical(self.writing_mode); match physical_property_id { % for specified_type, props in groupby(data.longhands, key=lambda x: x.specified_type()): @@ -1947,11 +1929,19 @@ impl ComputedValues { // whether the name corresponds to an inherited custom property // and then choose the inherited/non_inherited map accordingly. let p = &self.custom_properties; - let value = p - .inherited - .get(name) - .or_else(|| p.non_inherited.get(name)); - value.map_or(String::new(), |value| value.to_css_string()) + let Some(value) = p.inherited.get(name).or_else(|| p.non_inherited.get(name)) + else { + return String::new(); + }; + let mut context = resolved::Context { + style: self, + for_property: PropertyId::Custom(name.clone()), + current_longhand: None, + }; + value + .clone() + .to_resolved_value(&mut context) + .to_css_string() } } } diff --git a/style/properties/shorthands.rs b/style/properties/shorthands.rs index 8fed140360..340233a009 100644 --- a/style/properties/shorthands.rs +++ b/style/properties/shorthands.rs @@ -2555,13 +2555,13 @@ pub mod font { use super::*; #[cfg(feature = "gecko")] use crate::properties::longhands::{ - font_family, font_feature_settings, font_language_override, font_size, font_size_adjust, - font_variant_alternates, font_variant_east_asian, font_variant_emoji, - font_variant_ligatures, font_variant_numeric, font_variant_position, + font_family, font_language_override, font_size, font_size_adjust, + font_variant_alternates, font_variant_emoji }; use crate::properties::longhands::{ - font_kerning, font_optical_sizing, font_stretch, font_style, font_variant_caps, - font_variation_settings, font_weight, + font_feature_settings, font_kerning, font_optical_sizing, font_stretch, font_style, font_variant_caps, + font_variant_east_asian, font_variant_ligatures, font_variant_numeric, font_variation_settings, + font_variant_position, font_weight, }; #[cfg(feature = "gecko")] use crate::values::specified::font::SystemFont; @@ -2668,17 +2668,12 @@ pub mod font { font_size_adjust: font_size_adjust::get_initial_specified_value(), #[cfg(feature = "gecko")] font_variant_alternates: font_variant_alternates::get_initial_specified_value(), - #[cfg(feature = "gecko")] font_variant_east_asian: font_variant_east_asian::get_initial_specified_value(), #[cfg(feature = "gecko")] font_variant_emoji: font_variant_emoji::get_initial_specified_value(), - #[cfg(feature = "gecko")] font_variant_ligatures: font_variant_ligatures::get_initial_specified_value(), - #[cfg(feature = "gecko")] font_variant_numeric: font_variant_numeric::get_initial_specified_value(), - #[cfg(feature = "gecko")] font_variant_position: font_variant_position::get_initial_specified_value(), - #[cfg(feature = "gecko")] font_feature_settings: font_feature_settings::get_initial_specified_value(), }) } @@ -2737,26 +2732,21 @@ pub mod font { { return Ok(()); } - #[cfg(feature = "gecko")] if self.font_variant_east_asian != &font_variant_east_asian::get_initial_specified_value() { return Ok(()); } - #[cfg(feature = "gecko")] if self.font_variant_ligatures != &font_variant_ligatures::get_initial_specified_value() { return Ok(()); } - #[cfg(feature = "gecko")] if self.font_variant_numeric != &font_variant_numeric::get_initial_specified_value() { return Ok(()); } - #[cfg(feature = "gecko")] if self.font_variant_position != &font_variant_position::get_initial_specified_value() { return Ok(()); } - #[cfg(feature = "gecko")] if self.font_feature_settings != &font_feature_settings::get_initial_specified_value() { return Ok(()); } @@ -2885,29 +2875,26 @@ pub mod font_variant { pub use crate::properties::generated::shorthands::font_variant::*; use super::*; - use crate::properties::longhands::font_variant_caps; #[cfg(feature = "gecko")] use crate::properties::longhands::{ - font_variant_alternates, font_variant_east_asian, font_variant_emoji, - font_variant_ligatures, font_variant_numeric, font_variant_position, + font_variant_alternates, font_variant_emoji, + }; + use crate::properties::longhands::{ + font_variant_caps, font_variant_east_asian, font_variant_ligatures, font_variant_numeric, + font_variant_position, }; - #[allow(unused_imports)] use crate::values::specified::FontVariantLigatures; pub fn parse_value<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { - #[cfg(feature = "gecko")] let mut ligatures = None; let mut caps = None; #[cfg(feature = "gecko")] let mut alternates = None; - #[cfg(feature = "gecko")] let mut numeric = None; - #[cfg(feature = "gecko")] let mut east_asian = None; - #[cfg(feature = "gecko")] let mut position = None; #[cfg(feature = "gecko")] let mut emoji = None; @@ -2920,10 +2907,7 @@ pub mod font_variant { .try_parse(|input| input.expect_ident_matching("none")) .is_ok() { - #[cfg(feature = "gecko")] - { - ligatures = Some(FontVariantLigatures::NONE); - } + ligatures = Some(FontVariantLigatures::NONE); } else { let mut parsed = 0; loop { @@ -2937,16 +2921,12 @@ pub mod font_variant { { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } - #[cfg(feature = "gecko")] try_parse_one!(context, input, ligatures, font_variant_ligatures::parse); try_parse_one!(context, input, caps, font_variant_caps::parse); #[cfg(feature = "gecko")] try_parse_one!(context, input, alternates, font_variant_alternates::parse); - #[cfg(feature = "gecko")] try_parse_one!(context, input, numeric, font_variant_numeric::parse); - #[cfg(feature = "gecko")] try_parse_one!(context, input, east_asian, font_variant_east_asian::parse); - #[cfg(feature = "gecko")] try_parse_one!(context, input, position, font_variant_position::parse); #[cfg(feature = "gecko")] try_parse_one!(context, input, emoji, font_variant_emoji::parse); @@ -2972,6 +2952,10 @@ pub mod font_variant { #[cfg(feature = "servo")] return Ok(expanded! { font_variant_caps: unwrap_or_initial!(font_variant_caps, caps), + font_variant_east_asian: unwrap_or_initial!(font_variant_east_asian, east_asian), + font_variant_ligatures: unwrap_or_initial!(font_variant_ligatures, ligatures), + font_variant_numeric: unwrap_or_initial!(font_variant_numeric, numeric), + font_variant_position: unwrap_or_initial!(font_variant_position, position), }); } @@ -2981,15 +2965,12 @@ pub mod font_variant { where W: fmt::Write, { - #[cfg(feature = "gecko")] let has_none_ligatures = self.font_variant_ligatures == &FontVariantLigatures::NONE; - #[cfg(feature = "servo")] - let has_none_ligatures = false; #[cfg(feature = "gecko")] const TOTAL_SUBPROPS: usize = 7; #[cfg(feature = "servo")] - const TOTAL_SUBPROPS: usize = 1; + const TOTAL_SUBPROPS: usize = 5; let mut nb_normals = 0; macro_rules! count_normal { ($e: expr, $p: ident) => { @@ -3001,16 +2982,12 @@ pub mod font_variant { count_normal!(self.$v, $v); }; } - #[cfg(feature = "gecko")] count_normal!(font_variant_ligatures); count_normal!(font_variant_caps); #[cfg(feature = "gecko")] count_normal!(font_variant_alternates); - #[cfg(feature = "gecko")] count_normal!(font_variant_numeric); - #[cfg(feature = "gecko")] count_normal!(font_variant_east_asian); - #[cfg(feature = "gecko")] count_normal!(font_variant_position); #[cfg(feature = "gecko")] if let Some(value) = self.font_variant_emoji { @@ -3043,16 +3020,12 @@ pub mod font_variant { }; } - #[cfg(feature = "gecko")] write!(font_variant_ligatures); write!(font_variant_caps); #[cfg(feature = "gecko")] write!(font_variant_alternates); - #[cfg(feature = "gecko")] write!(font_variant_numeric); - #[cfg(feature = "gecko")] write!(font_variant_east_asian); - #[cfg(feature = "gecko")] write!(font_variant_position); #[cfg(feature = "gecko")] if let Some(v) = self.font_variant_emoji { @@ -3630,7 +3603,6 @@ pub mod animation { } } -#[cfg(feature = "gecko")] pub mod mask { pub use crate::properties::generated::shorthands::mask::*; @@ -3657,12 +3629,15 @@ pub mod mask { mask_origin::single_value::SpecifiedValue::BorderBox => { mask_clip::single_value::SpecifiedValue::BorderBox }, + #[cfg(feature = "gecko")] mask_origin::single_value::SpecifiedValue::FillBox => { mask_clip::single_value::SpecifiedValue::FillBox }, + #[cfg(feature = "gecko")] mask_origin::single_value::SpecifiedValue::StrokeBox => { mask_clip::single_value::SpecifiedValue::StrokeBox }, + #[cfg(feature = "gecko")] mask_origin::single_value::SpecifiedValue::ViewBox => { mask_clip::single_value::SpecifiedValue::ViewBox }, @@ -3889,8 +3864,18 @@ pub mod mask { writer.item(repeat)?; } - if has_origin || (has_clip && *clip != Clip::NoClip) { - writer.item(origin)?; + #[cfg(feature = "gecko")] + { + if has_origin || (has_clip && *clip != Clip::NoClip) { + writer.item(origin)?; + } + } + + #[cfg(feature = "servo")] + { + if has_origin || has_clip { + writer.item(origin)?; + } } if has_clip && *clip != From::from(*origin) { @@ -3911,7 +3896,6 @@ pub mod mask { } } -#[cfg(feature = "gecko")] pub mod mask_position { pub use crate::properties::generated::shorthands::mask_position::*; diff --git a/style/properties/shorthands.toml b/style/properties/shorthands.toml index 0621df14e8..7992b994a5 100644 --- a/style/properties/shorthands.toml +++ b/style/properties/shorthands.toml @@ -392,16 +392,16 @@ sub_properties = ["background-position-x", "background-position-y"] spec = "https://drafts.csswg.org/css-backgrounds-4/#the-background-position" [mask] -engine = "gecko" sub_properties = ["mask-mode", "mask-repeat", "mask-clip", "mask-origin", "mask-composite", "mask-position-x", "mask-position-y", "mask-size", "mask-image"] spec = "https://drafts.fxtf.org/css-masking/#propdef-mask" extra_prefixes = ["webkit"] +servo_pref = "layout.unimplemented" [mask-position] -engine = "gecko" sub_properties = ["mask-position-x", "mask-position-y"] spec = "https://drafts.csswg.org/css-masks-4/#the-mask-position" extra_prefixes = ["webkit"] +servo_pref = "layout.unimplemented" [border-radius] sub_properties = ["border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius"] @@ -485,6 +485,10 @@ spec = "https://drafts.csswg.org/css-text-decor/#propdef-text-decoration" sub_properties = [ "font-style", "font-variant-caps", + "font-variant-east-asian", + "font-variant-ligatures", + "font-variant-numeric", + "font-variant-position", "font-weight", "font-stretch", "font-size", @@ -492,25 +496,21 @@ sub_properties = [ "font-family", "font-optical-sizing", "font-variation-settings", - "font-kerning" + "font-kerning", + "font-feature-settings", ] extra_gecko_sub_properties = [ "font-size-adjust", "font-variant-alternates", - "font-variant-east-asian", "font-variant-emoji", - "font-variant-ligatures", - "font-variant-numeric", - "font-variant-position", - "font-language-override", - "font-feature-settings" + "font-language-override" ] spec = "https://drafts.csswg.org/css-fonts-3/#propdef-font" derive_value_info = false [font-variant] -sub_properties = ["font-variant-caps"] -extra_gecko_sub_properties = ["font-variant-alternates", "font-variant-east-asian", "font-variant-emoji", "font-variant-ligatures", "font-variant-numeric", "font-variant-position"] +sub_properties = ["font-variant-caps", "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", "font-variant-position"] +extra_gecko_sub_properties = ["font-variant-alternates", "font-variant-emoji"] spec = "https://drafts.csswg.org/css-fonts-3/#propdef-font-variant" [font-synthesis] 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 zcmZ{L1CS_DvSqvbwQbwBZQHhO^R@Nbwr$(CZQI)SwKIQqrz$Ecs-n(`jEq|unR!pj zO96u*0{{R(0OUa%C>X3D?EV4-09XJ50D%Ad*4fU+nodv8!q&oBPmk8#!zW(Ga)2H_ zt)&#X^jBvJ);Ay*b85!!)NYpXHPEW+OS2l_Eq|0tQN;LCf0iEln*OLW3 zI7hYx=P>~YEKU|s69;$^FU+lP6$5769}=u24&N0b%m+ra*5ch3(dB(X33I1stfO$HciH_5$xkjYHuv@RiA+zG6dN)_wHokI}jtH5+Z!{M|QROK7n#23J83?qikoGnD-Q zbz3l|o`Nv8ghwAf003?(007+oxXswa$j;cr@vqmWc&wZ@nv-|FsMKBE#Fy%qT|GZ$ z7rPd064NHPH(b}9Z!VL%FeUR1hY`yX{uq)i{B)lH`nxMOCSKE;?u#Sa|2=fs*#RQ2 zs>?UblbSrVtRE5sd(f7~+N+xtmW21mipd{Hcg7%_%GbG>$+X+5nlmImWSiYV_4K;9 zI4G9Y%^XKBic7~m6sjvuGst~dab@HP$sahW^iJkfoM-UzE;^qSjv@_|%P>1W%F`;3 z=!tu`EoRsIQ;n(ajKR&DpMY1q?+zkQTy*x3d&pVeVpTr!*HfrGT@!Zb9(1t2Qf-_( zcbPrjkkq(2Jf7}vW=?vz#bU`dVh#T|VE=GUVoJz8X4-@1i~gW06#e@27JM?7^*G?H@=R8TDvvAI%q^p2g3c<@~ zvQ(&vbTny}Z*n?Cq9nWd`K@?K8VCnq&_i>!V52(lQ(#AE^ zyjxpRjmw*1zjM3d4bA?bJ~hIQkD2_Yju6I|$|q?!`$Hd=BkDJ^eq^&PtnqkW((^+@ z`LZFR8(YxQlB<|+-B7m8nyaUZ4>?gD>Pc^}pKYB)6CpFln>pDw^l3A%@$JAU+v*wWlX(ECK{qSB9=R4Uyar z8)t&rTm7eB-Z{kEYpikN#}9|-Ak+}wo+sDxGylNet z*kT$wW^f8ksJD9@rIr$kR&(YBaHC~F#VE2k?u z-PFsVKw;Y&M}mH@Az}bQg$x!2akC&k#d8%K+P(tVFVXVUJ@hc`eTY{e{QY&)Zu)Ti z#CsOUM9XvJE6#kymnyk$>k!^^Dx${9 zNPDmW;QOzNJ z9920H5pz>>z!UAHeY!>M-srU+ zMD%9`6|__(qb0TEFkM1HT&B_iiAJ;BY1(z5ey-})RD!p-PXdxY_$hy$Lu^EFWN)`lGF)mD-CAGmd z5E7{sOqI!Tu`Zsg(f$Qu6>zUCMG}5=u-d@4r79D$2Y^8_GngwAEw_YcR?SINw`CJy zGB;niw{Gc56fGu?f~^9~PpymlbP;gWT^&G}@fm0LTS2oe6LDNy4+Nb6bBe3s?VZEI z7Ta3M=HVHY&nBV+8)7s%ULwc`Vnt8yeRtiuA6R0J-Zu#XKt-o_KW2E0>c~_m@}O8| zh!aPKTHVXtv%0$iUF$?XVgwl&0C+I4dCP7+P#S_Jy?yYo7urh%5-DQf`vmp%yUCUtPtG$uiRQ^P{XP0g?|5+Tct0?@ElzgkBZE%<;a0D^-%`ZRP+iP8+rDk#C$+8T;)F_ zpqPxsRbphg;g9QL>Fw+o-gT|0zFOM-3rqL5*4Pcm#66v+vf8Km1fNz`~YU`m*!&z~_ zuGJCGO~3xr7d|_EdbA< zJ9R&CgQ&klRC?VNm4!id1(c*M&UFAjpcmcC0?GYp)vUcWCqxJj`S< zYcTbaD?Yay3X#b`_I>slvEU+e=+*^j1w$3WWl4!?=M z_8j^0Dn!I|l*>y*Z}26INMc8O$1TqFc3$4}zN*iIQ@_;}-Y6w^FA7IbF>@L2s*hGG9=W3bJh#$FDH-Vj^O|o?g9O(p{>j%7HmR?p3=x?$(hFvPDub?Q8bzraG z3LA@zhV|<_5#&G}Dk0FKQ-uw-tX(&?ja~X#2tL7h)$s4D8a4EFi8>ZNB9#^9I-&STt0DZ^(uxTHL?s}7L~L~cVLKjunO{(?-0hD`fag4X2e{3bJVX=D;s-#?MnAxMvEMy=SgFSmYEHe-}0SXbR+L#iHNn zeYL>cT!Gg^3B}lNvB7lyk`&kmhI9pNmg;5%Ur)sqlAJ3S*;@_tdx;>UxowK7cQ`Mh!To zQU`0w&3WikrY`U_|D%TxWaP?<`c+%*ay*StZ9#oXxmP7XNf&We%r9JmmQHWS3A66x z>0}tA^O)Wa(q6#`du$wfJc$kAAsWZ&4CyCYohF*kcUmEIw@jxTl%f34IZ$WPxAqByrizcp#D|W8ip9~+&1eP0L)0M3Qga}xYOdWGp|~$EZNP(fq*NQ`(oyP5 zQX0!#<(M{3-L^PVC+u^QHYO#%mWUR)Rg~_o?$Mb1;44drf!{Irf?zII1 zUF;xpAnLyf$_nP9pb}jwBzsB8LwTlC)}nS|#jr1vf3+Yu7ESfClKm4gK!OmBm;v+C zgCleiLBPOK;0=oWuPm8fiNG%f$FyBCN@_$hyRgm;81fh6W z)r&tB8=g}Dxx%4b0a%Yxps$Ywh4=(BFPFj4zj5ezMBH6XF*K92r=fkst;;Au6S3Xg ziu6@^)0PWD?HNKp-)N`Q!>a?<#WvSV84{fSdW3-#^*8Al=1d%z)OD|F*2TdvlbqXJ zGn(v>FBJtkHjW&rW$ybWcb3DMg+sELr8&D0x4dTlK;kEs+DARiIhQ&>;$y^S`5Wab zY`2|kEuMTsU-VmO5FF|Io!Kdv%9^PC=}#Ij;{+k%>`?+9iY<44g`C5vGDRCo9ZQ#j zyD9f3(`n=H61SXmg0S0xR16zic)VB!Q=p*4Z6Uq>=fn|m5v8_YZ|4fOctO>80mXQ- zGrX$MsaXc^ROV|jPnd}Q0!*<(qu;xurf3S`J6gU0pKnocRl|PJdKBN$$sw)Ow*G`1 zH{^R9AKoI%NzVmf;L@*+-U~NO?q!yPRBbC9LQ-$sU9vlM5fHqRUm6H*Oo5E-pKpV+ zWHN@{C=X}!Tq?a%NVlw4;U%KV}EwjM&N{<77dm zZs9ZS7l+Ec2ywDr2e5!!Q*7!gE5Ph&)hIFNx)+Ehhckbx12vBb9q4fZ{YJyl_NGx9 z=?b8#3}ndSDLv*a8gjdz;a_cCqeJI#F&VtP=WYeF-f?Jp&8aR8-NNmHlV4@_oKu+f zJrMBP)i1G_apf8IcdZ2~hN8N3O$0&;R|LRf-9)9qMsKiK5n~6`nIE;;Zi0={_#5W;cWo9-1>vT36@hxzpFdd`xSW*T(e!YI9OCjaZEpp{BtHAl|KV^I{CQI z{eIa%{spv&wVtv}5Cw*Vv0P_j!+`SP8`FM;nL+tWW>kZE%M6Io#cq_nx{vZhEx64(JI_H_?7ElQ*S?|W zU1d$*(8)q1T@mi8QmY- zWU;QJJ|8bGcBtjFS>DFag-OrxL5tBRj?&o*e!cHErD+27ghMzw`Z~N_KJ<9s9jWP= zMUvdA80t9+e`}27=$I#K&|2Q_ym+)tmLTr}jJnD>&ecT>A=8*z>Pfo%>}}yfg$*X2 zl6ATOq}$bN>d1)?N2bskuI2CO3p7l=AS7&u`3}zwp%KZ!aF@XhnyKnU67)Fabh5?5wihb!X@E0e_$dXNZZSBy#cE)0GEnL|_uc9*NLAZ$#ZrC#rEZ zI-d*iDmH#YikO7y4@wh86%Rd?lMxKVZh{HD9=g?zcH&QQXGO87FH!JUWeN`N!5id_ zI+Kb2Ib9jbi^+ARi{ruJQql8!sAA_H``{6~b~gA5S-8cq7>Z0*U4~(ZS>|Tc^ihox zhJewNIe!(5PC-U+52;JXi=&9Pdh-wSok9rr4$7>m-p%c}G4~5?u;f@AaO38D5n0BO z20CxMsOc~n>^`EkpUTGR=x-0udM-8Lyl+C8gQtVQ9fC_0)S|W_bnzcy1*CC-A%5$o zO!0@GZjzBq2^Kv_X$+l#o@WwxT^Dt^frwa-Pyp5W;#WKHM=}Nzyw%jkdzDer@KG14 z2X{7H;NT~TKF;1nn(@{fJc>}3vfCVrQnd&6LF}tWJ$;>45e*q#%Y%j=&-*=+zPk2O zt&ecs1rzDxlSFO%13j#BEu!_SRCQ>qdj6$C1LuRwdETRn5}o=eI>;9l7sYQlm8R|d9t}PLSv!DX zhlIZbv#Ya+O?;Y@k3My@pB(WO^^w+tU^HBU-ei!UEqe$vpU{Q1J3OpOcAL!h;xN`2 zd6Ps<3w>F5L+T3ufXn;$5D`EstaZbw1qcwEci1$ri@2~p{>t6pn+Kdr!#dQA-Wc7iHfN}YUfx=-o1S2hC+sQULEZLz8*h-4*YMFktis}-sxDhFdjtZ%Wo9?=wX zuS|#&yb`k`n*jm@-lM@brJFcxbUCcZRCoagI^2NM=CDUv9&&{{2CX|?=*F`L;_is+ zJN)Xc)i|RJbX`;@R@~GM9ZYi@<@yj%>)$c;qWVD)4AqvHqlqZIX|j+DFhnFSn;-Qp zfsY}1@dMV3Kpn#)i7CM42M7`qw*8E)NAj zUamSW1UDM5j=lWp3<3~tQcq6?_^sG{1zX^6d6Pko=$(6h|7zsq$0;0n4HHLezs($^ zeSXz#-}JnETOihnvI8sFw`Wm7X#1X&GYT7k9)YY;0X`CQu{QhV7xR>J2ByLKxecZs zdhP<4J0kgJcR}Q3#PPeH`~ETl-Dp%lgGVw0r+&Hq@n#nCG9cdUQ9*)7r%1X?O2k9k z;?#H<*5&mjDi$a97q)7GGb$1aCl}!qz_MzEyj*u;AXzzQ8xkkzqvZA?O#Bgw?AUdO zQw$c|vs+Tg-L&Q~Q_}$=kR~W`%biN7obtfjTc#&S|D+s}$D-1?Kd% zei9MqMfg77QEkad?upTivmOOA4lSO2qgE5D9Uq5-<^7x%0~DFT>@vn7?S3zKoCurI z#Jh9izB@M9N5uEkT^s3r7|(HEz7I%ah8*+8YR>8>i1;`6^sa)>dB_US!2LSdWMPV6 z{6l?KuKq2glppx2Z1`duEzP0(0fxv0B^yI-Lu8$GSlIP zGli9#E;B#~FOY095XD0ay5Y6~v4AUS^ccIo^fti1%k7P58iG4m}Y=q(w7Alkaz!zk9qI6n(I(xK5ZF^CcU-r^I z+$F$jgy+?|=tx+sr~Lf_6Kpr7ikDv4Ly_g{gUKSiql{1bH0Fcm&Zk-|!#=0**A?T3B#zZ{Z4t z_0-jJcshMjD#A?%)exCTccnx^kGS^I#MQI+;e-qtU2osCA&Svev+i+CuAkG2Jqd(s zq#Y)T<@800EGrTp(5H?Y_b^J(jm%&}Ql2VC7@9#1w)lwk7@wK_jO>M&{Ki~Rw%m=- zkJnE6iK(~?YWKe6#PDGAih;`KblehvEzil9<5}C<6Mlq}=i92~Q{FKwt5N(JeV{2s zYQ{q^dwRwX_W{hCZKmdYIg*v>14_ zMRX%D2agcf2XhCyy4KV_$P&L2&28^)CN0T&y1TYQHMXo%8ITeZh5)cXa7!S6bLFM| zG)Tv`Z(6vQrhLuGmN)`ygmgL@*{%Ru%i6uMn(=`F9y#4|U=B5r!f-5Mb5=}I@gA%s zO#R7lCd(Hr08US?r->cB8u5em)`g*vTCm54obv%5s;|%en!^CC6>?9tY+fT4j+nFT z^di$(-jyMM2wnx48~D?fLE@afI~p2Z0sa$@_5?Xty@$oB?;~g@<(0C6(tUEb+Hq4l z_)b+Wz`&#I)mCEUg%Sc$4(QcLddT?FVpLR20MwW6H2`b!y>m zga$?P)SB(HEdVWIjg3Q2K-;GF;)JX2N8<6~bf3xhp*Z0zUI^Ra12&0usbOK@(6+lo zQ%hhILC9wt38uuIuX2a^Qn0#(?e580+~T(H02Ygo6pR+M9OJ_cx`1L|>Z%fDyp{KR z{*+sM|MTwqwvP#DtVemEJ5OOdNYtACTz5ZCa@e1PVw|Pwx=GALb-iO5FBoF$L52@X z;9;P+!N5rCDvxxGr-nMN{FIw>U>DJ4S1om`hLXFf_XvudUf2CMe3DBTt=_wogpTfk zx#?O>f#|UAmN&~u*U}=$O4AlsfB$t_vRf!y0E#Un>C-eBaIe-woLei`wZ;J0^&PA0 z^ALp0YQ$y$K2svk!e}<2Z`$OnE8Wl3s013r&196UiH`I5kEo|ncNB(#yJdC^I9uP| zaGmy(r;H6=2H2hu-oTUK(rmDff9~Tk8WQ zZzpQKD`G}Rp5m7)?uQfPS$d9WcUiJQBE3RVIyF_C7aU*rkT-2@KhBhk7pVQ|w1r5? zdDF1zq-ICk9wlWu>Ni$ZZ*`~>V&@QS0wWeClc5)*TN^bXVj?d4_%d>Hqni$>RU9Wv z-~_D^p(qR12F;4DBccgM%+gH6G=h)gQ`1=M?|=Qy_iTC zmpg!R;782LupzH%s(WY9eBb?7`iEs=A9AK_?sW@GBzI)YUaDRgy^2v!=KUCjwuzNm zH#4Upcp(45-eE$`%IKtR|L$Hih2LHY{;U{X$>dPIfy)!vRljhRL^CL_&obOEKB)bL z#=oTub>Q16d5g0;j{}^Py%rNT%-oSi>U6V*vzsw)Y-VIQ>AVdr&a_)!8N*Q%2zgq{ zO`z_{QTMPyx!j#;=r}l%N3v;RwnnAu*eaiGoIMupjqkaAo8hfgUhO?Dr+^o)queeq z`s3bb+TZh1KIK(%ohgxeV@Cm(nbfJxg; zg?tg>V;L9gK4h<1Sl5G3U^MOk(@kKVgx^4Fb|wvDeT^HNi>=zGBjm~g=Z!~`KJEQAbt`APWdLC0df3bUj?(@iX z&u#$P2~z=kIIhlP0p*A2%*LDAlh^H8s&CHD#_rCjt`c8K9h8$^jI(+NOKG3z_-`5DdlKoNn)qvyVqJ4nxi3csqNjhf<9D>VD)i0( z72IHicCztz2<=LwfHSWN2tkX#e#ch#`;vLyy77|27lfZjwPW~5Q+nEGh95gSv%V@0 zmni9WqDczlb3?rLlV*vb_lirVv!I@qDRqi+7P*r}=d%BjK3Beddi;IWbsZBrH}(VY zzts+8^6XQ&kN^OSr2nLLFtPo=Y6na!yAAgH4nI)2t`OqErdPJz}-To}J0K1aZxXg-u^}zuz>D+f13@Nsn<8`GykOca%K03gy|6 z&uVY-F|HxpwZV1^LYWL*-&vm6WmA&4^$_^R{gGh%L8FR&HTyEdJyI=8=6|l^G@TDc z(j@C-hh*zpoKQP4!x|8Rou2%%D=+(IE}rEW#}z@6s0 zx6#aZCV+NqJOnma&Y(m((p^v+V8Pyx2a?g0Cp!;U13bRJL;CMj6kS*ys;6XL`ERR2 z&NgnVmbDVp=Ze5a^b%H*_x-n zV&Sh18$PhHPB3-T{|Xr_KPM&Hd(|;OmVkKNm=kF5aMWA$m$FiXzdv&84cQF(qHREh$YY3R1tr-2%GM>nIv|lbFqh#ZP`Z8J?7GZaUvA8wD*Yj^%(T)`9UvCPv zcegafH_(t*ahuc;?!|97^G&`q*ifR9zRk+z;3$xXwS7P{Y40OdB9i= z?>*_gB>!efM59!Sz#zr8sc_c= zH7I}E@yw|DIr_my2mp#M2Dqpn&@+%_e+sDNZ7_D zjxas#e7n~WGg{fj{lNx`1NKG4>WZq(D@k1i_a-?3FGNoNL|1+&m#BF~%RJbA<3c^WP(etR4+ONu_7C zS_qa}0xV1F1>=tz){nX(Cnna8NJx8jzU=)o370}1 zh_t~un#prI%pJ5y8eQBGZ*{yu48k_rg~Qh8L>FbA^CvLX=dGyE#JI-LVZa9Jx7h2P zxLwKxa6;ABN3tit7}T<+ox}1e zbbF;Wr0vNM^V7)%NQr+_Sj0|su#B3Cw&aC;$N5W64^DR1%Yn9Dp>A3^Pa;5{ zfTv>1ZZMDtoN!ERkJQ3tuZlWkJH>;voEj_wQi)`S2nF1S?Pmv?uJC}-t->j8FLMlRidUVDvHJzj%kvy&NvT*OB%@F??Q0|9 zldm!`^K9y$db-Se3rh3~SYB~`V;(m0OI1VDY!?K^X>sxZMO8xX{{kbgEm0|07G#eGAO&C4bOqSK?8qdY~v5GFgF~>#=8&~wO z$;X%PV8mzzwFBoqPJsL8g<-8GqFv1y>RK)?=@gI!N7m<|2{gi#2I5FtMl1;$S7>p;+(aTKDX^x9+}E1IBu9rv_BhPEMX?;oj`)wS(yN}M`q@( z+n7A%=|}DlQY~Rp)y=HAYXdu;u9Eu zJTf4l8n(_Hl7@Cn!mv}U%7=uP-^#Kxh6jbKD{TMI3TFiyH`73n?T&J z5TDTNKSS?wYZ$6L zb#pT)3i&j@QOC*hWWJ7ze(pE6Lz}JUWyEn7($jYtU`3|sOeNZiL=in9p}*|Sa7u(u zJ$ji19Ns;3jC#o?jeVYljb)*T;Lh1>K2DY6n40fV4_Bn_Fd z&XykgP<|mLCMIBW4K49Xv7KBBowfxQ-5AWEH*j?Mc%NS(9X9SRrFOxtu{|WRB~5>L zo0!wYa=mZRQM1TDO6k}uH{YPje;uTF?5${%xal@rF1k<1L&W|3lXRCpI6zt>=Rz*> zuVmNouj={#N_XuX{|a|ajQ>h^n__t&2k4MvS>`|8`xCZSd=(Ifqe`8NCptwLYZvjDUiC0 z8rK{ggxVuvV8hO zDwP4l!09Xj5*1}}zJRt!eo?%^0sHeZa~pJ7RV)+YFA1#D-oT@XofQ8ZAF2yXqZ%C7 zUX@u;v}D6U+JHax>uJfqP>{Sp{`a5&70;7}B<@)PB4r zT>Ml__@CjOhc&q2hSmV>>#lrOe=r-P87~3DwyBQ|J^zSceuyTsr%MhPF7H;Mc+u^k z^K1*(X+W#bRB_QNG{bH5NECNQkaPLvam4Ra=9updZ)G%SJpB0t`iI*}{tDa`bt}e( z|GMlD1ONc%zuiVdPs>10%SdZ%;p9wXVQXqfCoLf)BC8~#qa+o(Ko8&jp%(QFqNuJ5 z1j5$h(h|xXp6NV+yESM7U9B?&Tk-WWS*Rng7J)c*;WbGvW2aApHELt!%^Wo~aAr{( z#@iR>cEi`*x1zSHc~Z$MXz~aj)N}nN%#*YCaawtY4?l=AO~gj=>0Jkis1J3T$p3|J%Uq9Dy{{$8#~L z?-F%nE2|<@+^FhWZt<{4k-dU8(lt=!m4aB?&&M2^Nka;zk zp0T#u#YhY@dtw^*DkNZ->?NMy6nzfxY;qT!v!kRY%{ps1tD$Ztw*jOJ3yGA7q-4SD z@W2Kg0PYl>P<@fd9Uy^wi98qMAW>40v5bX@*} zmmLjvOcTr7Y)s*ESUrfO3(9Zu@W@*m4^fgwDi=8OTklCRqRSQ5^|+q?uqj*Nd~KTl zkzsGI%GDlnVDM%BnC89)Wbvqe&5?wES-$LEmv#`y);;=8225+Ma$E*@HP3yHo4*Z2 zm%A5EgPIYZkBa0RQ*!9AeY~hQllu70lpvBTbx&JiUytl=M`*Sfgs_ueLIG)2v-Ay8 zh&Qp#8ke#8kBmU-gd$*AM1|-y8;6>_jk3xz&>0$p)~I1woJgg}+$JH_G5@9!z*EEt zhXO;lz*EFyUuPxmpH3~7TFQrz-b3kJM&5~`G)S2#C)yPZ>euw0aEo$bEvJ3i~mGW(h-6|H5BKSOt9b4#kE<9^^frQQAMI(e%+WJ37VHObf8r7(1|fdMc^dl^KtYSE9gwta3*#qhyTNa7enk0z0*pPD7Mb_jAWN`#ruF;}if$y)H zG`n;)s2t^{Yb4Fb?`pG&A6QO1))0km@(UjlGXYoh#JrNk>)uQ|Fcym`&^a5};?TXE zbm(Lnsm|tH-%{E;o5rqjJ#3wfP?jIJmxR;wc%UP=YRk+Rnj(B9_Z4P^T6p<`;N=4r z^|V?u9n){%zKaKY5vJe{4+8%B>$;k+^F>Yooe>%9PqK+k{2Dd06UF>sup&%^2myF( zcR&57aPmc3t&}GrL{c_PT$LY6ol9X*bbi%w?EIsV+>UbfEcdi;kZ0=1Lqil%^pmNK z)~eq#ZNyLk-9)iM$3}AC11I^Jeyxm&TF44RBDa%`mT4R)TH({{^!~}0U(cuKawvvph{N`>{%)M? zBUVZ5JD}%=4GW4zJF(-_4%QG%51iOk&n}i#v~Ru#2B#qX(h`jUgz*rGRJM19CdVZr zp-nkw3xn7642Tym0bMYG9T9$=Q74EjatAN7iVl;ZOiU70?<-qL2#IcBS801SZGo+W z*k?CTF-5^u7hSD~h!m;fg!q;pe}(Fxs+vzIp=pUTg3WAHz8l1*pXdSLWHigfsQ49D zC%Dur93ZANlGVRlqB=t(5)Xy}WS5(#bpkdS?hKO!-%aS>$**7IE|}t8CrN%20SaO9 zQKQ!*%81B+o;3%_!>df1RVP~#^aQ`TQ`gZJJXE-tN1@;#(G9~RAYxi1V(LFPr|o^D z49`WAFmq0U_TK$YGeUlRigJwqN4(z0@kQ)Ixv z&pm>Q+Ka3yPo5CZqNl&{DISUxK(A^Z0){(oen2f1OfNhC5qEig6c6l%w_#!Lwzj1+ zMtutI=cvE!i{L;{z>6Cjw!)3Dq>r*Mf2qiPWA7>bW0^AOE5-Kv&SEX3Dm0LrzmE|# zHe}6BS5D~(q%U#4URy!1EjfYN(DIHzE|R0iZaJR5alt_RC*6iId9abzS9*WIz-+2e zbQ$E}0ac+Ohv?iX(=fU5DVry7tSyI&iCJ;`tWK6taf0iG=NTggcdMbk;;>I|zy^KXq8NtlS#QPC*gL4`uYugb}=sjv>cqP;$R|)8~(<7HV zmzVwbo)+abU2^H(PTi(UJ!|yJ+^|(4wIdz>+GPJwpE$(O#I$w zirWk3Y1}UYo~x(T?S*aLgHV|Q(w^0p+_s2G+>CF$%paOvZ}3VJ$R#(;W7~$}5!@no z$AV#w}?iZB)9(@w#AeVZ8-zA;WRY<#=256u%3kRJL zC8e#;at92zVQDezTf)83+-%GnbHu~g;RFJ>Qj2IfEYU`RWP^ZC$Ubub{5*xyqx9MP zAOY2Rl7%rt(hC(}2O&%*++cBvW|tvGE5=|?SV2rMvz_uq+aBfRs*WeXsflPyP=bK0 zlXWd=r?6g^@IOe7Lm0_ODnLRm$))qCt4A0gAGkY_=szRlqpB#ra)QuaxOzAse*m0L z?TGxi?px5lT`%qEzODv$WWTOZwyu8?Zfq8oDmp7?HY;3CTwc_o0%%t8bwr&x@+Y9$ zu@{a=bYxE*CDF{EBZ5#n1Dnb`xotI_-PVoZlo@7D>-lhn9$=O-+HDoP1_R(5^3&T< zgzD^zOfERTB@OxT1ywgqKj+2{2aP1ZBHOH?k4X7p-{hx$Ec^=~9_%t6ePVWm;8n%D%Xj~+N0mP5V@K$^$mFV}pOJD}0auR6Yb2-K-qiy_{pm1(F7J_42(0$~DVPynYT=HMe>$Qw zNNv%^kB1R7lgTRJL)SS7?z3f3?_*lRixqqZ9-i3U{W{8stk{U0^=$&5rm%%fYHLC9 zh)Ku(MEc)JQ*dR+vfkfh$@cH{H*fmSV(b5{8q~x^M5Li+#AT-DV5Dd$rzd9`6zLb4 zb{*xWq-Z3m$LQ)6#U;n6X~XHj%M@nmXPH=+m=+Hq$7kr~o~V~$$Y~^}#-!^N$tfsh zjvytZTNKGlndhe_W~7#6CM$x&(TRjx|7j=YKdPC4fs$VEzxIIsy@>v;UCws)de$bc zCf2mh?#@(WGgEs&|H1=ghA+1Dx0qc2TNz>hTOLIbAvr~1m-(^ke0~7_xvvk@TZW|5 zd^G%!2#9gz@#1{wh)G5N+o!MgXq<&`KKq0WZN=j?wqc2Q_$aa^ewGy^@Zz4rWVs8G z9);9h*O(9it@NadXB8*o*>a8A?xxFGldiD}e?*S&$WMoUa`J)G?_uq$qJk0TFyo;S z19e(eah&2|Wq7!OC?dM+596qa}Rea(1~dXb2D zJCzC4KjmaqhD2?sBMIpp{3>wG?sCuY72Gpom%aHL*HAw9NzHC4sG~~Wza@$LgG-wqwv<5 zgXVqTK${@JlpNJ61k6u>8Dl~PmJ2zInl4fkc?3!4XqXWs2zhkr@r@ zG?!&H>o!5o3Z)GrSaVw_qs?((L$dOKmQ3O2h#)~owUG>fs|7_XE|Ondv6Ghq1VRS> z_w(R?*T#Pyg#rI@``794e@6fR#-jfV3jk0U!1i~s{U`b#L(=~W{?}c`e}G8{{~P>| zJ;#3r|Npu={{Y`p|2Oy_J)VCC|A*QC+FJPsxSZy{!T;H4`Dfh!ueb0I+#cf2I1@D(xRs`wah%>VImsf93ku;PnqKG_HTg^`D{aUy1%T+Wdp4=HC+iuh{di mg#Q|L{)ey;@a5kU{#Ou^mjVU*kFO#Bo~3_-7A)_7JpCVP85NrV literal 0 HcmV?d00001 diff --git a/style/servo/animation.rs b/style/servo/animation.rs index fbf0ca0ff9..89e2b4ebc6 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -30,6 +30,7 @@ use crate::values::computed::TimingFunction; use crate::values::generics::easing::BeforeFlag; use crate::values::specified::TransitionBehavior; use crate::Atom; +use debug_unreachable::debug_unreachable; use parking_lot::RwLock; use rustc_hash::FxHashMap; use servo_arc::Arc; @@ -620,7 +621,9 @@ impl Animation { self.current_direction = match self.current_direction { AnimationDirection::Normal => AnimationDirection::Reverse, AnimationDirection::Reverse => AnimationDirection::Normal, - _ => unreachable!(), + _ => unreachable!( + "Current animation direction can only be `normal` or `reverse`." + ), }; }, _ => {}, @@ -694,57 +697,102 @@ impl Animation { // NB: We shall not touch the started_at field, since we don't want to // restart the animation. let old_started_at = self.started_at; + let old_delay = self.delay; let old_duration = self.duration; let old_direction = self.current_direction; let old_state = self.state.clone(); let old_iteration_state = self.iteration_state.clone(); *self = other.clone(); - - self.started_at = old_started_at; self.current_direction = old_direction; - // Don't update the iteration count, just the iteration limit. - // TODO: see how changing the limit affects rendering in other browsers. - // We might need to keep the iteration count even when it's infinite. - match (&mut self.iteration_state, old_iteration_state) { - ( - &mut KeyframesIterationState::Finite(ref mut iters, _), - KeyframesIterationState::Finite(old_iters, _), - ) => *iters = old_iters, - _ => {}, - } + if self.delay != old_delay { + // `started_at` incorporates the delay, so changing the delay necessarily changes `started_at`. + // Note: `started_at` may actually be in the future. + self.started_at = old_started_at + (self.delay - old_delay); + + match old_state { + Paused(old_progress) => { + let mut progress = old_progress + (old_delay - self.delay) / self.duration; + while progress > 1. && !self.on_last_iteration() { + self.iterate(); + progress -= 1.; + } + self.state = Paused(progress); + }, + Finished => { + if self.has_ended(now) { + self.state = Finished; + } else if self.started_at <= now { + self.state = Running; + } else { + self.state = Pending; + } + }, + _ => { + // Running or Pending — re-advance iterations from a fresh + // iteration state. + let mut starting_progress = (now - self.started_at) / self.duration; + match self.iteration_state { + KeyframesIterationState::Finite(ref mut current, _) => *current = 0.0, + _ => {}, + } + while starting_progress > 1. && !self.on_last_iteration() { + self.iterate(); + starting_progress -= 1.; + } + }, + } - // Don't pause or restart animations that should remain finished. - // We call mem::replace because `has_ended(...)` looks at `Animation::state`. - let new_state = std::mem::replace(&mut self.state, Running); - if old_state == Finished && self.has_ended(now) { - self.state = Finished; + // Don't check old_state when delay changed. + if self.state == Pending && self.started_at <= now { + self.state = Running; + } } else { - self.state = new_state; - } + self.started_at = old_started_at; + + // Don't update the iteration count, just the iteration limit. + // TODO: see how changing the limit affects rendering in other browsers. + // We might need to keep the iteration count even when it's infinite. + match (&mut self.iteration_state, old_iteration_state) { + ( + &mut KeyframesIterationState::Finite(ref mut iters, _), + KeyframesIterationState::Finite(old_iters, _), + ) => *iters = old_iters, + _ => {}, + } - // If we're unpausing the animation, fake the start time so we seem to - // restore it. - // - // If the animation keeps paused, keep the old value. - // - // If we're pausing the animation, compute the progress value. - match (&mut self.state, &old_state) { - (&mut Pending, &Paused(progress)) => { - self.started_at = now - (self.duration * progress); - }, - (&mut Paused(ref mut new), &Paused(old)) => *new = old, - (&mut Paused(ref mut progress), &Running) => { - *progress = (now - old_started_at) / old_duration - }, - _ => {}, - } + // Don't pause or restart animations that should remain finished. + // We call mem::replace because `has_ended(...)` looks at `Animation::state`. + let new_state = std::mem::replace(&mut self.state, Running); + if old_state == Finished && self.has_ended(now) { + self.state = Finished; + } else { + self.state = new_state; + } + + // If we're unpausing the animation, fake the start time so we seem to + // restore it. + // + // If the animation keeps paused, keep the old value. + // + // If we're pausing the animation, compute the progress value. + match (&mut self.state, &old_state) { + (&mut Pending, &Paused(progress)) => { + self.started_at = now - (self.duration * progress); + }, + (&mut Paused(ref mut new), &Paused(old)) => *new = old, + (&mut Paused(ref mut progress), &Running) => { + *progress = (now - old_started_at) / old_duration + }, + _ => {}, + } - // Try to detect when we should skip straight to the running phase to - // avoid sending multiple animationstart events. - if self.state == Pending && self.started_at <= now && old_state != Pending { - self.state = Running; + // Try to detect when we should skip straight to the running phase to + // avoid sending multiple animationstart events. + if self.state == Pending && self.started_at <= now && old_state != Pending { + self.state = Running; + } } } @@ -756,7 +804,9 @@ 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 or during multiple iterations). + let progress = match self.state { AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { (now - self.started_at) / self.duration }, @@ -764,7 +814,7 @@ impl Animation { AnimationState::Canceled => return, }; - if total_progress < 0. + if progress < 0. && self.fill_mode != AnimationFillMode::Backwards && self.fill_mode != AnimationFillMode::Both { @@ -776,9 +826,43 @@ 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. + let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { + for value_or_reference in keyframe.values.iter() { + let AnimationValueOrReference::AnimationValue(value) = value_or_reference else { + unreachable!("First or last keyframes define all properties"); + }; + map.insert(value.id().to_owned(), value.clone()); + } + }; + + // Handle negative progress (before animation start) with backwards/both fill mode + if progress < 0.0 { + if let Some(keyframe) = match self.current_direction { + AnimationDirection::Normal => self.computed_steps.first(), + AnimationDirection::Reverse => self.computed_steps.last(), + _ => unreachable!("Current animation direction can only be `normal` or `reverse`."), + } { + add_declarations_to_map(keyframe); + } + return; + } + + // Progress clamped to the current iteration [0.0, 1.0]. + let total_progress = progress.min(self.current_iteration_end_progress()).max(0.0); + + // At 1.0 there is nothing left to interpolate. Return end keyframe. + if total_progress == 1.0 { + let keyframe = match self.current_direction { + AnimationDirection::Normal => self.computed_steps.last().unwrap(), + AnimationDirection::Reverse => self.computed_steps.first().unwrap(), + _ => unreachable!("Current animation direction can only be `normal` or `reverse`."), + }; + add_declarations_to_map(keyframe); + return; + } // Get the indices of the previous (from) keyframe and the next (to) keyframe. let next_keyframe_index; @@ -789,7 +873,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); @@ -819,41 +903,27 @@ impl Animation { prev_keyframe_index, next_keyframe_index ); + let prev_keyframe = &self.computed_steps[prev_keyframe_index]; let Some(next_keyframe_index) = next_keyframe_index else { - 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_index: usize| { - for value_or_reference in &self.computed_steps[keyframe_index].values { - let AnimationValueOrReference::AnimationValue(value) = value_or_reference else { - unreachable!("First or last keyframes define all properties"); - }; - - map.insert(value.id().to_owned(), value.clone()); + unsafe { + debug_unreachable!( + "next_keyframe_index should always be Some: \ + total_progress is in [0, 1) at this point. \ + Normal direction: keyframe with start_percentage 1.0 always satisfies. \ + Reverse direction: keyframe with start_percentage 0.0 always satisfies." + ); } }; - let reversed = self.current_direction != AnimationDirection::Normal; - if total_progress <= 0.0 { - if reversed { - add_declarations_to_map(self.computed_steps.len() - 1); - } else { - add_declarations_to_map(0); - } - return; - } - if total_progress >= 1.0 { - if reversed { - add_declarations_to_map(0); - } else { - add_declarations_to_map(self.computed_steps.len() - 1); - } + // Prevent division by zero from percentage_between_keyframes. + // This can happen for reverse direction at total_progress == 0.0. + if prev_keyframe_index == next_keyframe_index { + add_declarations_to_map(&prev_keyframe); return; } // Interpolate a new value for each animating property + let reversed = self.current_direction != AnimationDirection::Normal; for property_index in 0..self.number_of_animating_properties { let Some(previous_keyframe) = self.next_relevant_keyframe_for_property_in_direction( property_index, diff --git a/style/servo/attr.rs b/style/servo/attr.rs index 2a6793b5c5..ffae7b8ade 100644 --- a/style/servo/attr.rs +++ b/style/servo/attr.rs @@ -6,24 +6,29 @@ //! //! [attr]: https://dom.spec.whatwg.org/#interface-attr +use std::str::FromStr; +use std::sync::OnceLock; + +use app_units::Au; +use euclid::num::Zero; +use num_traits::ToPrimitive; +use selectors::attr::AttrSelectorOperation; +use servo_arc::Arc; + use super::shadow_parts::ShadowParts; -use crate::color::{parsing::parse_color_keyword, AbsoluteColor}; +use crate::color::parsing::parse_color_keyword; +use crate::color::AbsoluteColor; use crate::derives::*; use crate::properties::PropertyDeclarationBlock; -use crate::shared_lock::Locked; -use crate::str::str_join; -use crate::str::{read_exponent, read_fraction, HTML_SPACE_CHARACTERS}; -use crate::str::{read_numbers, split_commas, split_html_space_chars}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::str::{ + read_exponent, read_fraction, read_numbers, split_commas, split_html_space_chars, str_join, + HTML_SPACE_CHARACTERS, +}; use crate::values::specified::color::Color; use crate::values::specified::LengthPercentage; use crate::values::AtomString; use crate::{Atom, LocalName, Namespace, Prefix}; -use app_units::Au; -use euclid::num::Zero; -use num_traits::ToPrimitive; -use selectors::attr::AttrSelectorOperation; -use servo_arc::Arc; -use std::str::FromStr; // Duplicated from script::dom::values. const UNSIGNED_LONG_MAX: u32 = 2147483647; @@ -39,16 +44,36 @@ pub enum LengthOrPercentageOrAuto { #[derive(Clone, Debug)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] pub enum AttrValue { + // + // Variants that are stored in their serialized form. + // String(String), - TokenList(String, Vec), - UInt(String, u32), - Int(String, i32), - Double(String, f64), Atom(Atom), + + // + // Variants that support lazy serialization. + // + TokenList(OnceLock, Vec), + UInt(OnceLock, u32), + Int(OnceLock, i32), + Double(OnceLock, f64), + /// Note that this variant is only used transitively as a fast path to set + /// the property declaration block relevant to the style of an element when + /// set from the inline declaration of that element (that is, + /// `element.style`). + Declaration { + #[ignore_malloc_size_of = "Arc"] + block: Arc>, + lock: SharedRwLock, + serialization: OnceLock, + }, + + // + // Variants without a serialization implementation which must be eagerly serialized. + // LengthPercentage(String, Option), Color(String, Option), Dimension(String, LengthOrPercentageOrAuto), - /// Stores a URL, computed from the input string and a document's base URL. /// /// The URL is resolved at setting-time, so this kind of attribute value is @@ -57,28 +82,40 @@ pub enum AttrValue { String, #[ignore_malloc_size_of = "Arc"] Option>, ), - - /// Note that this variant is only used transitively as a fast path to set - /// the property declaration block relevant to the style of an element when - /// set from the inline declaration of that element (that is, - /// `element.style`). - /// - /// This can, as of this writing, only correspond to the value of the - /// `style` element, and is set from its relevant CSSInlineStyleDeclaration, - /// and then converted to a string in Element::attribute_mutated. - /// - /// Note that we don't necessarily need to do that (we could just clone the - /// declaration block), but that avoids keeping a refcounted - /// declarationblock for longer than needed. - Declaration( - String, - #[ignore_malloc_size_of = "Arc"] Arc>, - ), - /// The value of an `exportparts` attribute. ShadowParts(String, ShadowParts), } +impl From for AttrValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for AttrValue { + fn from(value: u32) -> Self { + Self::UInt(OnceLock::new(), value) + } +} + +impl From for AttrValue { + fn from(value: i32) -> Self { + Self::Int(OnceLock::new(), value) + } +} + +impl From for AttrValue { + fn from(value: f64) -> Self { + Self::Double(OnceLock::new(), value) + } +} + +impl From> for AttrValue { + fn from(value: Vec) -> Self { + Self::TokenList(OnceLock::new(), value) + } +} + /// Shared implementation to parse an integer according to /// or /// @@ -176,7 +213,7 @@ impl AttrValue { } acc }); - AttrValue::TokenList(tokens, atoms) + AttrValue::TokenList(tokens.into(), atoms) } pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue { @@ -188,13 +225,7 @@ impl AttrValue { } acc }); - AttrValue::TokenList(tokens, atoms) - } - - pub fn from_atomic_tokens(atoms: Vec) -> AttrValue { - // TODO(ajeffrey): effecient conversion of Vec to String - let tokens = String::from(str_join(&atoms, "\x20")); - AttrValue::TokenList(tokens, atoms) + AttrValue::TokenList(tokens.into(), atoms) } // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long @@ -205,12 +236,12 @@ impl AttrValue { } else { result }; - AttrValue::UInt(string, result) + AttrValue::UInt(string.into(), result) } pub fn from_i32(string: String, default: i32) -> AttrValue { let result = parse_integer(string.chars()).unwrap_or(default); - AttrValue::Int(string, result) + AttrValue::Int(string.into(), result) } // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double @@ -218,9 +249,9 @@ impl AttrValue { let result = parse_double(&string).unwrap_or(default); if result.is_normal() { - AttrValue::Double(string, result) + AttrValue::Double(string.into(), result) } else { - AttrValue::Double(string, default) + AttrValue::Double(string.into(), default) } } @@ -229,9 +260,9 @@ impl AttrValue { let result = parse_integer(string.chars()).unwrap_or(default); if result < 0 { - AttrValue::Int(string, default) + AttrValue::Int(string.into(), default) } else { - AttrValue::Int(string, result) + AttrValue::Int(string.into(), result) } } @@ -243,12 +274,11 @@ impl AttrValue { } else { result }; - AttrValue::UInt(string, result) + AttrValue::UInt(string.into(), result) } pub fn from_atomic(string: String) -> AttrValue { - let value = Atom::from(string); - AttrValue::Atom(value) + AttrValue::Atom(string.into()) } pub fn from_resolved_url(base: &Arc<::url::Url>, url: String) -> AttrValue { @@ -276,6 +306,17 @@ impl AttrValue { AttrValue::ShadowParts(string, shadow_parts) } + pub fn from_declaration( + block: Arc>, + lock: SharedRwLock, + ) -> AttrValue { + AttrValue::Declaration { + block, + lock, + serialization: OnceLock::new(), + } + } + /// Assumes the `AttrValue` is a `TokenList` and returns its tokens /// /// ## Panics @@ -423,19 +464,39 @@ impl ::std::ops::Deref for AttrValue { type Target = str; fn deref(&self) -> &str { - match *self { - AttrValue::String(ref value) - | AttrValue::TokenList(ref value, _) - | AttrValue::UInt(ref value, _) - | AttrValue::Double(ref value, _) - | AttrValue::LengthPercentage(ref value, _) - | AttrValue::Color(ref value, _) - | AttrValue::Int(ref value, _) - | AttrValue::ResolvedUrl(ref value, _) - | AttrValue::Declaration(ref value, _) - | AttrValue::ShadowParts(ref value, _) - | AttrValue::Dimension(ref value, _) => &value, - AttrValue::Atom(ref value) => &value, + match self { + AttrValue::String(value) => &value, + AttrValue::Atom(atom) => &atom, + AttrValue::TokenList(serialization, tokens) => { + serialization.get_or_init(|| { + // TODO(ajeffrey): Efficient conversion of Vec to String + str_join(tokens, "\x20") + }) + }, + AttrValue::UInt(serialization, value) => { + serialization.get_or_init(|| value.to_string()) + }, + AttrValue::Double(serialization, value) => { + serialization.get_or_init(|| value.to_string()) + }, + AttrValue::Int(serialization, value) => serialization.get_or_init(|| value.to_string()), + AttrValue::Declaration { + block, + lock, + serialization, + } => serialization.get_or_init(|| { + let mut serialization = String::new(); + block + .read_with(&lock.read()) + .to_css(&mut serialization) + .expect("Should always be able to produce a valid serialization"); + serialization + }), + AttrValue::LengthPercentage(serialization, _) + | AttrValue::Color(serialization, _) + | AttrValue::Dimension(serialization, _) + | AttrValue::ResolvedUrl(serialization, _) + | AttrValue::ShadowParts(serialization, _) => &serialization, } } } diff --git a/style/shared_lock.rs b/style/shared_lock.rs index 397509a025..fe2e602dee 100644 --- a/style/shared_lock.rs +++ b/style/shared_lock.rs @@ -5,46 +5,33 @@ //! Different objects protected by the same lock use crate::stylesheets::Origin; -#[cfg(feature = "gecko")] use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; -#[cfg(feature = "servo")] -use parking_lot::RwLock; use servo_arc::Arc; use std::cell::UnsafeCell; use std::fmt; -#[cfg(feature = "servo")] -use std::mem; -#[cfg(feature = "gecko")] use std::ptr; use style_traits::{CssString, CssStringWriter}; use to_shmem::{SharedMemoryBuilder, ToShmem}; /// A shared read/write lock that can protect multiple objects. /// -/// In Gecko builds, we don't need the blocking behavior, just the safety. As -/// such we implement this with an AtomicRefCell instead in Gecko builds, -/// which is ~2x as fast, and panics (rather than deadlocking) when things go -/// wrong (which is much easier to debug on CI). -/// -/// Servo needs the blocking behavior for its unsynchronized animation setup, -/// but that may not be web-compatible and may need to be changed (at which -/// point Servo could use AtomicRefCell too). +/// We don't need the blocking behavior, just the safety. As such we implement +/// this with an AtomicRefCell, which is ~2x as fast as an RwLock, and panics +/// (rather than deadlocking) when things go wrong (which is much easier to +/// debug on CI). /// /// Gecko also needs the ability to have "read only" SharedRwLocks, which are /// used for objects stored in (read only) shared memory. Attempting to acquire /// write access to objects protected by a read only SharedRwLock will panic. #[derive(Clone)] -#[cfg_attr(feature = "servo", derive(crate::derives::MallocSizeOf))] pub struct SharedRwLock { - #[cfg(feature = "servo")] - #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] - arc: Arc>, - - #[cfg(feature = "gecko")] cell: Option>>, } -#[cfg(feature = "gecko")] +#[cfg(feature = "servo")] +malloc_size_of::malloc_size_of_is_0!(SharedRwLock); + +#[cfg_attr(feature = "servo", derive(crate::derives::MallocSizeOf))] struct SomethingZeroSizedButTyped; impl fmt::Debug for SharedRwLock { @@ -54,32 +41,14 @@ impl fmt::Debug for SharedRwLock { } impl SharedRwLock { - /// Create a new shared lock (servo). - #[cfg(feature = "servo")] - pub fn new() -> Self { - SharedRwLock { - arc: Arc::new(RwLock::new(())), - } - } - - /// Create a new shared lock (gecko). - #[cfg(feature = "gecko")] + /// Create a new shared lock. pub fn new() -> Self { SharedRwLock { cell: Some(Arc::new(AtomicRefCell::new(SomethingZeroSizedButTyped))), } } - /// Create a new global shared lock (servo). - #[cfg(feature = "servo")] - pub fn new_leaked() -> Self { - SharedRwLock { - arc: Arc::new_leaked(RwLock::new(())), - } - } - - /// Create a new global shared lock (gecko). - #[cfg(feature = "gecko")] + /// Create a new global shared lock. pub fn new_leaked() -> Self { SharedRwLock { cell: Some(Arc::new_leaked(AtomicRefCell::new( @@ -88,13 +57,11 @@ impl SharedRwLock { } } - /// Create a new read-only shared lock (gecko). - #[cfg(feature = "gecko")] + /// Create a new read-only shared lock. pub fn read_only() -> Self { SharedRwLock { cell: None } } - #[cfg(feature = "gecko")] #[inline] fn ptr(&self) -> *const SomethingZeroSizedButTyped { self.cell @@ -111,51 +78,22 @@ impl SharedRwLock { } } - /// Obtain the lock for reading (servo). - #[cfg(feature = "servo")] - pub fn read(&self) -> SharedRwLockReadGuard<'_> { - mem::forget(self.arc.read()); - SharedRwLockReadGuard(self) - } - - /// Obtain the lock for reading (gecko). - #[cfg(feature = "gecko")] + /// Obtain the lock for reading. pub fn read(&self) -> SharedRwLockReadGuard<'_> { SharedRwLockReadGuard(self.cell.as_ref().map(|cell| cell.borrow())) } - /// Obtain the lock for writing (servo). - #[cfg(feature = "servo")] - pub fn write(&self) -> SharedRwLockWriteGuard<'_> { - mem::forget(self.arc.write()); - SharedRwLockWriteGuard(self) - } - - /// Obtain the lock for writing (gecko). - #[cfg(feature = "gecko")] + /// Obtain the lock for writing. pub fn write(&self) -> SharedRwLockWriteGuard<'_> { SharedRwLockWriteGuard(self.cell.as_ref().unwrap().borrow_mut()) } } -/// Proof that a shared lock was obtained for reading (servo). -#[cfg(feature = "servo")] -pub struct SharedRwLockReadGuard<'a>(&'a SharedRwLock); -/// Proof that a shared lock was obtained for reading (gecko). -#[cfg(feature = "gecko")] +/// Proof that a shared lock was obtained for reading. pub struct SharedRwLockReadGuard<'a>(Option>); -#[cfg(feature = "servo")] -impl<'a> Drop for SharedRwLockReadGuard<'a> { - fn drop(&mut self) { - // Unsafe: self.lock is private to this module, only ever set after `read()`, - // and never copied or cloned (see `compile_time_assert` below). - unsafe { self.0.arc.force_unlock_read() } - } -} impl<'a> SharedRwLockReadGuard<'a> { #[inline] - #[cfg(feature = "gecko")] fn ptr(&self) -> *const SomethingZeroSizedButTyped { self.0 .as_ref() @@ -164,20 +102,8 @@ impl<'a> SharedRwLockReadGuard<'a> { } } -/// Proof that a shared lock was obtained for writing (servo). -#[cfg(feature = "servo")] -pub struct SharedRwLockWriteGuard<'a>(&'a SharedRwLock); -/// Proof that a shared lock was obtained for writing (gecko). -#[cfg(feature = "gecko")] +/// Proof that a shared lock was obtained for writing. pub struct SharedRwLockWriteGuard<'a>(AtomicRefMut<'a, SomethingZeroSizedButTyped>); -#[cfg(feature = "servo")] -impl<'a> Drop for SharedRwLockWriteGuard<'a> { - fn drop(&mut self) { - // Unsafe: self.lock is private to this module, only ever set after `write()`, - // and never copied or cloned (see `compile_time_assert` below). - unsafe { self.0.arc.force_unlock_write() } - } -} /// Data protect by a shared lock. pub struct Locked { @@ -198,33 +124,23 @@ impl fmt::Debug for Locked { } impl Locked { - #[cfg(feature = "gecko")] #[inline] fn is_read_only_lock(&self) -> bool { self.shared_lock.cell.is_none() } - #[cfg(feature = "servo")] - fn same_lock_as(&self, lock: &SharedRwLock) -> bool { - Arc::ptr_eq(&self.shared_lock.arc, &lock.arc) - } - - #[cfg(feature = "gecko")] fn same_lock_as(&self, ptr: *const SomethingZeroSizedButTyped) -> bool { ptr::eq(self.shared_lock.ptr(), ptr) } /// Access the data for reading. pub fn read_with<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a T { - #[cfg(feature = "gecko")] assert!( self.is_read_only_lock() || self.same_lock_as(guard.ptr()), "Locked::read_with called with a guard from an unrelated SharedRwLock: {:?} vs. {:?}", self.shared_lock.ptr(), guard.ptr(), ); - #[cfg(not(feature = "gecko"))] - assert!(self.same_lock_as(&guard.0)); let ptr = self.data.get(); @@ -245,13 +161,10 @@ impl Locked { /// Access the data for writing. pub fn write_with<'a>(&'a self, guard: &'a mut SharedRwLockWriteGuard) -> &'a mut T { - #[cfg(feature = "gecko")] assert!( !self.is_read_only_lock() && self.same_lock_as(&*guard.0), "Locked::write_with called with a guard from a read only or unrelated SharedRwLock" ); - #[cfg(not(feature = "gecko"))] - assert!(self.same_lock_as(&guard.0)); let ptr = self.data.get(); @@ -267,7 +180,6 @@ impl Locked { } } -#[cfg(feature = "gecko")] impl ToShmem for Locked { fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> to_shmem::Result { use std::mem::ManuallyDrop; @@ -282,13 +194,6 @@ impl ToShmem for Locked { } } -#[cfg(feature = "servo")] -impl ToShmem for Locked { - fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result { - panic!("ToShmem not supported in Servo currently") - } -} - #[allow(dead_code)] mod compile_time_assert { use super::{SharedRwLockReadGuard, SharedRwLockWriteGuard}; diff --git a/style/values/generics/font.rs b/style/values/generics/font.rs index 3ad75de64e..832b0c7375 100644 --- a/style/values/generics/font.rs +++ b/style/values/generics/font.rs @@ -36,6 +36,7 @@ pub trait TaggedFontValue { ToResolvedValue, ToShmem, )] +#[cfg_attr(feature = "servo", derive(Deserialize, Hash, Serialize))] pub struct FeatureTagValue { /// A four-character tag, packed into a u32 (one byte per character). pub tag: FontTag, @@ -115,7 +116,7 @@ impl TaggedFontValue for VariationValue { ToShmem, ToTyped, )] -#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "servo", derive(Deserialize, Hash, Serialize))] #[css(comma)] #[typed(todo_derive_fields)] pub struct FontSettings(#[css(if_empty = "normal", iterable)] pub Box<[T]>); @@ -159,7 +160,6 @@ impl Parse for FontSettings { #[derive( Clone, Copy, - Debug, Eq, MallocSizeOf, PartialEq, @@ -169,9 +169,23 @@ impl Parse for FontSettings { ToResolvedValue, ToShmem, )] -#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "servo", derive(Deserialize, Hash, Serialize))] pub struct FontTag(pub u32); +impl fmt::Debug for FontTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let tag_bytes = self.0.to_be_bytes(); + + let mut tuple = f.debug_tuple("FontTag"); + if let Ok(utf8_tag) = str::from_utf8(&tag_bytes) { + tuple.field(&utf8_tag); + } else { + tuple.field(&tag_bytes); + }; + tuple.finish() + } +} + impl ToCss for FontTag { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where diff --git a/style/values/specified/font.rs b/style/values/specified/font.rs index 986fdd25bc..a0d22ebe2f 100644 --- a/style/values/specified/font.rs +++ b/style/values/specified/font.rs @@ -1318,6 +1318,7 @@ impl Parse for FontVariantAlternates { ToShmem, ToTyped, )] +#[cfg_attr(feature = "servo", derive(Deserialize, Hash, Serialize))] #[css(bitflags( single = "normal", mixed = "jis78,jis83,jis90,jis04,simplified,traditional,full-width,proportional-width,ruby", @@ -1387,6 +1388,7 @@ impl FontVariantEastAsian { ToShmem, ToTyped, )] +#[cfg_attr(feature = "servo", derive(Deserialize, Hash, Serialize))] #[css(bitflags( single = "normal,none", mixed = "common-ligatures,no-common-ligatures,discretionary-ligatures,no-discretionary-ligatures,historical-ligatures,no-historical-ligatures,contextual,no-contextual", @@ -1458,6 +1460,7 @@ impl FontVariantLigatures { mixed = "lining-nums,oldstyle-nums,proportional-nums,tabular-nums,diagonal-fractions,stacked-fractions,ordinal,slashed-zero", validate_mixed = "Self::validate_mixed_flags", ))] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize, Hash))] #[repr(C)] pub struct FontVariantNumeric(u8); bitflags! { 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 73ddfa72d3..18d065433e 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.37" 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..f9477fc92a --- /dev/null +++ b/stylo_atoms/static_atoms.txt @@ -0,0 +1,189 @@ +-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 +all +animationcancel +animationend +animationiteration +animationstart +aspect-ratio +beforeinput +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 +match-element +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..3c6ef6804c --- /dev/null +++ b/stylo_static_prefs/Cargo.toml @@ -0,0 +1,15 @@ +[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" +build = "build.rs" + +[build-dependencies] +toml = { version = "1.1.2" } diff --git a/stylo_static_prefs/build.rs b/stylo_static_prefs/build.rs new file mode 100644 index 0000000000..b1b0dfcc8e --- /dev/null +++ b/stylo_static_prefs/build.rs @@ -0,0 +1,131 @@ +/* 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, fs, path::Path}; + +use toml::{Table, Value}; + +struct BooleanPreference { + name: String, + default: bool, +} + +struct IntegerPreference { + name: String, + default: i64, +} + +fn main() -> Result<(), std::io::Error> { + println!("cargo::rerun-if-changed=preferences.toml"); + generate_code(parse_preferences()?) +} + +fn parse_preferences() -> Result<(Vec, Vec), std::io::Error> { + let preferences_text = fs::read_to_string("preferences.toml")?; + let toml = preferences_text + .parse::() + .expect("Could not parse preferences.toml"); + + let mut boolean_preferences = Vec::new(); + let mut integer_preferences = Vec::new(); + for (name, default) in toml { + match default { + Value::Boolean(default) => { + boolean_preferences.push(BooleanPreference { name, default }); + }, + Value::Integer(default) => { + integer_preferences.push(IntegerPreference { name, default }); + }, + _ => panic!("Found unknown preference type: {default:?}"), + } + } + + Ok((boolean_preferences, integer_preferences)) +} + +fn generate_code( + preferences: (Vec, Vec), +) -> Result<(), std::io::Error> { + let mut output = Vec::new(); + + let (boolean_preferences, integer_preferences) = preferences; + let boolean_count = boolean_preferences.len(); + output.push(format!( + "pub static BOOLS: [AtomicBool; {boolean_count}] = [" + )); + for preference in &boolean_preferences { + output.push(format!(" AtomicBool::new({}),", preference.default)); + } + output.push(format!("];")); + + let integer_count = integer_preferences.len(); + output.push(format!( + "pub static INTEGERS: [AtomicI32; {integer_count}] = [" + )); + for preference in &integer_preferences { + output.push(format!(" AtomicI32::new({}),", preference.default)); + } + output.push(format!("];")); + + output.push(format!( + "/// Returns the value of a preference exposed to the style crate. If the embedder" + )); + output.push(format!( + "/// has not set a value for it, this returns the default value of the preference" + )); + output.push(format!("#[macro_export]")); + output.push(format!("macro_rules! pref {{")); + + for (index, preference) in boolean_preferences.iter().enumerate() { + output.push(format!(" ({:?}) => {{", preference.name)); + output.push(format!( + " $crate::BOOLS[{index:?}].load(std::sync::atomic::Ordering::Relaxed)", + )); + output.push(format!(" }};")); + } + + for (index, preference) in integer_preferences.iter().enumerate() { + output.push(format!(" ({:?}) => {{", preference.name)); + output.push(format!( + " $crate::INTEGERS[{index:?}].load(std::sync::atomic::Ordering::Relaxed)", + )); + output.push(format!(" }};")); + } + output.push(format!("}}")); + + output.push(format!("#[macro_export]")); + output.push(format!("macro_rules! set_pref {{")); + + for (index, preference) in boolean_preferences.iter().enumerate() { + output.push(format!( + " ({:?}, $($value:expr)+) => {{", + preference.name + )); + + output.push(format!("let value = $($value)+;")); + output.push(format!( + " $crate::BOOLS[{index:?}].store(value, std::sync::atomic::Ordering::Relaxed)", + )); + output.push(format!(" }};")); + } + + for (index, preference) in integer_preferences.iter().enumerate() { + output.push(format!( + " ({:?}, $($value:expr)+) => {{", + preference.name + )); + output.push(format!("let value = $($value)+;")); + output.push(format!( + " $crate::INTEGERS[{index:?}].store(value, std::sync::atomic::Ordering::Relaxed)", + )); + output.push(format!(" }};")); + } + output.push(format!("}}")); + + let output = output.join("\n"); + println!("{output}"); + + let out_dir = env::var_os("OUT_DIR").expect("Should always have OUT_DIR set"); + fs::write(Path::new(&out_dir).join("generated.rs"), output) +} diff --git a/stylo_static_prefs/preferences.toml b/stylo_static_prefs/preferences.toml new file mode 100644 index 0000000000..e95170d306 --- /dev/null +++ b/stylo_static_prefs/preferences.toml @@ -0,0 +1,44 @@ +"dom.select.customizable_select.enabled" = false +"dom.viewTransitions.cross-document.enabled" = false +"layout.columns.enabled" = false +"layout.container-queries.enabled" = false +"layout.css.anchor-positioning.enabled" = false +"layout.css.appearance-base.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" = true +"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.marker.restricted" = true +"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.revert-rule.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 +# Negative means auto, 0 disables the thread-pool (main-thread styling), +# other numbers override as specified. +"layout.threads" = -1 +"layout.unimplemented" = false +"layout.variable_fonts.enabled" = false +"layout.writing-mode.enabled" = false diff --git a/stylo_static_prefs/src/lib.rs b/stylo_static_prefs/src/lib.rs new file mode 100644 index 0000000000..7b176c9749 --- /dev/null +++ b/stylo_static_prefs/src/lib.rs @@ -0,0 +1,18 @@ +/* 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::sync::atomic::{AtomicBool, AtomicI32}; + +include!(concat!(env!("OUT_DIR"), "/generated.rs")); + +#[test] +fn test_basic_preferences() { + assert!(!pref!("layout.unimplemented")); + set_pref!("layout.unimplemented", true); + assert!(pref!("layout.unimplemented")); + + assert_eq!(pref!("layout.threads"), -1); + set_pref!("layout.threads", 42); + assert_eq!(pref!("layout.threads"), 42); +} 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 fdc04b8970..1f0aa8a5df 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.4.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.37", 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 3422488697754d361b9e79304eb5eb2104f559f9 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 4 Jun 2026 02:32:28 +0200 Subject: [PATCH 03/29] Fixup for https://phabricator.services.mozilla.com/D299242 Signed-off-by: Oriol Brufau --- style/stylist.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/style/stylist.rs b/style/stylist.rs index 10f035367a..30bd8d1f0f 100644 --- a/style/stylist.rs +++ b/style/stylist.rs @@ -13,7 +13,9 @@ use crate::custom_properties::ComputedCustomProperties; use crate::custom_properties::{parse_name, SpecifiedValue}; use crate::derives::*; use crate::device::Device; -use crate::dom::{TElement, TShadowRoot}; +use crate::dom::TElement; +#[cfg(feature = "gecko")] +use crate::dom::TShadowRoot; #[cfg(feature = "gecko")] use crate::gecko_bindings::structs::{ServoStyleSetSizes, StyleRuleInclusion}; use crate::invalidation::element::invalidation_map::{ From 1c7754cb517a6a077d52aacb41851eaf640629c3 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 4 Jun 2026 02:36:30 +0200 Subject: [PATCH 04/29] Fixup for https://phabricator.services.mozilla.com/D296480 Signed-off-by: Oriol Brufau --- 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 89e2b4ebc6..402af9efd2 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -161,7 +161,7 @@ impl IntermediateComputedKeyframe { let mut intermediate_steps: Vec = Vec::with_capacity(animation.steps.len()); let mut current_step = IntermediateComputedKeyframe::new(0.); for step in animation.steps.iter() { - let start_percentage = step.start_percentage.0; + let start_percentage = step.start_offset.percentage.0; if start_percentage != current_step.start_percentage { let new_step = IntermediateComputedKeyframe::new(start_percentage); intermediate_steps.push(std::mem::replace(&mut current_step, new_step)); From 7d539163c00f9a58941d4e930a65f19498a91a2c Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 5 Jun 2026 16:55:27 +0200 Subject: [PATCH 05/29] Fixup for https://phabricator.services.mozilla.com/D286140 Signed-off-by: Oriol Brufau --- stylo_dom/lib.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/stylo_dom/lib.rs b/stylo_dom/lib.rs index 98a0330cf4..5a968cd52d 100644 --- a/stylo_dom/lib.rs +++ b/stylo_dom/lib.rs @@ -2,12 +2,13 @@ * 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/. */ +//! DOM types to be shared between Rust and C++. + use bitflags::bitflags; use malloc_size_of::malloc_size_of_is_0; -pub const HEADING_LEVEL_OFFSET: usize = 52; +pub const HEADING_LEVEL_OFFSET: usize = 57; -// DOM types to be shared between Rust and C++. bitflags! { /// Event-based element states. #[repr(C)] @@ -143,10 +144,24 @@ bitflags! { const ACTIVE_VIEW_TRANSITION = 1u64 << 50; /// For :-moz-suppress-for-print-selection. const SUPPRESS_FOR_PRINT_SELECTION = 1u64 << 51; + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-paused + const PAUSED = 1u64 << 52; + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-seeking + const SEEKING = 1u64 << 53; + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-buffering + const BUFFERING = 1u64 << 54; + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-stalled + const STALLED = 1u64 << 55; + /// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-muted + const MUTED = 1u64 << 56; + /// This element is fullscreen and was requested to have keyboard lock. + const FULLSCREEN_KEYBOARD_LOCK = 1u64 << 57; /// 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; + /// https://w3c.github.io/picture-in-picture/#css-pseudo-class + const PICTURE_IN_PICTURE = 1u64 << 61; /// Some convenience unions. const DIR_STATES = Self::LTR.bits() | Self::RTL.bits(); From 369ba1b59f85ac543491f273d0c085815e3bb21f Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 5 Jun 2026 18:08:12 +0200 Subject: [PATCH 06/29] Fixup for https://phabricator.services.mozilla.com/D301762 Signed-off-by: Oriol Brufau --- style/values/specified/box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/values/specified/box.rs b/style/values/specified/box.rs index a43d8c2a22..9947a57a80 100644 --- a/style/values/specified/box.rs +++ b/style/values/specified/box.rs @@ -557,7 +557,7 @@ impl ToTyped for Display { } let keyword = self.to_css_cssstring(); - debug_assert!(!keyword.as_ref().contains(&b' ')); + debug_assert!(!AsRef::<[u8]>::as_ref(&keyword).contains(&b' ')); dest.push(TypedValue::Keyword(KeywordValue(keyword))); return Ok(()); From b06117036100fc7df605e306bdabc9ebada5c496 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 5 Jun 2026 21:55:16 +0200 Subject: [PATCH 07/29] Fixup for https://phabricator.services.mozilla.com/D296935 Signed-off-by: Oriol Brufau --- style/properties/longhands.toml | 8 ++++++++ style/properties/shorthands.toml | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/style/properties/longhands.toml b/style/properties/longhands.toml index 58ce039323..bd8f336d6f 100644 --- a/style/properties/longhands.toml +++ b/style/properties/longhands.toml @@ -3147,6 +3147,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-top-left-shape" gecko_ffi_name = "mCornerShape.top_left" logical_group = "corner-shape" @@ -3158,6 +3159,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-top-right-shape" gecko_ffi_name = "mCornerShape.top_right" logical_group = "corner-shape" @@ -3169,6 +3171,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-bottom-right-shape" gecko_ffi_name = "mCornerShape.bottom_right" logical_group = "corner-shape" @@ -3180,6 +3183,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-bottom-left-shape" gecko_ffi_name = "mCornerShape.bottom_left" logical_group = "corner-shape" @@ -3191,6 +3195,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-start-start-shape" gecko_ffi_name = "mCornerShape.start_start" logical_group = "corner-shape" @@ -3203,6 +3208,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-start-end-shape" gecko_ffi_name = "mCornerShape.start_end" logical_group = "corner-shape" @@ -3215,6 +3221,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-end-start-shape" gecko_ffi_name = "mCornerShape.end_start" logical_group = "corner-shape" @@ -3227,6 +3234,7 @@ initial = "computed::CornerShape::round()" initial_specified_value = "specified::CornerShape::round()" struct = "border" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-end-end-shape" gecko_ffi_name = "mCornerShape.end_end" logical_group = "corner-shape" diff --git a/style/properties/shorthands.toml b/style/properties/shorthands.toml index 7992b994a5..529b60a3ad 100644 --- a/style/properties/shorthands.toml +++ b/style/properties/shorthands.toml @@ -412,53 +412,62 @@ extra_prefixes = ["webkit"] sub_properties = ["corner-top-left-shape", "corner-top-right-shape", "corner-bottom-right-shape", "corner-bottom-left-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" [corner-top-shape] sub_properties = ["corner-top-left-shape", "corner-top-right-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-top-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-right-shape] sub_properties = ["corner-top-right-shape", "corner-bottom-right-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-right-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-bottom-shape] sub_properties = ["corner-bottom-left-shape", "corner-bottom-right-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-bottom-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-left-shape] sub_properties = ["corner-top-left-shape", "corner-bottom-left-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-left-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-block-start-shape] sub_properties = ["corner-start-start-shape", "corner-start-end-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-block-start-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-block-end-shape] sub_properties = ["corner-end-start-shape", "corner-end-end-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-block-end-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-inline-start-shape] sub_properties = ["corner-start-start-shape", "corner-end-start-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-inline-start-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [corner-inline-end-shape] sub_properties = ["corner-start-end-shape", "corner-end-end-shape"] spec = "https://drafts.csswg.org/css-borders-4/#propdef-corner-inline-end-shape" gecko_pref = "layout.css.corner-shape.enabled" +servo_pref = "layout.unimplemented" kind = "two_properties" [outline] From 12eb772d507deba864e7287098a08972f6bfe61b Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 5 Jun 2026 23:29:58 +0200 Subject: [PATCH 08/29] Fixup for https://phabricator.services.mozilla.com/D292136 Signed-off-by: Oriol Brufau --- style/values/specified/box.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/style/values/specified/box.rs b/style/values/specified/box.rs index 9947a57a80..d95aa98ef0 100644 --- a/style/values/specified/box.rs +++ b/style/values/specified/box.rs @@ -746,18 +746,23 @@ pub enum AlignmentBaseline { /// Use the text-under baseline. TextBottom, /// Use the alphabetic baseline. + #[cfg(feature = "gecko")] Alphabetic, /// Use the ideographic-under baseline. + #[cfg(feature = "gecko")] Ideographic, /// In general, use the x-middle baselines; except under text-orientation: upright /// (where the alphabetic and x-height baselines are essentially meaningless) use /// the central baseline instead. Middle, /// Use the central baseline. + #[cfg(feature = "gecko")] Central, /// Use the math baseline. + #[cfg(feature = "gecko")] Mathematical, /// Use the hanging baseline. + #[cfg(feature = "gecko")] Hanging, /// Use the text-over baseline. TextTop, From 813923186eff5bcb957fe6da8538038acd918e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Thu, 4 Jun 2026 12:00:03 +0000 Subject: [PATCH 09/29] Bug 2044947 - Fix calc node's Invert unit validation. r=sajidanwar,janv The unit of (1 / 2px) is definitely not a px. Right now we disallow dividing by non-numbers, so enforce it. Once we implement unit math we could start tracking exponents (and support something like squared pixels etc). Differential Revision: https://phabricator.services.mozilla.com/D304694 --- style/values/generics/calc.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/style/values/generics/calc.rs b/style/values/generics/calc.rs index cebc2b4a32..78809de2a2 100644 --- a/style/values/generics/calc.rs +++ b/style/values/generics/calc.rs @@ -574,9 +574,7 @@ impl CalcNode { pub fn unit(&self) -> Result { Ok(match self { CalcNode::Leaf(l) => l.unit(), - CalcNode::Negate(child) | CalcNode::Invert(child) | CalcNode::Abs(child) => { - child.unit()? - }, + CalcNode::Negate(child) | CalcNode::Abs(child) => child.unit()?, CalcNode::Sum(children) => { let mut unit = children.first().unwrap().unit()?; for child in children.iter().skip(1) { @@ -692,7 +690,7 @@ impl CalcNode { } CalcUnits::empty() }, - CalcNode::Sqrt(ref c) | CalcNode::Exp(ref c) => { + CalcNode::Invert(ref c) | CalcNode::Sqrt(ref c) | CalcNode::Exp(ref c) => { let child_unit = c.unit()?; if !child_unit.is_empty() { return Err(()); From 9ed64699ec352852e96e882a42e0057caf704280 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Sat, 13 Jun 2026 02:26:53 -0400 Subject: [PATCH 10/29] Add device-width/device-height media features. (#368) Servo PR: https://github.com/servo/servo/pull/44978 Signed-off-by: Josh Matthews --- style/device/servo.rs | 19 +++++++++++++++++++ style/servo/media_features.rs | 28 +++++++++++++++++++++++++++- stylo_atoms/static_atoms.txt | 2 ++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/style/device/servo.rs b/style/device/servo.rs index 2d14029f91..4f45a32276 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -56,6 +56,8 @@ pub(super) struct ExtraDeviceData { media_type: MediaType, /// The current viewport size, in CSS pixels. viewport_size: Size2D, + /// The current screen size, in device pixels. + device_size: Size2D, /// The current device pixel ratio, from CSS pixels to device pixels. device_pixel_ratio: Scale, /// The current quirks mode. @@ -75,6 +77,7 @@ impl Device { media_type: MediaType, quirks_mode: QuirksMode, viewport_size: Size2D, + device_size: Size2D, device_pixel_ratio: Scale, font_metrics_provider: Box, default_values: Arc, @@ -101,6 +104,7 @@ impl Device { extra: ExtraDeviceData { media_type, viewport_size, + device_size, device_pixel_ratio, quirks_mode, prefers_color_scheme, @@ -207,6 +211,21 @@ impl Device { self.extra.device_pixel_ratio = device_pixel_ratio; } + /// Set the device size on this [`Device`] (e.g. the available screen dimensions). + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_device_size(&mut self, device_size: Size2D) { + self.extra.device_size = device_size; + } + + /// Returns the screen size of the current device in app units. + #[inline] + pub fn device_size(&self) -> Size2D { + self.extra.device_size + } + /// Gets the size of the scrollbar in CSS pixels. pub fn scrollbar_inline_size(&self) -> CSSPixelLength { // TODO: implement this. diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index a6a56039df..65f507b9ea 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -15,6 +15,20 @@ fn eval_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) } +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width(context: &Context) -> CSSPixelLength { + let device = context.device(); + let scaled = device.device_size() / device.device_pixel_ratio(); + CSSPixelLength::new(scaled.width) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height(context: &Context) -> CSSPixelLength { + let device = context.device(); + let scaled = device.device_size() / device.device_pixel_ratio(); + CSSPixelLength::new(scaled.height) +} + #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Scan { @@ -47,13 +61,25 @@ fn eval_prefers_color_scheme(context: &Context, query_value: Option Date: Mon, 15 Jun 2026 01:25:27 -0700 Subject: [PATCH 11/29] Use f64 everywhere in animation logic (#392) We were previously using a mix of f32 and f64. Then it could happen that we would first check that `a < b` in f64, but when converting to f32, they might become equal due to the precision loss. Thus this could break invariants in the logic, making a `debug_unreachable!()` reachable. Servo PR: https://github.com/servo/servo/pull/45682 Signed-off-by: Oriol Brufau Co-authored-by: Martin Robinson --- style/servo/animation.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 402af9efd2..23b2bcc747 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -135,11 +135,11 @@ pub enum KeyframesIterationState { struct IntermediateComputedKeyframe { declarations: PropertyDeclarationBlock, timing_function: Option, - start_percentage: f32, + start_percentage: f64, } impl IntermediateComputedKeyframe { - fn new(start_percentage: f32) -> Self { + fn new(start_percentage: f64) -> Self { IntermediateComputedKeyframe { declarations: PropertyDeclarationBlock::new(), timing_function: None, @@ -161,7 +161,7 @@ impl IntermediateComputedKeyframe { let mut intermediate_steps: Vec = Vec::with_capacity(animation.steps.len()); let mut current_step = IntermediateComputedKeyframe::new(0.); for step in animation.steps.iter() { - let start_percentage = step.start_offset.percentage.0; + let start_percentage = step.start_offset.percentage.0 as f64; if start_percentage != current_step.start_percentage { let new_step = IntermediateComputedKeyframe::new(start_percentage); intermediate_steps.push(std::mem::replace(&mut current_step, new_step)); @@ -288,7 +288,7 @@ struct ComputedKeyframe { /// The starting percentage (a number between 0 and 1) which represents /// at what point in an animation iteration this step is. - start_percentage: f32, + start_percentage: f64, /// The animation values to transition to and from when processing this /// keyframe animation step. @@ -324,7 +324,7 @@ struct KeyframeDataForProperty<'a> { /// The starting percentage (a number between 0 and 1) which represents /// at what point in an animation iteration this step is. - start_percentage: f32, + start_percentage: f64, value: &'a AnimationValue, } @@ -873,7 +873,7 @@ impl Animation { next_keyframe_index = self .computed_steps .iter() - .position(|step| (total_progress as f32) < step.start_percentage); + .position(|step| total_progress < step.start_percentage); prev_keyframe_index = next_keyframe_index .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) .unwrap_or(0); @@ -883,7 +883,7 @@ impl Animation { .computed_steps .iter() .rev() - .position(|step| total_progress as f32 <= 1. - step.start_percentage) + .position(|step| total_progress <= 1. - step.start_percentage) .map(|pos| num_steps - pos - 1); prev_keyframe_index = next_keyframe_index .and_then(|pos| { @@ -948,11 +948,11 @@ impl Animation { }; let percentage_between_keyframes = - (next_keyframe.start_percentage - previous_keyframe.start_percentage).abs() as f64; + (next_keyframe.start_percentage - previous_keyframe.start_percentage).abs(); let duration_between_keyframes = percentage_between_keyframes * self.duration; let direction_aware_prev_keyframe_start_percentage = match self.current_direction { - AnimationDirection::Normal => previous_keyframe.start_percentage as f64, - AnimationDirection::Reverse => 1. - previous_keyframe.start_percentage as f64, + AnimationDirection::Normal => previous_keyframe.start_percentage, + AnimationDirection::Reverse => 1. - previous_keyframe.start_percentage, _ => unreachable!(), }; let progress_between_keyframes = (total_progress @@ -962,7 +962,7 @@ impl Animation { from: previous_keyframe.value.clone(), to: next_keyframe.value.clone(), timing_function: previous_keyframe.timing_function.clone(), - duration: duration_between_keyframes as f64, + duration: duration_between_keyframes, }; let value = animation.calculate_value(progress_between_keyframes); @@ -1781,7 +1781,7 @@ pub fn maybe_start_animations( // 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. - let delay = style.animation_delay_mod(i).seconds(); + let delay = style.animation_delay_mod(i).seconds() as f64; let iteration_count = style.animation_iteration_count_mod(i); let iteration_state = if iteration_count.0.is_infinite() { @@ -1802,7 +1802,7 @@ pub fn maybe_start_animations( }; let now = context.current_time_for_animations; - let started_at = now + delay as f64; + let started_at = now + delay; let mut starting_progress = (now - started_at) / duration; let state = match style.animation_play_state_mod(i) { AnimationPlayState::Paused => AnimationState::Paused(starting_progress), @@ -1840,7 +1840,7 @@ pub fn maybe_start_animations( started_at, duration, fill_mode: style.animation_fill_mode_mod(i), - delay: delay as f64, + delay, iteration_state, state, direction: animation_direction, From 3a386f0028c693df4574092def105081294bf8bb Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Mon, 15 Jun 2026 13:12:01 +0200 Subject: [PATCH 12/29] Revert "Add device-width/device-height media features." (#394) Reverts servo/stylo#368 More work is required in Servo to integrate this. --- style/device/servo.rs | 19 ------------------- style/servo/media_features.rs | 28 +--------------------------- stylo_atoms/static_atoms.txt | 2 -- 3 files changed, 1 insertion(+), 48 deletions(-) diff --git a/style/device/servo.rs b/style/device/servo.rs index 4f45a32276..2d14029f91 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -56,8 +56,6 @@ pub(super) struct ExtraDeviceData { media_type: MediaType, /// The current viewport size, in CSS pixels. viewport_size: Size2D, - /// The current screen size, in device pixels. - device_size: Size2D, /// The current device pixel ratio, from CSS pixels to device pixels. device_pixel_ratio: Scale, /// The current quirks mode. @@ -77,7 +75,6 @@ impl Device { media_type: MediaType, quirks_mode: QuirksMode, viewport_size: Size2D, - device_size: Size2D, device_pixel_ratio: Scale, font_metrics_provider: Box, default_values: Arc, @@ -104,7 +101,6 @@ impl Device { extra: ExtraDeviceData { media_type, viewport_size, - device_size, device_pixel_ratio, quirks_mode, prefers_color_scheme, @@ -211,21 +207,6 @@ impl Device { self.extra.device_pixel_ratio = device_pixel_ratio; } - /// Set the device size on this [`Device`] (e.g. the available screen dimensions). - /// - /// Note that this does not update any associated `Stylist`. For this you must call - /// `Stylist::media_features_change_changed_style` and - /// `Stylist::force_stylesheet_origins_dirty`. - pub fn set_device_size(&mut self, device_size: Size2D) { - self.extra.device_size = device_size; - } - - /// Returns the screen size of the current device in app units. - #[inline] - pub fn device_size(&self) -> Size2D { - self.extra.device_size - } - /// Gets the size of the scrollbar in CSS pixels. pub fn scrollbar_inline_size(&self) -> CSSPixelLength { // TODO: implement this. diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index 65f507b9ea..a6a56039df 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -15,20 +15,6 @@ fn eval_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) } -/// https://drafts.csswg.org/mediaqueries-4/#device-width -fn eval_device_width(context: &Context) -> CSSPixelLength { - let device = context.device(); - let scaled = device.device_size() / device.device_pixel_ratio(); - CSSPixelLength::new(scaled.width) -} - -/// https://drafts.csswg.org/mediaqueries-4/#device-height -fn eval_device_height(context: &Context) -> CSSPixelLength { - let device = context.device(); - let scaled = device.device_size() / device.device_pixel_ratio(); - CSSPixelLength::new(scaled.height) -} - #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Scan { @@ -61,25 +47,13 @@ fn eval_prefers_color_scheme(context: &Context, query_value: Option Date: Mon, 15 Jun 2026 12:53:19 +0100 Subject: [PATCH 13/29] Add the pointer/hover media features (#391) Fixes #390 This feature is quite impactful as it is required for Tailwind's hover styles to work. Servo PR: https://github.com/servo/servo/pull/45681 Signed-off-by: Nico Burns --- style/device/servo.rs | 39 ++++++++++++ style/servo/media_features.rs | 109 +++++++++++++++++++++++++++++++++- stylo_atoms/static_atoms.txt | 4 ++ 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/style/device/servo.rs b/style/device/servo.rs index 2d14029f91..7bc1e2c551 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -13,6 +13,7 @@ use crate::media_queries::MediaType; use crate::properties::style_structs::Font; use crate::properties::ComputedValues; use crate::queries::values::PrefersColorScheme; +use crate::servo::media_features::PointerCapabilities; use crate::values::computed::font::GenericFontFamily; use crate::values::computed::{CSSPixelLength, Length, LineHeight, NonNegativeLength}; use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; @@ -64,6 +65,12 @@ pub(super) struct ExtraDeviceData { /// Whether the user prefers light mode or dark mode #[ignore_malloc_size_of = "Pure stack type"] prefers_color_scheme: PrefersColorScheme, + /// The capabilities of the primary pointer input + #[ignore_malloc_size_of = "Pure stack type"] + primary_pointer_capabilities: PointerCapabilities, + /// The union of the capabilities of all pointer inputs + #[ignore_malloc_size_of = "Pure stack type"] + all_pointer_capabilities: PointerCapabilities, /// An implementation of a trait which implements support for querying font metrics. #[ignore_malloc_size_of = "Owned by embedder"] font_metrics_provider: Box, @@ -79,6 +86,8 @@ impl Device { font_metrics_provider: Box, default_values: Arc, prefers_color_scheme: PrefersColorScheme, + primary_pointer_capabilities: PointerCapabilities, + all_pointer_capabilities: PointerCapabilities, ) -> Device { let root_style = RwLock::new(Arc::clone(&default_values)); Device { @@ -104,6 +113,8 @@ impl Device { device_pixel_ratio, quirks_mode, prefers_color_scheme, + primary_pointer_capabilities, + all_pointer_capabilities, font_metrics_provider, }, } @@ -264,6 +275,34 @@ impl Device { self.extra.prefers_color_scheme } + /// Set the [`PointerCapbabilities`] value for the primary pointer on this [`Device`] + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_primary_pointer_capabilities(&mut self, capabilities: PointerCapabilities) { + self.extra.primary_pointer_capabilities = capabilities; + } + + /// Returns the pointer capabilities of this [`Device`]. + pub fn primary_pointer_capabilities(&self) -> PointerCapabilities { + self.extra.primary_pointer_capabilities + } + + /// Set the [`PointerCapbabilities`] value for all pointers on this [`Device`] + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_all_pointer_capabilities(&mut self, capabilities: PointerCapabilities) { + self.extra.all_pointer_capabilities = capabilities; + } + + /// Returns the pointer capabilities of this [`Device`]. + pub fn all_pointer_capabilities(&self) -> PointerCapabilities { + self.extra.all_pointer_capabilities + } + pub(crate) fn is_dark_color_scheme(&self, _: ColorSchemeFlags) -> bool { false } diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index a6a56039df..2f4210e1dc 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -46,14 +46,121 @@ fn eval_prefers_color_scheme(context: &Context, query_value: Option Self { + PointerCapabilities::COARSE + } + #[cfg(not(any(target_os = "ios", target_os = "android", target_env = "ohos")))] + fn default() -> Self { + PointerCapabilities::FINE | PointerCapabilities::HOVER + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Pointer { + None, + Coarse, + Fine, +} + +fn eval_pointer_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + match query_value { + None => !pointer_capabilities.is_empty(), + Some(Pointer::None) => pointer_capabilities.is_empty(), + Some(Pointer::Coarse) => pointer_capabilities.intersects(PointerCapabilities::COARSE), + Some(Pointer::Fine) => pointer_capabilities.intersects(PointerCapabilities::FINE), + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#pointer +fn eval_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, context.device().primary_pointer_capabilities()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer +fn eval_any_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, context.device().all_pointer_capabilities()) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Hover { + None, + Hover, +} + +fn eval_hover_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER); + match query_value { + Some(Hover::None) => !can_hover, + Some(Hover::Hover) => can_hover, + None => return can_hover, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#hover +fn eval_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, context.device().primary_pointer_capabilities()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover +fn eval_any_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, context.device().all_pointer_capabilities()) +} + /// A list with all the media features that Servo supports. -pub static MEDIA_FEATURES: [QueryFeatureDescription; 6] = [ +pub static MEDIA_FEATURES: [QueryFeatureDescription; 10] = [ feature!( atom!("width"), AllowsRanges::Yes, Evaluator::Length(eval_width), FeatureFlags::empty(), ), + feature!( + atom!("pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("any-pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_any_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("hover"), + AllowsRanges::No, + keyword_evaluator!(eval_hover, Hover), + FeatureFlags::empty(), + ), + feature!( + atom!("any-hover"), + AllowsRanges::No, + keyword_evaluator!(eval_any_hover, Hover), + FeatureFlags::empty(), + ), feature!( atom!("scan"), AllowsRanges::No, diff --git a/stylo_atoms/static_atoms.txt b/stylo_atoms/static_atoms.txt index f9477fc92a..f92e668be5 100644 --- a/stylo_atoms/static_atoms.txt +++ b/stylo_atoms/static_atoms.txt @@ -20,6 +20,8 @@ animationcancel animationend animationiteration animationstart +any-hover +any-pointer aspect-ratio beforeinput beforetoggle @@ -69,6 +71,7 @@ hairline hashchange height hidden +hover icecandidate iceconnectionstatechange icegatheringstatechange @@ -111,6 +114,7 @@ password pause play playing +pointer popstate postershown prefers-color-scheme From 85b34b05f17288dc57b1f5444bb20c2c41750c4e Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 16 Jun 2026 13:37:49 +0200 Subject: [PATCH 14/29] Remove `overrides_transform_style()` and `get_used_transform_style()` (#388) It's not possible to properly calculate these only looking at style, because the results depends on the used `overflow` value. Servo PR: servo/servo#45629 Signed-off-by: Martin Robinson --- style/properties/properties.mako.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index 76e880b8f8..3210eb4eba 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -2121,32 +2121,6 @@ impl ComputedValuesInner { &position_style.left, )) } - - /// Return true if the effects force the transform style to be Flat - pub fn overrides_transform_style(&self) -> bool { - use crate::computed_values::mix_blend_mode::T as MixBlendMode; - - let effects = self.get_effects(); - // TODO(gw): Add clip-path, isolation, mask-image, mask-border-source when supported. - effects.opacity < 1.0 || - !effects.filter.0.is_empty() || - !effects.clip.is_auto() || - effects.mix_blend_mode != MixBlendMode::Normal - } - - /// - pub fn get_used_transform_style(&self) -> computed_values::transform_style::T { - use crate::computed_values::transform_style::T as TransformStyle; - - let box_ = self.get_box(); - - if self.overrides_transform_style() { - TransformStyle::Flat - } else { - // Return the computed value if not overridden by the above exceptions - box_.transform_style - } - } } /// A reference to a style struct of the parent, or our own style struct. From f59f7427e99aa352e1f5579eca7786d2134bbb32 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Thu, 18 Jun 2026 06:37:42 -0700 Subject: [PATCH 15/29] Remove unused import: `crate::computed_values` (#398) Fixes a compile warning from #388. Signed-off-by: Oriol Brufau --- style/properties/properties.mako.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/style/properties/properties.mako.rs b/style/properties/properties.mako.rs index 3210eb4eba..c675e47c8a 100644 --- a/style/properties/properties.mako.rs +++ b/style/properties/properties.mako.rs @@ -14,7 +14,6 @@ use std::{ops, ptr, fmt, mem}; #[cfg(feature = "servo")] use euclid::SideOffsets2D; #[cfg(feature = "gecko")] use crate::gecko_bindings::structs::{self, NonCustomCSSPropertyId}; #[cfg(feature = "servo")] use crate::logical_geometry::LogicalMargin; -#[cfg(feature = "servo")] use crate::computed_values; #[cfg(feature = "servo")] use crate::dom::AttributeReferences; use crate::logical_geometry::WritingMode; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; From 71bbb705678fbcdbab105f0ffc5a339b6600186f Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Tue, 23 Jun 2026 09:27:27 -0700 Subject: [PATCH 16/29] Allow Servo to use computed `@font-face` ranges (#399) Performs some changes so that Servo will be able to use these: - `ComputedFontWeightRange` - `ComputedFontStretchRange` - `ComputedFontStyleDescriptor` Servo PR: https://github.com/servo/servo/pull/45821 Signed-off-by: Oriol Brufau --- style/font_face.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/style/font_face.rs b/style/font_face.rs index 192c38dd4d..de7af74114 100644 --- a/style/font_face.rs +++ b/style/font_face.rs @@ -10,6 +10,8 @@ use crate::derives::*; use crate::error_reporting::ContextualParseError; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::values::computed::font::FontStyleFixedPoint; +use crate::values::computed::FontWeight; use crate::values::generics::font::FontStyle as GenericFontStyle; use crate::values::specified::{url::SpecifiedUrl, Angle}; use cssparser::{Parser, RuleBodyParser, SourceLocation}; @@ -333,13 +335,17 @@ macro_rules! impl_range { pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight); impl_range!(FontWeightRange, AbsoluteFontWeight); -/// The computed representation of the above so Gecko can read them easily. +/// The computed representation of the above so Gecko and Servo can read them easily. /// /// This one is needed because cbindgen doesn't know how to generate /// specified::Number. #[repr(C)] #[allow(missing_docs)] -pub struct ComputedFontWeightRange(f32, f32); +#[cfg_attr( + feature = "servo", + derive(Clone, Debug, Deserialize, Hash, MallocSizeOf, PartialEq, Serialize) +)] +pub struct ComputedFontWeightRange(pub FontWeight, pub FontWeight); #[inline] fn sort_range(a: T, b: T) -> (T, T) { @@ -353,7 +359,7 @@ fn sort_range(a: T, b: T) -> (T, T) { impl FontWeightRange { /// Returns a computed font-weight range, or None if either bound is an unresolvable calc. pub fn compute(&self) -> Option { - let (min, max) = sort_range(self.0.compute()?.value(), self.1.compute()?.value()); + let (min, max) = sort_range(self.0.compute()?, self.1.compute()?); Some(ComputedFontWeightRange(min, max)) } } @@ -365,11 +371,15 @@ impl FontWeightRange { pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch); impl_range!(FontStretchRange, SpecifiedFontStretch); -/// The computed representation of the above, so that Gecko can read them +/// The computed representation of the above, so that Gecko and Servo can read them /// easily. #[repr(C)] #[allow(missing_docs)] -pub struct ComputedFontStretchRange(FontStretch, FontStretch); +#[cfg_attr( + feature = "servo", + derive(Clone, Debug, Deserialize, Hash, MallocSizeOf, PartialEq, Serialize) +)] +pub struct ComputedFontStretchRange(pub FontStretch, pub FontStretch); impl FontStretchRange { /// Returns a computed font-stretch range, or None if any value contains a calc @@ -400,13 +410,14 @@ pub enum FontStyle { Oblique(Angle, Angle), } -/// The computed representation of the above, with angles in degrees, so that -/// Gecko can read them easily. +/// The computed representation of the above, with angles in degrees stored as +/// signed 8.8 fixed-point values, so that Gecko and Servo can read them easily. #[repr(u8)] #[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Clone, Debug, Deserialize, Serialize))] pub enum ComputedFontStyleDescriptor { Italic, - Oblique(f32, f32), + Oblique(FontStyleFixedPoint, FontStyleFixedPoint), } impl Parse for FontStyle { @@ -474,7 +485,10 @@ impl FontStyle { let first = SpecifiedFontStyle::compute_angle_degrees(first)?; let second = SpecifiedFontStyle::compute_angle_degrees(second)?; let (min, max) = sort_range(first, second); - Some(ComputedFontStyleDescriptor::Oblique(min, max)) + Some(ComputedFontStyleDescriptor::Oblique( + FontStyleFixedPoint::from_float(min), + FontStyleFixedPoint::from_float(max), + )) }, } } From 89499265377cfdf48b9257531259b02642aafa06 Mon Sep 17 00:00:00 2001 From: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:36:27 +0200 Subject: [PATCH 17/29] Add "aspect-ratio" media feature. (#385) The implementation does the same as what gecko does (except that I didn't add support for `device-aspect-ratio`, hence slightly simpler / inlined. Servo PR: https://github.com/servo/servo/pull/45490 --------- Signed-off-by: Martin Robinson Co-authored-by: Martin Robinson --- style/servo/media_features.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index 2f4210e1dc..8540af94d3 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -7,7 +7,7 @@ use crate::derives::*; use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; use crate::queries::values::PrefersColorScheme; -use crate::values::computed::{CSSPixelLength, Context, Resolution}; +use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; use std::fmt::Debug; /// https://drafts.csswg.org/mediaqueries-4/#width @@ -129,8 +129,14 @@ fn eval_any_hover(context: &Context, query_value: Option) -> bool { eval_hover_capabilities(query_value, context.device().all_pointer_capabilities()) } +/// +fn eval_aspect_ratio(context: &Context) -> Ratio { + let size = context.device().au_viewport_size(); + Ratio::new(size.width.0 as f32, size.height.0 as f32) +} + /// A list with all the media features that Servo supports. -pub static MEDIA_FEATURES: [QueryFeatureDescription; 10] = [ +pub static MEDIA_FEATURES: [QueryFeatureDescription; 11] = [ feature!( atom!("width"), AllowsRanges::Yes, @@ -161,6 +167,12 @@ pub static MEDIA_FEATURES: [QueryFeatureDescription; 10] = [ keyword_evaluator!(eval_any_hover, Hover), FeatureFlags::empty(), ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_aspect_ratio), + FeatureFlags::VIEWPORT_DEPENDENT, + ), feature!( atom!("scan"), AllowsRanges::No, From e7a7e348e700039af890fae301c481bdaec78b9e Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 26 Jun 2026 01:02:29 -0700 Subject: [PATCH 18/29] Optimize `Animation::iterate()` into `Animation::iterate_by()` (#400) This method was performing a single iteration. But most callers want to perform multiple iterations, so they were calling it in a loop. So this patch replaces `iterate()` with `iterate_by()`, which takes the desired number of iterations as a parameter, and returns the actual number of iterations that were performed. Servo PR: https://github.com/servo/servo/pull/45978 Signed-off-by: Oriol Brufau --- style/servo/animation.rs | 67 +++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 23b2bcc747..925096328a 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -594,30 +594,33 @@ impl Animation { return false; } - if self.on_last_iteration() { - return false; - } - - self.iterate(); - true + self.iterate_by(1.) == 1. } - fn iterate(&mut self) { - debug_assert!(!self.on_last_iteration()); + /// Attempts to advance this animation by `n` iterations, but stops when reaching + /// the last iteration, and doesn't perform fractional iterations. + /// Returns the actual number of iterations that happened. + fn iterate_by(&mut self, n: f64) -> f64 { + let n = n.trunc().min(self.remaining_iterations().ceil() - 1.0); + if n < 1. { + return 0.; + } if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { - *current = (*current + 1.).min(max); + *current = (*current + n).min(max); } if let AnimationState::Paused(ref mut progress) = self.state { - debug_assert!(*progress > 1.); - *progress -= 1.; + debug_assert!(*progress > n); + *progress -= n; } // Update the next iteration direction if applicable. - self.started_at += self.duration; + self.started_at += self.duration * n; match self.direction { - AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { + AnimationDirection::Alternate | AnimationDirection::AlternateReverse + if n % 2. == 1.0 => + { self.current_direction = match self.current_direction { AnimationDirection::Normal => AnimationDirection::Reverse, AnimationDirection::Reverse => AnimationDirection::Normal, @@ -628,6 +631,15 @@ impl Animation { }, _ => {}, } + + n + } + + fn remaining_iterations(&self) -> f64 { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => max - current, + KeyframesIterationState::Infinite(_) => f64::INFINITY, + } } /// A number (> 0 and <= 1) which represents the fraction of a full iteration @@ -635,10 +647,7 @@ impl Animation { /// if the current iteration is the fractional remainder of a non-integral /// iteration count. pub fn current_iteration_end_progress(&self) -> f64 { - match self.iteration_state { - KeyframesIterationState::Finite(current, max) => (max - current).min(1.), - KeyframesIterationState::Infinite(_) => 1., - } + self.remaining_iterations().min(1.) } /// The duration of the current iteration of this animation which may be less @@ -655,10 +664,7 @@ impl Animation { /// Assuming this animation is running, whether or not it is on the last iteration. fn on_last_iteration(&self) -> bool { - match self.iteration_state { - KeyframesIterationState::Finite(current, max) => current >= (max - 1.), - KeyframesIterationState::Infinite(_) => false, - } + self.remaining_iterations() <= 1. } /// Whether or not this animation has finished at the provided time. This does @@ -714,10 +720,7 @@ impl Animation { match old_state { Paused(old_progress) => { let mut progress = old_progress + (old_delay - self.delay) / self.duration; - while progress > 1. && !self.on_last_iteration() { - self.iterate(); - progress -= 1.; - } + progress -= self.iterate_by(progress); self.state = Paused(progress); }, Finished => { @@ -732,15 +735,12 @@ impl Animation { _ => { // Running or Pending — re-advance iterations from a fresh // iteration state. - let mut starting_progress = (now - self.started_at) / self.duration; + let starting_progress = (now - self.started_at) / self.duration; match self.iteration_state { KeyframesIterationState::Finite(ref mut current, _) => *current = 0.0, _ => {}, } - while starting_progress > 1. && !self.on_last_iteration() { - self.iterate(); - starting_progress -= 1.; - } + self.iterate_by(starting_progress); }, } @@ -1803,7 +1803,7 @@ pub fn maybe_start_animations( let now = context.current_time_for_animations; let started_at = now + delay; - let mut starting_progress = (now - started_at) / duration; + let starting_progress = (now - started_at) / duration; let state = match style.animation_play_state_mod(i) { AnimationPlayState::Paused => AnimationState::Paused(starting_progress), AnimationPlayState::Running => AnimationState::Pending, @@ -1851,10 +1851,7 @@ pub fn maybe_start_animations( // If we started with a negative delay, make sure we iterate the animation if // the delay moves us past the first iteration. - while starting_progress > 1. && !new_animation.on_last_iteration() { - new_animation.iterate(); - starting_progress -= 1.; - } + new_animation.iterate_by(starting_progress); animation_state.dirty = true; From 49c0bc3037b2168c2a24d18f30f99863c25f3e88 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Fri, 26 Jun 2026 01:25:45 -0700 Subject: [PATCH 19/29] Make `iterate()` increment the iteration state for infinite animations (#401) It was only incremented for finite animations, but infinite animations need to be incremented too. Servo PR: https://github.com/servo/servo/pull/45990 Signed-off-by: Oriol Brufau --- style/servo/animation.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index 925096328a..aebe21c57c 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -606,8 +606,13 @@ impl Animation { return 0.; } - if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { - *current = (*current + n).min(max); + match self.iteration_state { + KeyframesIterationState::Finite(ref mut current, max) => { + *current = (*current + n).min(max); + }, + KeyframesIterationState::Infinite(ref mut current) => { + *current += n; + }, } if let AnimationState::Paused(ref mut progress) = self.state { From 64ec89985d7708de869bcec5fff3d7c6072be164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=BClker?= Date: Fri, 26 Jun 2026 13:07:19 +0200 Subject: [PATCH 20/29] style: Derive MallocSizeOf for ComputedFontStyle (#402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Needed for https://github.com/servo/servo/pull/45901 The servo PR is already approved, feel free to merge this immediately. Signed-off-by: Simon Wülker --- style/font_face.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/font_face.rs b/style/font_face.rs index de7af74114..0f0ba9237e 100644 --- a/style/font_face.rs +++ b/style/font_face.rs @@ -414,7 +414,7 @@ pub enum FontStyle { /// signed 8.8 fixed-point values, so that Gecko and Servo can read them easily. #[repr(u8)] #[allow(missing_docs)] -#[cfg_attr(feature = "servo", derive(Clone, Debug, Deserialize, Serialize))] +#[cfg_attr(feature = "servo", derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize))] pub enum ComputedFontStyleDescriptor { Italic, Oblique(FontStyleFixedPoint, FontStyleFixedPoint), From 556f7273f299462d84e1ab30e8b0269addbe76bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlad=20Negar=C4=83?= <150728638+VladNegara@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:24:15 +0200 Subject: [PATCH 21/29] Fix typo in `selectors` README (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I noticed a typo in the README file of the `selectors` library. Signed-off-by: Vlad Negară <150728638+VladNegara@users.noreply.github.com> --- selectors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selectors/README.md b/selectors/README.md index 3ce269fa3c..e6f5689e6f 100644 --- a/selectors/README.md +++ b/selectors/README.md @@ -6,7 +6,7 @@ rust-selectors * [crates.io](https://crates.io/crates/selectors) CSS Selectors library for Rust. -Includes parsing and serilization of selectors, +Includes parsing and serialization of selectors, as well as matching against a generic tree of elements. Pseudo-elements and most pseudo-classes are generic as well. From 93f1ccf721a5049ead29cf2389188597fdc062ad Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Fri, 26 Jun 2026 10:54:11 -0400 Subject: [PATCH 22/29] Add device-width/device-height media features. (#397) Servo PR: https://github.com/servo/servo/pull/44978 Signed-off-by: Josh Matthews --- style/device/servo.rs | 19 +++++++++++++++++++ style/servo/media_features.rs | 28 +++++++++++++++++++++++++++- stylo_atoms/static_atoms.txt | 2 ++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/style/device/servo.rs b/style/device/servo.rs index 7bc1e2c551..65641e689f 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -57,6 +57,8 @@ pub(super) struct ExtraDeviceData { media_type: MediaType, /// The current viewport size, in CSS pixels. viewport_size: Size2D, + /// The current screen size, in device pixels. + device_size: Size2D, /// The current device pixel ratio, from CSS pixels to device pixels. device_pixel_ratio: Scale, /// The current quirks mode. @@ -82,6 +84,7 @@ impl Device { media_type: MediaType, quirks_mode: QuirksMode, viewport_size: Size2D, + device_size: Size2D, device_pixel_ratio: Scale, font_metrics_provider: Box, default_values: Arc, @@ -110,6 +113,7 @@ impl Device { extra: ExtraDeviceData { media_type, viewport_size, + device_size, device_pixel_ratio, quirks_mode, prefers_color_scheme, @@ -218,6 +222,21 @@ impl Device { self.extra.device_pixel_ratio = device_pixel_ratio; } + /// Set the device size on this [`Device`] (e.g. the available screen dimensions). + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_device_size(&mut self, device_size: Size2D) { + self.extra.device_size = device_size; + } + + /// Returns the screen size of the current device in app units. + #[inline] + pub fn device_size(&self) -> Size2D { + self.extra.device_size + } + /// Gets the size of the scrollbar in CSS pixels. pub fn scrollbar_inline_size(&self) -> CSSPixelLength { // TODO: implement this. diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index 8540af94d3..c4b3b5c760 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -15,6 +15,20 @@ fn eval_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) } +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width(context: &Context) -> CSSPixelLength { + let device = context.device(); + let scaled = device.device_size() / device.device_pixel_ratio(); + CSSPixelLength::new(scaled.width) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height(context: &Context) -> CSSPixelLength { + let device = context.device(); + let scaled = device.device_size() / device.device_pixel_ratio(); + CSSPixelLength::new(scaled.height) +} + #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Scan { @@ -136,7 +150,7 @@ fn eval_aspect_ratio(context: &Context) -> Ratio { } /// A list with all the media features that Servo supports. -pub static MEDIA_FEATURES: [QueryFeatureDescription; 11] = [ +pub static MEDIA_FEATURES: [QueryFeatureDescription; 13] = [ feature!( atom!("width"), AllowsRanges::Yes, @@ -173,6 +187,18 @@ pub static MEDIA_FEATURES: [QueryFeatureDescription; 11] = [ Evaluator::NumberRatio(eval_aspect_ratio), FeatureFlags::VIEWPORT_DEPENDENT, ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + FeatureFlags::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + FeatureFlags::empty(), + ), feature!( atom!("scan"), AllowsRanges::No, diff --git a/stylo_atoms/static_atoms.txt b/stylo_atoms/static_atoms.txt index f92e668be5..af047ae96e 100644 --- a/stylo_atoms/static_atoms.txt +++ b/stylo_atoms/static_atoms.txt @@ -51,7 +51,9 @@ datachannel date datetime-local dir +device-height device-pixel-ratio +device-width durationchange email emptied From 44bdcc6a017141ff8b6a856cd717c52a476d90cb Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Sat, 27 Jun 2026 14:44:59 +0200 Subject: [PATCH 23/29] Always use progress=1.0 for 0 duration animation values (#403) Transitions can sometimes have 0 duration when the transition reversing shortening factor is also calculated to be 0. When that happens, we should always use 1.0 for the progress when calculating the value for the transition. This avoid division by zero. Servo PR: servo/servo#46016 Signed-off-by: Martin Robinson --- style/servo/animation.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/style/servo/animation.rs b/style/servo/animation.rs index aebe21c57c..c1c40d36f9 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -1126,9 +1126,15 @@ impl Transition { /// Update the given animation at a given point of progress. pub fn calculate_value(&self, time: f64) -> AnimationValue { - let progress = (time - self.start_time) / (self.property_animation.duration); - self.property_animation - .calculate_value(progress.clamp(0.0, 1.0)) + let progress = if time < self.start_time { + 0.0 + } else if self.property_animation.duration == 0.0 { + 1.0 + } else { + ((time - self.start_time) / self.property_animation.duration).clamp(0.0, 1.0) + }; + + self.property_animation.calculate_value(progress) } } From 6b4eb30c6a312babd3bf12f5a75a3479c6b840ec Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Sun, 28 Jun 2026 06:58:14 -0700 Subject: [PATCH 24/29] Relax assert for paused animations to include equality (#404) It's fine to subtract `n` when the value is `n`, we just don't want to get negative. Signed-off-by: Oriol Brufau --- 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 c1c40d36f9..b19d2b55b6 100644 --- a/style/servo/animation.rs +++ b/style/servo/animation.rs @@ -616,7 +616,7 @@ impl Animation { } if let AnimationState::Paused(ref mut progress) = self.state { - debug_assert!(*progress > n); + debug_assert!(*progress >= n); *progress -= n; } From 9d12364ecccb69cd26622391180f130c5346e393 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sun, 28 Jun 2026 23:06:34 +0100 Subject: [PATCH 25/29] Implement the `height` and `orientation` media features (#395) We were already passing viewport dimensions into Stylo as part of the `Device` which is sufficient to determine, `width`, `height` and `orientation`, but we were previously only exposing the `width` to CSS.| Servo PR: https://github.com/servo/servo/pull/45707 --------- Signed-off-by: Nico Burns --- style/servo/media_features.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index c4b3b5c760..9e5a77e82a 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -6,7 +6,7 @@ use crate::derives::*; use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; -use crate::queries::values::PrefersColorScheme; +use crate::queries::values::{Orientation, PrefersColorScheme}; use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; use std::fmt::Debug; @@ -15,6 +15,11 @@ fn eval_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) } +/// https://drafts.csswg.org/mediaqueries-4/#height +fn eval_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px()) +} + /// https://drafts.csswg.org/mediaqueries-4/#device-width fn eval_device_width(context: &Context) -> CSSPixelLength { let device = context.device(); @@ -29,6 +34,11 @@ fn eval_device_height(context: &Context) -> CSSPixelLength { CSSPixelLength::new(scaled.height) } +/// https://drafts.csswg.org/mediaqueries-4/#orientation +fn eval_orientation(context: &Context, value: Option) -> bool { + Orientation::eval(context.device().au_viewport_size(), value) +} + #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Scan { @@ -150,12 +160,24 @@ fn eval_aspect_ratio(context: &Context) -> Ratio { } /// A list with all the media features that Servo supports. -pub static MEDIA_FEATURES: [QueryFeatureDescription; 13] = [ +pub static MEDIA_FEATURES: [QueryFeatureDescription; 15] = [ feature!( atom!("width"), AllowsRanges::Yes, Evaluator::Length(eval_width), - FeatureFlags::empty(), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::VIEWPORT_DEPENDENT, ), feature!( atom!("pointer"), From e0bcd28a1f1a0b35903b4a9b7652c6b993a26ccc Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Wed, 1 Jul 2026 05:21:18 -0700 Subject: [PATCH 26/29] Bump stylo to 0.19.0, selectors to 0.39.0, to_shmem to 0.5.0 (#408) Fixes: #407 Signed-off-by: Oriol Brufau --- Cargo.toml | 20 ++++++++++---------- selectors/Cargo.toml | 2 +- to_shmem/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5302b7e107..617ba02dc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,20 +17,20 @@ members = [ default-members = ["style"] [workspace.package] -version = "0.18.0" +version = "0.19.0" [workspace.dependencies] # in-repo dependencies (separately versioned) servo_arc = { version = "0.4.3", path = "./servo_arc" } -selectors = { version = "0.38.0", path = "./selectors" } -to_shmem = { version = "0.4.0", path = "./to_shmem", features = ["servo"] } +selectors = { version = "0.39.0", path = "./selectors" } +to_shmem = { version = "0.5.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.18.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } -static_prefs = { version = "0.18.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } -stylo_atoms = { version = "0.18.0", path = "./stylo_atoms" } -dom = { version = "0.18.0", path = "./stylo_dom", package = "stylo_dom" } -style_traits = { version = "0.18.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} -style_derive = { version = "0.18.0", path = "./style_derive", package = "stylo_derive"} -stylo = { version = "0.18.0", path = "./style" } +malloc_size_of = { version = "0.19.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } +static_prefs = { version = "0.19.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } +stylo_atoms = { version = "0.19.0", path = "./stylo_atoms" } +dom = { version = "0.19.0", path = "./stylo_dom", package = "stylo_dom" } +style_traits = { version = "0.19.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} +style_derive = { version = "0.19.0", path = "./style_derive", package = "stylo_derive"} +stylo = { version = "0.19.0", path = "./style" } diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml index de27b2e60e..3611efe0c1 100644 --- a/selectors/Cargo.toml +++ b/selectors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selectors" -version = "0.38.0" +version = "0.39.0" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/selectors/" description = "CSS Selectors matching for Rust" diff --git a/to_shmem/Cargo.toml b/to_shmem/Cargo.toml index 1f0aa8a5df..21c89aa2d8 100644 --- a/to_shmem/Cargo.toml +++ b/to_shmem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "to_shmem" -version = "0.4.0" +version = "0.5.0" authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" From bc25ec938fa224410eebf21a8963074bc8d8126d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 1 Jul 2026 17:27:32 +0100 Subject: [PATCH 27/29] Enable the `touch-action` property for Servo builds of Stylo (#409) I'm implementing this in Blitz, so I would like to enable it here if possible. Should be low cost/risk for Servo as it's just `u8` bitflags. Servo PR: https://github.com/servo/servo/pull/46159 Signed-off-by: Nico Burns --- style/properties/longhands.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/properties/longhands.toml b/style/properties/longhands.toml index bd8f336d6f..93e2eb6242 100644 --- a/style/properties/longhands.toml +++ b/style/properties/longhands.toml @@ -2068,7 +2068,7 @@ affects = "layout" type = "TouchAction" initial = "computed::TouchAction::auto()" struct = "box" -engine = "gecko" +servo_pref = "layout.unimplemented" animation_type = "discrete" spec = "https://compat.spec.whatwg.org/#touch-action" affects = "paint" From 8e4f4edd7f48f6cb3ca5fc234557040e30650ddb Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 2 Jul 2026 14:09:13 +0100 Subject: [PATCH 28/29] Add a setter for the media_type to Device Signed-off-by: Nico Burns --- style/device/servo.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/style/device/servo.rs b/style/device/servo.rs index 65641e689f..cf241e889c 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -260,6 +260,15 @@ impl Device { .query_font_metrics(vertical, font, base_size, flags) } + /// Set the media type on this [`Device`]. + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_media_type(&mut self, media_type: MediaType) { + self.extra.media_type = media_type; + } + /// Return the media type of the current device. pub fn media_type(&self) -> MediaType { self.extra.media_type.clone() From bbfa9ca5d04ca67c07c2523c55596a711ecfc4e2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 2 Jul 2026 14:39:09 +0100 Subject: [PATCH 29/29] Group media data into a struct --- style/device/servo.rs | 111 ++++++++++++++++++++-------------- style/servo/media_features.rs | 2 +- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/style/device/servo.rs b/style/device/servo.rs index cf241e889c..38ce1a054c 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -53,44 +53,44 @@ pub trait FontMetricsProvider: Debug + Sync { #[derive(Debug, MallocSizeOf)] pub(super) struct ExtraDeviceData { + /// Data which Stylo uses to evaluate media queries + media_data: MediaData, + /// An implementation of a trait which implements support for querying font metrics. + #[ignore_malloc_size_of = "Owned by embedder"] + font_metrics_provider: Box, +} + +#[derive(Debug, MallocSizeOf, Clone, PartialEq)] +/// Data which Stylo uses to evaluate media queries +pub struct MediaData { /// The current media type used by de device. - media_type: MediaType, + pub media_type: MediaType, /// The current viewport size, in CSS pixels. - viewport_size: Size2D, + pub viewport_size: Size2D, /// The current screen size, in device pixels. - device_size: Size2D, + pub device_size: Size2D, /// The current device pixel ratio, from CSS pixels to device pixels. - device_pixel_ratio: Scale, + pub device_pixel_ratio: Scale, /// The current quirks mode. #[ignore_malloc_size_of = "Pure stack type"] - quirks_mode: QuirksMode, + pub quirks_mode: QuirksMode, /// Whether the user prefers light mode or dark mode #[ignore_malloc_size_of = "Pure stack type"] - prefers_color_scheme: PrefersColorScheme, + pub prefers_color_scheme: PrefersColorScheme, /// The capabilities of the primary pointer input #[ignore_malloc_size_of = "Pure stack type"] - primary_pointer_capabilities: PointerCapabilities, + pub primary_pointer_capabilities: PointerCapabilities, /// The union of the capabilities of all pointer inputs #[ignore_malloc_size_of = "Pure stack type"] - all_pointer_capabilities: PointerCapabilities, - /// An implementation of a trait which implements support for querying font metrics. - #[ignore_malloc_size_of = "Owned by embedder"] - font_metrics_provider: Box, + pub all_pointer_capabilities: PointerCapabilities, } impl Device { /// Trivially construct a new `Device`. pub fn new( - media_type: MediaType, - quirks_mode: QuirksMode, - viewport_size: Size2D, - device_size: Size2D, - device_pixel_ratio: Scale, - font_metrics_provider: Box, default_values: Arc, - prefers_color_scheme: PrefersColorScheme, - primary_pointer_capabilities: PointerCapabilities, - all_pointer_capabilities: PointerCapabilities, + font_metrics_provider: Box, + media_data: MediaData, ) -> Device { let root_style = RwLock::new(Arc::clone(&default_values)); Device { @@ -111,14 +111,7 @@ impl Device { default_values, body_text_color: AtomicU32::new(AbsoluteColor::BLACK.to_nscolor()), extra: ExtraDeviceData { - media_type, - viewport_size, - device_size, - device_pixel_ratio, - quirks_mode, - prefers_color_scheme, - primary_pointer_capabilities, - all_pointer_capabilities, + media_data, font_metrics_provider, }, } @@ -144,7 +137,7 @@ impl Device { /// Get the quirks mode of the current device. pub fn quirks_mode(&self) -> QuirksMode { - self.extra.quirks_mode + self.extra.media_data.quirks_mode } /// Gets the base size given a generic font family. @@ -160,9 +153,33 @@ impl Device { true } + /// Get the [`MediaData`] associated with this [`Device`]. + #[inline] + pub fn media_data(&self) -> &MediaData { + &self.extra.media_data + } + + /// Mutate the [`MediaData`] on this [`Device`]. + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn media_data_mut(&mut self) -> &mut MediaData { + &mut self.extra.media_data + } + + /// Set the [`MediaData`] on this [`Device`]. + /// + /// Note that this does not update any associated `Stylist`. For this you must call + /// `Stylist::media_features_change_changed_style` and + /// `Stylist::force_stylesheet_origins_dirty`. + pub fn set_media_data(&mut self, media_data: MediaData) { + self.extra.media_data = media_data; + } + /// Get the viewport size on this [`Device`]. pub fn viewport_size(&self) -> Size2D { - self.extra.viewport_size + self.extra.media_data.viewport_size } /// Set the viewport size on this [`Device`]. @@ -171,7 +188,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.extra.viewport_size = viewport_size; + self.extra.media_data.viewport_size = viewport_size; } /// Returns the viewport size of the current device in app units, needed, @@ -179,8 +196,8 @@ impl Device { #[inline] pub fn au_viewport_size(&self) -> UntypedSize2D { Size2D::new( - Au::from_f32_px(self.extra.viewport_size.width), - Au::from_f32_px(self.extra.viewport_size.height), + Au::from_f32_px(self.extra.media_data.viewport_size.width), + Au::from_f32_px(self.extra.media_data.viewport_size.height), ) } @@ -197,17 +214,17 @@ impl Device { /// 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.extra.device_pixel_ratio.0) as i32 + (AU_PER_PX as f32 / self.extra.media_data.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.extra.device_pixel_ratio + self.extra.media_data.device_pixel_ratio } /// Returns the device pixel ratio. pub fn device_pixel_ratio(&self) -> Scale { - self.extra.device_pixel_ratio + self.extra.media_data.device_pixel_ratio } /// Set a new device pixel ratio on this [`Device`]. @@ -219,7 +236,7 @@ impl Device { &mut self, device_pixel_ratio: Scale, ) { - self.extra.device_pixel_ratio = device_pixel_ratio; + self.extra.media_data.device_pixel_ratio = device_pixel_ratio; } /// Set the device size on this [`Device`] (e.g. the available screen dimensions). @@ -228,13 +245,13 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_device_size(&mut self, device_size: Size2D) { - self.extra.device_size = device_size; + self.extra.media_data.device_size = device_size; } /// Returns the screen size of the current device in app units. #[inline] pub fn device_size(&self) -> Size2D { - self.extra.device_size + self.extra.media_data.device_size } /// Gets the size of the scrollbar in CSS pixels. @@ -266,12 +283,12 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_media_type(&mut self, media_type: MediaType) { - self.extra.media_type = media_type; + self.extra.media_data.media_type = media_type; } /// Return the media type of the current device. pub fn media_type(&self) -> MediaType { - self.extra.media_type.clone() + self.extra.media_data.media_type.clone() } /// Returns whether document colors are enabled. @@ -295,12 +312,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.extra.prefers_color_scheme = new_color_scheme; + self.extra.media_data.prefers_color_scheme = new_color_scheme; } /// Returns the color scheme of this [`Device`]. pub fn color_scheme(&self) -> PrefersColorScheme { - self.extra.prefers_color_scheme + self.extra.media_data.prefers_color_scheme } /// Set the [`PointerCapbabilities`] value for the primary pointer on this [`Device`] @@ -309,12 +326,12 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_primary_pointer_capabilities(&mut self, capabilities: PointerCapabilities) { - self.extra.primary_pointer_capabilities = capabilities; + self.extra.media_data.primary_pointer_capabilities = capabilities; } /// Returns the pointer capabilities of this [`Device`]. pub fn primary_pointer_capabilities(&self) -> PointerCapabilities { - self.extra.primary_pointer_capabilities + self.extra.media_data.primary_pointer_capabilities } /// Set the [`PointerCapbabilities`] value for all pointers on this [`Device`] @@ -323,12 +340,12 @@ impl Device { /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_all_pointer_capabilities(&mut self, capabilities: PointerCapabilities) { - self.extra.all_pointer_capabilities = capabilities; + self.extra.media_data.all_pointer_capabilities = capabilities; } /// Returns the pointer capabilities of this [`Device`]. pub fn all_pointer_capabilities(&self) -> PointerCapabilities { - self.extra.all_pointer_capabilities + self.extra.media_data.all_pointer_capabilities } pub(crate) fn is_dark_color_scheme(&self, _: ColorSchemeFlags) -> bool { diff --git a/style/servo/media_features.rs b/style/servo/media_features.rs index 9e5a77e82a..14a3e520c9 100644 --- a/style/servo/media_features.rs +++ b/style/servo/media_features.rs @@ -72,7 +72,7 @@ fn eval_prefers_color_scheme(context: &Context, query_value: Option