Merge branch 'main' into xpath-lang

* main: (510 commits)
  DevTools: Fix empty `debugger > source` panel (#37197)
  dom: implement signal abort on controller and signal (#37192)
  build(deps): bump parking_lot from 0.12.3 to 0.12.4 (#37199)
  layout: Split overflow calculation after fragment tree construction (#37203)
  build(deps): bump parking_lot_core from 0.9.10 to 0.9.11 (#37202)
  build(deps): bump lock_api from 0.4.12 to 0.4.13 (#37201)
  build(deps): bump cc from 1.2.24 to 1.2.25 (#37198)
  Constellation can now optionally report memory usage when the page is loaded. (#37151)
  Implement Input `type=text` UA Shadow DOM (#37065)
  constellation: Wait for canvas thread to shut down before shutting down system font service (#37182)
  Add slot default display style test (#37189)
  Send synthetic keydown/keyup at ime_insert_text (#37175)
  script: Let canvas serialization to image fail gracefully (#37184)
  Implement basics of link preloading (#37036)
  compositor: Add an initial RefreshDriver (#37169)
  pixels: Add limitation to max image total bytes length (#37172)
  Chore: Remove unused variable in `transition-zero-duration-with-delay.html` (#37179)
  build(deps): bump ohos-ime from 0.2.0 to 0.3.0 (#37180)
  Add a user agent style for the `<slot>` element (#37174)
  build(deps): bump hitrace from 0.1.4 to 0.1.5 (#37170)
  ...
This commit is contained in:
Ville Lindholm 2025-06-01 11:52:33 +03:00
commit a1ed93d313
No known key found for this signature in database
6167 changed files with 194943 additions and 462433 deletions

18
.flake8
View file

@ -1,18 +0,0 @@
[flake8]
ignore =
# trailing whitespace; the standard tidy process will enforce no trailing whitespace
W291,
# linebreak before binary operator; replaced by W504 - linebreak after binary operator
W503,
# 80 character line length; the standard tidy process will enforce line length
E501
exclude =
# temporary local files
target
__pycache__
python/_venv*
# upstream
third_party
python/mach
components
tests

1
.github/CODEOWNERS vendored
View file

@ -11,7 +11,6 @@
/components/compositing @mrobinson
# Reviewers for layout-related code
/components/layout_2020 @mrobinson @Loirooriol @nicoburns
/components/layout @mrobinson @Loirooriol @nicoburns
# Reviewers for Minibrowser related code

View file

@ -31,6 +31,9 @@ updates:
servo-media-related:
patterns:
- "servo-media*"
objc2-related:
patterns:
- "objc2*"
ignore:
# Ignore all stylo crates as their upgrades are coordinated via companion PRs.
- dependency-name: selectors

View file

@ -8,12 +8,18 @@ on:
profile:
required: true
type: string
build-args:
required: true
type: string
wpt-args:
required: true
type: string
wpt:
required: true
type: boolean
number-of-wpt-chunks:
required: true
type: number
unit-tests:
required: true
type: boolean
@ -32,6 +38,7 @@ jobs:
secrets: inherit
with:
profile: ${{ inputs.profile }}
build-args: ${{ inputs.build-args }}
unit-tests: ${{ inputs.unit-tests }}
build-libservo: ${{ inputs.build-libservo }}
bencher: ${{ inputs.bencher }}
@ -43,6 +50,7 @@ jobs:
secrets: inherit
with:
profile: ${{ inputs.profile }}
build-args: ${{ inputs.build-args }}
wpt: ${{ inputs.wpt }}
unit-tests: ${{ inputs.unit-tests }}
build-libservo: ${{ inputs.build-libservo }}
@ -56,7 +64,9 @@ jobs:
secrets: inherit
with:
profile: ${{ inputs.profile }}
build-args: ${{ inputs.build-args }}
wpt: ${{ inputs.wpt }}
number-of-wpt-chunks: ${{ inputs.number-of-wpt-chunks }}
unit-tests: ${{ inputs.unit-tests }}
build-libservo: ${{ inputs.build-libservo }}
wpt-args: ${{ inputs.wpt-args }}

View file

@ -13,6 +13,10 @@ on:
default: false
required: false
type: boolean
number-of-wpt-chunks:
default: 20
required: false
type: number
env:
RUST_BACKTRACE: 1
@ -24,15 +28,25 @@ env:
WPT_ALWAYS_SUCCEED_ARG: "${{ inputs.wpt-sync-from-upstream && '--always-succeed' || '' }}"
jobs:
chunks:
name: Generate chunks array
runs-on: ubuntu-22.04
outputs:
chunks-array: ${{ steps.generate-chunks-array.outputs.result }}
steps:
- uses: actions/github-script@v7
id: generate-chunks-array
with:
script: |
return Array.from({length: ${{ inputs.number-of-wpt-chunks }}}, (_, i) => i + 1)
linux-wpt:
name: WPT
runs-on: ubuntu-22.04
env:
max_chunk_id: 20
needs: chunks
strategy:
fail-fast: false
matrix:
chunk_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
chunk_id: ${{ fromJson(needs.chunks.outputs.chunks-array) }}
steps:
- uses: actions/checkout@v4
if: github.event_name != 'pull_request_target'
@ -66,7 +80,7 @@ jobs:
./mach test-wpt \
$WPT_ALWAYS_SUCCEED_ARG \
--${{ inputs.profile }} --processes $(nproc) --timeout-multiplier 2 \
--total-chunks ${{ env.max_chunk_id }} --this-chunk ${{ matrix.chunk_id }} \
--total-chunks ${{ inputs.number-of-wpt-chunks }} --this-chunk ${{ matrix.chunk_id }} \
--log-raw wpt-full-logs/linux/raw/${{ matrix.chunk_id }}.log \
--log-wptreport wpt-full-logs/linux/wptreport/${{ matrix.chunk_id }}.json \
--log-raw-unexpected wpt-filtered-logs/linux/${{ matrix.chunk_id }}.log \

View file

@ -6,6 +6,10 @@ on:
required: false
default: "release"
type: string
build-args:
default: ""
required: false
type: string
wpt-args:
default: ""
required: false
@ -17,6 +21,10 @@ on:
wpt:
required: false
type: boolean
number-of-wpt-chunks:
default: 20
required: false
type: number
unit-tests:
required: false
default: false
@ -58,6 +66,10 @@ on:
wpt:
required: false
type: boolean
number-of-wpt-chunks:
default: 20
required: false
type: number
unit-tests:
required: false
default: false
@ -90,10 +102,10 @@ jobs:
uses: ./.github/workflows/self-hosted-runner-select.yml
secrets: inherit
with:
# Ubuntu 22.04 has glibc 2.34 so the binaries produced
# won't run on systems with older glibc e.g wpt.fyi
# runners which still use 20.04.
github-hosted-runner-label: ubuntu-${{ inputs.upload && '20.04' || '22.04' }}
# Before updating the GH action runner image for the nightly job, ensure
# that the system has a glibc version that is compatible with the one
# used by the wpt.fyi runners.
github-hosted-runner-label: ubuntu-22.04
self-hosted-image-name: servo-ubuntu2204
force-github-hosted-runner: ${{ inputs.upload || inputs.force-github-hosted-runner }}
runner-timeout:
@ -138,16 +150,8 @@ jobs:
tool-cache: false
large-packages: false
swap-storage: false
- name: Install LLVM and Clang
# Expliclity install Clang 14 on Ubuntu 20.04 used by the nightly job (#34713).
if: ${{ runner.environment != 'self-hosted' && inputs.upload }}
uses: KyleMayes/install-llvm-action@v2
with:
version: 14
# FIXME: It would be better to use the above step for regular Linux jobs
# and remove the following step, but currenty that is failing and needs investigation.
- name: Set LIBCLANG_PATH env # needed for bindgen in mozangle
if: ${{ runner.environment != 'self-hosted' && !inputs.upload }} # not needed on ubuntu 20.04 used for nightly
if: ${{ runner.environment != 'self-hosted' }}
run: echo "LIBCLANG_PATH=/usr/lib/llvm-14/lib" >> $GITHUB_ENV
- name: Setup Python
if: ${{ runner.environment != 'self-hosted' }}
@ -166,7 +170,7 @@ jobs:
- name: Build (${{ inputs.profile }})
run: |
./mach build --use-crown --locked --${{ inputs.profile }}
./mach build --use-crown --locked --${{ inputs.profile }} ${{ inputs.build-args }}
cp -r target/cargo-timings target/cargo-timings-linux
- name: Smoketest
run: xvfb-run ./mach smoketest --${{ inputs.profile }}
@ -223,6 +227,7 @@ jobs:
wpt-args: ${{ inputs.wpt-args }}
profile: ${{ inputs.profile }}
wpt-sync-from-upstream: ${{ inputs.wpt-sync-from-upstream }}
number-of-wpt-chunks: ${{ inputs. number-of-wpt-chunks }}
secrets: inherit
bencher:

View file

@ -7,6 +7,10 @@ on:
required: false
default: "release"
type: string
build-args:
default: ""
required: false
type: string
wpt-args:
default: ""
required: false
@ -146,7 +150,7 @@ jobs:
brew install gnu-tar
- name: Build (${{ inputs.profile }})
run: |
./mach build --use-crown --locked --${{ inputs.profile }}
./mach build --use-crown --locked --${{ inputs.profile }} ${{ inputs.build-args }}
cp -r target/cargo-timings target/cargo-timings-macos
- name: Smoketest
uses: nick-fields/retry@v3

View file

@ -56,6 +56,8 @@ jobs:
unit-tests: ${{ matrix.unit_tests }}
build-libservo: ${{ matrix.build_libservo }}
wpt-args: ${{ matrix.wpt_args }}
build-args: ${{ matrix.build_args }}
number-of-wpt-chunks: ${{ matrix.number_of_wpt_chunks }}
bencher: ${{ matrix.bencher }}
build-result:

View file

@ -36,6 +36,7 @@ env:
RUST_BACKTRACE: 1
SHELL: /bin/bash
CARGO_INCREMENTAL: 0
BENCHER_PROJECT: ${{ vars.BENCHER_PROJECT || 'servo' }}
jobs:
build:
@ -168,11 +169,17 @@ jobs:
- name: Build for aarch64 HarmonyOS
run: |
./mach build --locked --target aarch64-unknown-linux-ohos --profile=${{ inputs.profile }} --flavor=harmonyos --no-default-features --features tracing,tracing-hitrace
- name: Upload supprt/hitrace-bencher/runs.json
uses: actions/upload-artifact@v4
with:
name: runs.json
path: support/hitrace-bencher/runs.json
overwrite: true
- uses: actions/upload-artifact@v4
with:
# Upload the **unsigned** artifact - We don't have the signing materials in pull request workflows
path: target/openharmony/aarch64-unknown-linux-ohos/${{ inputs.profile }}/entry/build/harmonyos/outputs/default/servoshell-default-unsigned.hap
name: servoshell-hos-${{ inputs.profile }}.hap
path: target/openharmony/aarch64-unknown-linux-ohos/${{ inputs.profile }}/entry/build/harmonyos/outputs/default/servoshell-default-unsigned.hap
test-harmonyos-aarch64:
@ -223,10 +230,8 @@ jobs:
hdc shell snapshot_display -f /data/local/tmp/servo.jpeg
hdc file recv /data/local/tmp/servo.jpeg test_output/servo_hos_screenshot.jpeg
hdc file recv /data/local/tmp/ohtrace.txt test_output/servo.ftrace
# To limit the logsize we only save logs from servo.
# Todo: investigate giving servo a custom domain tag, so we can also log appspawn etc,
# since another common error might be the dynamic loader failing to relocate libservoshell.so
hdc shell hilog --exit --pid=${servo_pid} > test_output/servo.log
# To limit the logsize we only save logs from servo.
hdc shell hilog --exit -D 0xE0C3 > test_output/servo.log
# todo: Also benchmark some other websites....
- name: Upload artifacts
uses: actions/upload-artifact@v4
@ -240,3 +245,19 @@ jobs:
[[ $servo_pid =~ ^[0-9]+$ ]] || { echo "It looks like servo crashed!" ; exit 1; }
# If the grep fails, then the trace output for the "page loaded" prompt is missing
grep 'org\.servo\.servo-.* tracing_mark_write.*PageLoadEndedPrompt' test_output/servo.ftrace
- name: Getting runs file
uses: actions/download-artifact@v4
with:
# Name of the artifact to download.
# If unspecified, all artifacts for the run are downloaded.
name: runs.json
- name: "Run benchmark"
run: hitrace-bench -r runs.json
- name: Getting bencher
uses: bencherdev/bencher@main
- name: Getting model name
run: |
echo "MODEL_NAME=$(hdc bugreport | head -n 20 | grep MarketName | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}' -)" >> $GITHUB_ENV
- name: Uploading to bencher.dev
run: |
bencher run --adapter json --file bench.json --project '${{ env.BENCHER_PROJECT }}' --token '${{ secrets.BENCHER_API_TOKEN }}' --github-actions '${{ secrets.GITHUB_TOKEN }}' --testbed="$MODEL_NAME"

View file

@ -128,6 +128,8 @@ jobs:
unit-tests: ${{ matrix.unit_tests }}
build-libservo: ${{ matrix.build_libservo }}
wpt-args: ${{ matrix.wpt_args }}
build-args: ${{ matrix.build_args }}
number-of-wpt-chunks: ${{ matrix.number_of_wpt_chunks }}
bencher: ${{ matrix.bencher }}
results:

View file

@ -10,6 +10,10 @@ on:
default: "release"
type: choice
options: ["release", "debug", "production"]
build-args:
default: ""
required: false
type: string
wpt-args:
default: ""
required: false
@ -79,6 +83,7 @@ jobs:
// WPT-related overrides only affect Linux currently, as tests don't run by default on other platforms.
configuration.matrix[0].wpt = Boolean(${{ inputs.wpt }});
configuration.matrix[0].wpt_args = "${{ inputs.wpt-args }}" || "";
configuration.matrix[0].build_args = "${{ inputs.build-args }}" || "";
let unit_tests = Boolean(${{ inputs.unit-tests }});
let profile = '${{ inputs.profile }}';
@ -107,7 +112,9 @@ jobs:
profile: ${{ matrix.profile }}
unit-tests: ${{ matrix.unit_tests }}
build-libservo: ${{ matrix.build_libservo }}
build-args: ${{ matrix.build_args }}
wpt-args: ${{ matrix.wpt_args }}
number-of-wpt-chunks: ${{ matrix.number_of_wpt_chunks }}
bencher: ${{ matrix.bencher }}
build-result:

View file

@ -7,6 +7,10 @@ on:
required: false
default: "release"
type: string
build-args:
default: ""
required: false
type: string
unit-tests:
required: false
default: false
@ -161,7 +165,7 @@ jobs:
- name: Build (${{ inputs.profile }})
run: |
.\mach build --use-crown --locked --${{ inputs.profile }}
.\mach build --use-crown --locked --${{ inputs.profile }} ${{ inputs.build-args }}
cp C:\a\servo\servo\target\cargo-timings C:\a\servo\servo\target\cargo-timings-windows -Recurse
- name: Copy resources
if: ${{ runner.environment != 'self-hosted' }}

3
.gitignore vendored
View file

@ -57,6 +57,9 @@ webrender-captures/
Session.vim
Sessionx.vim
# Zed
/.zed
/unminified-js
/unminified-css

View file

@ -16,6 +16,8 @@
// IDL language support
"mythmon.idl",
// TOML files
"tamasfe.even-better-toml"
"tamasfe.even-better-toml",
// Python files
"charliermarsh.ruff"
]
}

1550
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ publish = false
rust-version = "1.85.0"
[workspace.dependencies]
accountable-refcell = "0.2.0"
accountable-refcell = "0.2.2"
aes = "0.8.4"
aes-gcm = "0.10.3"
aes-kw = { version = "0.2.1", features = ["alloc"] }
@ -41,7 +41,7 @@ chrono = { version = "0.4", features = ["serde"] }
cipher = { version = "0.4.4", features = ["alloc"] }
compositing_traits = { path = "components/shared/compositing" }
constellation_traits = { path = "components/shared/constellation" }
content-security-policy = { version = "0.5", features = ["serde"] }
content-security-policy = { git = "https://github.com/servo/rust-content-security-policy/", branch = "servo-csp", features = ["serde"] }
cookie = { package = "cookie", version = "0.18" }
crossbeam-channel = "0.5"
cssparser = { version = "0.35", features = ["serde"] }
@ -70,8 +70,8 @@ gstreamer-sys = "0.23"
gstreamer-video = "0.23"
harfbuzz-sys = "0.6.1"
headers = "0.4"
hitrace = "0.1.4"
html5ever = "0.30"
hitrace = "0.1.5"
html5ever = "0.31"
http = "1.3"
http-body-util = "0.1"
hyper = "1.6"
@ -82,9 +82,9 @@ icu_locid = "1.5.0"
icu_segmenter = "1.5.0"
image = "0.24"
imsz = "0.2"
indexmap = { version = "2.8.0", features = ["std"] }
indexmap = { version = "2.9.0", features = ["std"] }
ipc-channel = "0.19"
itertools = "0.13"
itertools = "0.14"
js = { package = "mozjs", git = "https://github.com/servo/mozjs" }
keyboard-types = "0.7"
libc = "0.2"
@ -92,7 +92,7 @@ log = "0.4"
mach2 = "0.4"
malloc_size_of = { package = "servo_malloc_size_of", path = "components/malloc_size_of" }
malloc_size_of_derive = "0.1"
markup5ever = "0.15"
markup5ever = "0.16.1"
memmap2 = "0.9.5"
mime = "0.3.13"
mime_guess = "2.0.5"
@ -113,32 +113,35 @@ rand_isaac = "0.3"
raw-window-handle = "0.6"
rayon = "1"
regex = "1.11"
resvg = "0.45.0"
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }
rustls-pemfile = "2.0"
rustls-pki-types = "1.11"
rustls-pki-types = "1.12"
script_layout_interface = { path = "components/shared/script_layout" }
script_traits = { path = "components/shared/script" }
selectors = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
selectors = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
serde = "1.0.219"
serde_bytes = "0.11"
serde_json = "1.0"
servo-media = { git = "https://github.com/servo/media" }
servo-media-dummy = { git = "https://github.com/servo/media" }
servo-media-gstreamer = { git = "https://github.com/servo/media" }
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
servo-tracing = { path = "components/servo_tracing" }
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
smallbitvec = "2.6.0"
smallvec = "1.14"
smallvec = "1.15"
snapshot = { path = "./components/shared/snapshot" }
static_assertions = "1.1"
string_cache = "0.8"
string_cache_codegen = "0.5"
strum = "0.26"
strum_macros = "0.26"
stylo = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
stylo = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] }
syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] }
synstructure = "0.13"
@ -160,22 +163,22 @@ unicode-properties = { version = "0.1.3", features = ["emoji"] }
unicode-script = "0.5"
unicode-segmentation = "1.12.0"
url = "2.5"
urlpattern = "0.3"
uuid = { version = "1.12.1", features = ["v4"] }
webdriver = "0.51.0"
webdriver = "0.53.0"
webgpu_traits = { path = "components/shared/webgpu" }
webpki-roots = "0.26"
webrender = { git = "https://github.com/servo/webrender", branch = "0.66", features = ["capture"] }
webrender_api = { git = "https://github.com/servo/webrender", branch = "0.66" }
webrender_traits = { path = "components/shared/webrender" }
webrender = { git = "https://github.com/servo/webrender", branch = "0.67", features = ["capture"] }
webrender_api = { git = "https://github.com/servo/webrender", branch = "0.67" }
webxr-api = { path = "components/shared/webxr" }
wgpu-core = { git = "https://github.com/gfx-rs/wgpu", rev = "2f255edc60e9669c8c737464c59af10d59a31126" }
wgpu-types = { git = "https://github.com/gfx-rs/wgpu", rev = "2f255edc60e9669c8c737464c59af10d59a31126" }
wgpu-core = "25"
wgpu-types = "25"
winapi = "0.3"
windows-sys = "0.59"
wio = "0.2"
wr_malloc_size_of = { git = "https://github.com/servo/webrender", branch = "0.66" }
wr_malloc_size_of = { git = "https://github.com/servo/webrender", branch = "0.67" }
xi-unicode = "0.3.0"
xml5ever = "0.21"
xml5ever = "0.22"
[profile.release]
opt-level = 3
@ -242,3 +245,7 @@ codegen-units = 1
#
# [patch."https://github.com/servo/<repository>"]
# <crate> = { path = "/path/to/local/checkout" }
#
# [patch."https://github.com/servo/rust-content-security-policy"]
# content-security-policy = { path = "../rust-content-security-policy/" }
# content-security-policy = { git = "https://github.com/timvdlippe/rust-content-security-policy/", branch = "fix-report-checks", features = ["serde"] }

View file

@ -4,7 +4,15 @@ Servo is a prototype web browser engine written in the
[Rust](https://github.com/rust-lang/rust) language. It is currently developed on
64-bit macOS, 64-bit Linux, 64-bit Windows, 64-bit OpenHarmony, and Android.
Servo welcomes contribution from everyone. Check out [The Servo Book](https://book.servo.org) to get started, or go to [servo.org](https://servo.org/) for news and guides.
Servo welcomes contribution from everyone. Check out:
- The [Servo Book](https://book.servo.org) for documentation
- [servo.org](https://servo.org/) for news and guides
Coordination of Servo development happens:
- Here in the Github Issues
- On the [Servo Zulip](https://servo.zulipchat.com/)
- In video calls advertised in the [Servo Project](https://github.com/servo/project/issues) repo.
## Getting started

View file

@ -1,4 +1,4 @@
# Security Policy
Given that Servo does not yet have customers or products, we are comfortable accepting the security related vulnerabilities as a [new GitHub issue](https://github.com/servo/servo/security/advisories/new) for now.
Given that Servo does not yet have customers or products, we are comfortable accepting the security related issues as [GitHub security reports](https://github.com/servo/servo/security/advisories/new) for now.

View file

@ -1,55 +1,165 @@
# Bluetooth Rust lib using macOS CoreBluetooth
# Bluetooth
[![Build Status](https://travis-ci.org/akosthekiss/blurmac.svg?branch=master)](https://travis-ci.org/akosthekiss/blurmac)
[![Crates.io](https://img.shields.io/crates/v/blurmac.svg)](https://crates.io/crates/blurmac)
Servo-specific APIs to access Bluetooth devices.
The main aim of BlurMac is to enable [WebBluetooth](https://webbluetoothcg.github.io)
in [Servo](https://github.com/servo/servo) on macOS. Thus, API and implementation
decisions are affected by the encapsulating [Devices](https://github.com/servo/devices),
and the sibling [BlurZ](https://github.com/szeged/blurz) and [BlurDroid](https://github.com/szeged/blurdroid)
crates.
Bluetooth related code is located in `bluetooth.rs`.
### Implementation
## Run Servo with WebBluetooth Enabled
Underlying dependency crates:
Usually, you don't want to work with BlurMac on its own but use it within Servo.
So, most probably you'll want to run Servo with WebBluetooth enabled:
- Android platform: [blurdroid](https://crates.io/crates/blurdroid)
- Linux platform: [blurz](https://crates.io/crates/blurz)
- macOS platform: [blurmac](https://crates.io/crates/blurmac)
- `Fake` prefixed structures: [blurmock](https://crates.io/crates/blurmock)
```
RUST_LOG=blurmac \
./mach run \
--dev \
--pref=dom.bluetooth.enabled \
--pref=dom.permissions.testing.allowed_in_nonsecure_contexts \
URL
`Empty` prefixed structures are located in `empty.rs`.
### Usage
#### Without the *bluetooth-test* feature
There are three supported platforms (Android, Linux, macOS), on other platforms we fall back to a default (`Empty` prefixed) implementation. Each enum (`BluetoothAdapter`, `BluetoothDevice`, etc.) will contain only one variant for each targeted platform. See the following `BluetoothAdapter` example:
Android:
```rust
pub enum BluetoothAdapter {
Android(Arc<BluetoothAdapterAndroid>),
}
```
Notes:
* The above command is actually not really BlurMac-specific (except for the `RUST_LOG`
part). It runs Servo with WBT enabled on any platform where WBT is supported.
* You don't need the `RUST_LOG=blurmac` part if you don't want to see BlurMac debug
messages on the console.
* You don't need the `--dev` part if you want to run a release build.
* You don't need the `--pref=dom.permissions.testing.allowed_in_nonsecure_contexts`
part if your `URL` is https (but you do need it if you test a local file).
Linux:
```rust
pub enum BluetoothAdapter {
Bluez(Arc<BluetoothAdapterBluez>),
}
```
## Known Issues
macOS:
* Device RSSI can not be retrieved yet.
* Support for included services is incomplete.
* Descriptors are not supported yet.
* Notifications on characteristics are not supported yet (the limitation comes from
Devices).
```rust
pub enum BluetoothAdapter {
Mac(Arc<BluetoothAdapterMac>),
}
```
Unsupported platforms:
## Compatibility
```rust
pub enum BluetoothAdapter {
Empty(Arc<BluetoothAdapterEmpty>),
}
```
Tested on:
You will have a platform specific adapter, e.g. on Android target, `BluetoothAdapter::init()` will create a `BluetoothAdapter::Android` enum variant, which wraps an `Arc<BluetoothAdapterAndroid>`.
* macOS Sierra 10.12.
```rust
pub fn init() -> Result<BluetoothAdapter, Box<Error>> {
let blurdroid_adapter = try!(BluetoothAdapterAndroid::get_adapter());
Ok(BluetoothAdapter::Android(Arc::new(blurdroid_adapter)))
}
```
On each platform you can call the same functions to reach the same GATT hierarchy elements. The following code can access the same Bluetooth device on all supported platforms:
## Copyright and Licensing
```rust
use device::{BluetoothAdapter, BluetoothDevice};
Licensed under the BSD 3-Clause [License](LICENSE.md).
fn main() {
// Get the bluetooth adapter.
let adapter = BluetoothAdpater::init().expect("No bluetooth adapter found!");
// Get a device with the id 01:2A:00:4D:00:04 if it exists.
let device = adapter.get_device("01:2A:00:4D:00:04".to_owned() /*device address*/)
.expect("No bluetooth device found!");
}
```
#### With the *bluetooth-test* feature
Each enum (`BluetoothAdapter`, `BluetoothDevice`, etc.) will contain one variant of the three possible default target, and a `Mock` variant, which wraps a `Fake` structure.
Android:
```rust
pub enum BluetoothAdapter {
Android(Arc<BluetoothAdapterAndroid>),
Mock(Arc<FakeBluetoothAdapter>),
}
```
Linux:
```rust
pub enum BluetoothAdapter {
Bluez(Arc<BluetoothAdapterBluez>),
Mock(Arc<FakeBluetoothAdapter>),
}
```
macOS:
```rust
pub enum BluetoothAdapter {
Mac(Arc<BluetoothAdapterMac>),
Mock(Arc<FakeBluetoothAdapter>),
}
```
Unsupported platforms:
```rust
pub enum BluetoothAdapter {
Empty(Arc<BluetoothAdapterEmpty>),
Mock(Arc<FakeBluetoothAdapter>),
}
```
Beside the platform specific structures, you can create and access mock adapters, devices, services etc. These mock structures implements all the platform specific functions too. To create a mock GATT hierarchy, first you need to call the `BluetoothAdapter::init_mock()` function, insted of `BluetoothAdapter::init()`.
```rust
use device::{BluetoothAdapter, BluetoothDevice};
use std::String;
// This function takes a BluetoothAdapter,
// and print the ids of the devices, which the adapter can find.
fn print_device_ids(adapter: &BluetoothAdpater) {
let devices = match adapter.get_devices().expect("No devices on the adapter!");
for device in devices {
println!("{:?}", device.get_id());
}
}
fn main() {
// This code uses a real adapter.
// Get the bluetooth adapter.
let adapter = BluetoothAdpater::init().expect("No bluetooth adapter found!");
// Get a device with the id 01:2A:00:4D:00:04 if it exists.
let device = adapter.get_device("01:2A:00:4D:00:04".to_owned() /*device address*/)
.expect("No bluetooth device found!");
// This code uses a mock adapter.
// Creating a mock adapter.
let mock_adapter = BluetoothAdpater::init_mock().unwrap();
// Creating a mock device.
let mock_device =
BluetoothDevice::create_mock_device(mock_adapter,
"device_id_string_goes_here".to_owned())
.unwrap();
// Changing its device_id.
let new_device_id = String::from("new_device_id_string".to_owned());
mock_device.set_id(new_device_id.clone());
// Calling the get_id function must return the last id we set.
assert_equals!(new_device_id, mock_device.get_id());
// Getting the mock_device with its id
// must return the same mock device object we created before.
assert_equals!(Some(mock_device),
mock_adapter.get_device(new_device_id.clone()).unwrap());
// The print_device_ids function accept real and mock adapters too.
print_device_ids(&adapter);
print_device_ids(&mock_adapter);
}
```
Calling a test function on a not `Mock` structure, will result an error with the message: `Error! Test functions are not supported on real devices!`.

View file

@ -11,37 +11,24 @@ rust-version.workspace = true
name = "canvas"
path = "lib.rs"
[features]
webgl_backtrace = ["canvas_traits/webgl_backtrace"]
webxr = ["dep:webxr", "dep:webxr-api"]
[dependencies]
app_units = { workspace = true }
bitflags = { workspace = true }
byteorder = { workspace = true }
canvas_traits = { workspace = true }
compositing_traits = { workspace = true }
crossbeam-channel = { workspace = true }
cssparser = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
font-kit = "0.14"
fonts = { path = "../fonts" }
glow = { workspace = true }
half = "2"
ipc-channel = { workspace = true }
log = { workspace = true }
lyon_geom = "1.0.4"
net_traits = { workspace = true }
num-traits = { workspace = true }
pixels = { path = "../pixels" }
range = { path = "../range" }
raqote = "0.8.5"
servo_arc = { workspace = true }
snapshot = { workspace = true }
stylo = { workspace = true }
surfman = { workspace = true }
unicode-script = { workspace = true }
webrender = { workspace = true }
webrender_api = { workspace = true }
webrender_traits = { workspace = true }
webxr = { path = "../webxr", features = ["ipc"], optional = true }
webxr-api = { workspace = true, features = ["ipc"], optional = true }

View file

@ -0,0 +1,270 @@
/* 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 canvas_traits::canvas::{
CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle,
};
use euclid::Angle;
use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D};
use lyon_geom::Arc;
use style::color::AbsoluteColor;
use crate::canvas_data::{CanvasPaintState, Filter, TextRun};
pub(crate) trait Backend: Clone + Sized {
type Pattern<'a>: PatternHelpers + Clone;
type StrokeOptions: StrokeOptionsHelpers + Clone;
type Color: Clone;
type DrawOptions: DrawOptionsHelpers + Clone;
type CompositionOp;
type DrawTarget: GenericDrawTarget<Self>;
type PathBuilder: GenericPathBuilder<Self>;
type SourceSurface;
type Bytes<'a>: AsRef<[u8]>;
type Path: PathHelpers<Self> + Clone;
type GradientStop;
type GradientStops;
fn get_composition_op(&self, opts: &Self::DrawOptions) -> Self::CompositionOp;
fn need_to_draw_shadow(&self, color: &Self::Color) -> bool;
fn set_shadow_color(&mut self, color: AbsoluteColor, state: &mut CanvasPaintState<'_, Self>);
fn set_fill_style(
&mut self,
style: FillOrStrokeStyle,
state: &mut CanvasPaintState<'_, Self>,
drawtarget: &Self::DrawTarget,
);
fn set_stroke_style(
&mut self,
style: FillOrStrokeStyle,
state: &mut CanvasPaintState<'_, Self>,
drawtarget: &Self::DrawTarget,
);
fn set_global_composition(
&mut self,
op: CompositionOrBlending,
state: &mut CanvasPaintState<'_, Self>,
);
fn create_drawtarget(&self, size: Size2D<u64>) -> Self::DrawTarget;
fn new_paint_state<'a>(&self) -> CanvasPaintState<'a, Self>;
}
// This defines required methods for a DrawTarget (currently only implemented for raqote). The
// prototypes are derived from the now-removed Azure backend's methods.
pub(crate) trait GenericDrawTarget<B: Backend> {
fn clear_rect(&mut self, rect: &Rect<f32>);
fn copy_surface(
&mut self,
surface: B::SourceSurface,
source: Rect<i32>,
destination: Point2D<i32>,
);
fn create_path_builder(&self) -> B::PathBuilder;
fn create_similar_draw_target(&self, size: &Size2D<i32>) -> Self;
fn create_source_surface_from_data(&self, data: &[u8]) -> Option<B::SourceSurface>;
fn draw_surface(
&mut self,
surface: B::SourceSurface,
dest: Rect<f64>,
source: Rect<f64>,
filter: Filter,
draw_options: &B::DrawOptions,
);
fn draw_surface_with_shadow(
&self,
surface: B::SourceSurface,
dest: &Point2D<f32>,
color: &B::Color,
offset: &Vector2D<f32>,
sigma: f32,
operator: B::CompositionOp,
);
fn fill(&mut self, path: &B::Path, pattern: B::Pattern<'_>, draw_options: &B::DrawOptions);
fn fill_text(
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
pattern: &B::Pattern<'_>,
draw_options: &B::DrawOptions,
);
fn fill_rect(
&mut self,
rect: &Rect<f32>,
pattern: B::Pattern<'_>,
draw_options: Option<&B::DrawOptions>,
);
fn get_size(&self) -> Size2D<i32>;
fn get_transform(&self) -> Transform2D<f32>;
fn pop_clip(&mut self);
fn push_clip(&mut self, path: &B::Path);
fn set_transform(&mut self, matrix: &Transform2D<f32>);
fn stroke(
&mut self,
path: &B::Path,
pattern: B::Pattern<'_>,
stroke_options: &B::StrokeOptions,
draw_options: &B::DrawOptions,
);
fn stroke_line(
&mut self,
start: Point2D<f32>,
end: Point2D<f32>,
pattern: B::Pattern<'_>,
stroke_options: &B::StrokeOptions,
draw_options: &B::DrawOptions,
);
fn stroke_rect(
&mut self,
rect: &Rect<f32>,
pattern: B::Pattern<'_>,
stroke_options: &B::StrokeOptions,
draw_options: &B::DrawOptions,
);
fn surface(&self) -> B::SourceSurface;
fn bytes(&'_ self) -> B::Bytes<'_>;
}
/// A generic PathBuilder that abstracts the interface for azure's and raqote's PathBuilder.
pub(crate) trait GenericPathBuilder<B: Backend> {
fn arc(
&mut self,
origin: Point2D<f32>,
radius: f32,
start_angle: f32,
end_angle: f32,
anticlockwise: bool,
) {
Self::ellipse(
self,
origin,
radius,
radius,
0.,
start_angle,
end_angle,
anticlockwise,
);
}
fn bezier_curve_to(
&mut self,
control_point1: &Point2D<f32>,
control_point2: &Point2D<f32>,
control_point3: &Point2D<f32>,
);
fn close(&mut self);
#[allow(clippy::too_many_arguments)]
fn ellipse(
&mut self,
origin: Point2D<f32>,
radius_x: f32,
radius_y: f32,
rotation_angle: f32,
start_angle: f32,
end_angle: f32,
anticlockwise: bool,
) {
let mut start = Angle::radians(start_angle);
let mut end = Angle::radians(end_angle);
// Wrap angles mod 2 * PI if necessary
if !anticlockwise && start > end + Angle::two_pi() ||
anticlockwise && end > start + Angle::two_pi()
{
start = start.positive();
end = end.positive();
}
// Calculate the total arc we're going to sweep.
let sweep = match anticlockwise {
true => {
if end - start == Angle::two_pi() {
-Angle::two_pi()
} else if end > start {
-(Angle::two_pi() - (end - start))
} else {
-(start - end)
}
},
false => {
if start - end == Angle::two_pi() {
Angle::two_pi()
} else if start > end {
Angle::two_pi() - (start - end)
} else {
end - start
}
},
};
let arc: Arc<f32> = Arc {
center: origin,
radii: Vector2D::new(radius_x, radius_y),
start_angle: start,
sweep_angle: sweep,
x_rotation: Angle::radians(rotation_angle),
};
self.line_to(arc.from());
arc.for_each_quadratic_bezier(&mut |q| {
self.quadratic_curve_to(&q.ctrl, &q.to);
});
}
fn get_current_point(&mut self) -> Option<Point2D<f32>>;
fn line_to(&mut self, point: Point2D<f32>);
fn move_to(&mut self, point: Point2D<f32>);
fn quadratic_curve_to(&mut self, control_point: &Point2D<f32>, end_point: &Point2D<f32>);
fn svg_arc(
&mut self,
radius_x: f32,
radius_y: f32,
rotation_angle: f32,
large_arc: bool,
sweep: bool,
end_point: Point2D<f32>,
) {
let Some(start) = self.get_current_point() else {
return;
};
let arc = lyon_geom::SvgArc {
from: start,
to: end_point,
radii: lyon_geom::vector(radius_x, radius_y),
x_rotation: lyon_geom::Angle::degrees(rotation_angle),
flags: lyon_geom::ArcFlags { large_arc, sweep },
};
arc.for_each_quadratic_bezier(&mut |q| {
self.quadratic_curve_to(&q.ctrl, &q.to);
});
}
fn finish(&mut self) -> B::Path;
}
pub(crate) trait PatternHelpers {
fn is_zero_size_gradient(&self) -> bool;
fn draw_rect(&self, rect: &Rect<f32>) -> Rect<f32>;
}
pub(crate) trait StrokeOptionsHelpers {
fn set_line_width(&mut self, _val: f32);
fn set_miter_limit(&mut self, _val: f32);
fn set_line_join(&mut self, val: LineJoinStyle);
fn set_line_cap(&mut self, val: LineCapStyle);
fn set_line_dash(&mut self, items: Vec<f32>);
fn set_line_dash_offset(&mut self, offset: f32);
}
pub(crate) trait DrawOptionsHelpers {
fn set_alpha(&mut self, val: f32);
}
pub(crate) trait PathHelpers<B: Backend> {
fn transformed_copy_to_builder(&self, transform: &Transform2D<f32>) -> B::PathBuilder;
fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D<f32>) -> bool;
fn copy_to_builder(&self) -> B::PathBuilder;
}

File diff suppressed because it is too large Load diff

View file

@ -9,20 +9,23 @@ use std::thread;
use canvas_traits::ConstellationCanvasMsg;
use canvas_traits::canvas::*;
use compositing_traits::CrossProcessCompositorApi;
use crossbeam_channel::{Sender, select, unbounded};
use euclid::default::Size2D;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use fonts::{FontContext, SystemFontServiceProxy};
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use log::warn;
use net_traits::ResourceThreads;
use style::color::AbsoluteColor;
use style::properties::style_structs::Font as FontStyleStruct;
use webrender_api::ImageKey;
use webrender_traits::CrossProcessCompositorApi;
use crate::canvas_data::*;
use crate::raqote_backend::RaqoteBackend;
pub struct CanvasPaintThread<'a> {
canvases: HashMap<CanvasId, CanvasData<'a>>,
canvases: HashMap<CanvasId, Canvas<'a>>,
next_canvas_id: CanvasId,
compositor_api: CrossProcessCompositorApi,
font_context: Arc<FontContext>,
@ -76,7 +79,11 @@ impl<'a> CanvasPaintThread<'a> {
},
Ok(CanvasMsg::FromScript(message, canvas_id)) => match message {
FromScriptMsg::SendPixels(chan) => {
canvas_paint_thread.canvas(canvas_id).send_pixels(chan);
chan.send(canvas_paint_thread
.canvas(canvas_id)
.read_pixels(None, None)
.as_ipc()
).unwrap();
},
},
Err(e) => {
@ -90,7 +97,10 @@ impl<'a> CanvasPaintThread<'a> {
let canvas_data = canvas_paint_thread.create_canvas(size);
creator.send(canvas_data).unwrap();
},
Ok(ConstellationCanvasMsg::Exit) => break,
Ok(ConstellationCanvasMsg::Exit(exit_sender)) => {
let _ = exit_sender.send(());
break;
},
Err(e) => {
warn!("Error on CanvasPaintThread receive ({})", e);
break;
@ -109,10 +119,14 @@ impl<'a> CanvasPaintThread<'a> {
let canvas_id = self.next_canvas_id;
self.next_canvas_id.0 += 1;
let canvas_data =
CanvasData::new(size, self.compositor_api.clone(), self.font_context.clone());
let canvas_data = CanvasData::new(
size,
self.compositor_api.clone(),
self.font_context.clone(),
RaqoteBackend,
);
let image_key = canvas_data.image_key();
self.canvases.insert(canvas_id, canvas_data);
self.canvases.insert(canvas_id, Canvas::Raqote(canvas_data));
(canvas_id, image_key)
}
@ -159,24 +173,21 @@ impl<'a> CanvasPaintThread<'a> {
Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, chan) => self
.canvas(canvas_id)
.is_point_in_path_(&path[..], x, y, fill_rule, chan),
Canvas2dMsg::DrawImage(
ref image_data,
image_size,
dest_rect,
source_rect,
smoothing_enabled,
) => self.canvas(canvas_id).draw_image(
image_data,
image_size,
dest_rect,
source_rect,
smoothing_enabled,
true,
),
Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => {
let snapshot = snapshot.to_owned();
self.canvas(canvas_id).draw_image(
snapshot.data(),
snapshot.size(),
dest_rect,
source_rect,
smoothing_enabled,
!snapshot.alpha_mode().is_premultiplied(),
)
},
Canvas2dMsg::DrawEmptyImage(image_size, dest_rect, source_rect) => {
self.canvas(canvas_id).draw_image(
&vec![0; image_size.area() as usize * 4],
image_size,
image_size.to_u64(),
dest_rect,
source_rect,
false,
@ -192,10 +203,10 @@ impl<'a> CanvasPaintThread<'a> {
) => {
let image_data = self
.canvas(canvas_id)
.read_pixels(source_rect.to_u64(), image_size.to_u64());
.read_pixels(Some(source_rect.to_u64()), Some(image_size.to_u64()));
self.canvas(other_canvas_id).draw_image(
&image_data,
source_rect.size,
image_data.data(),
source_rect.size.to_u64(),
dest_rect,
source_rect,
smoothing,
@ -244,8 +255,10 @@ impl<'a> CanvasPaintThread<'a> {
self.canvas(canvas_id).set_global_composition(op)
},
Canvas2dMsg::GetImageData(dest_rect, canvas_size, sender) => {
let pixels = self.canvas(canvas_id).read_pixels(dest_rect, canvas_size);
sender.send(&pixels).unwrap();
let snapshot = self
.canvas(canvas_id)
.read_pixels(Some(dest_rect), Some(canvas_size));
sender.send(snapshot.as_ipc()).unwrap();
},
Canvas2dMsg::PutImageData(rect, receiver) => {
self.canvas(canvas_id)
@ -273,7 +286,347 @@ impl<'a> CanvasPaintThread<'a> {
}
}
fn canvas(&mut self, canvas_id: CanvasId) -> &mut CanvasData<'a> {
fn canvas(&mut self, canvas_id: CanvasId) -> &mut Canvas<'a> {
self.canvases.get_mut(&canvas_id).expect("Bogus canvas id")
}
}
enum Canvas<'a> {
Raqote(CanvasData<'a, RaqoteBackend>),
}
impl Canvas<'_> {
fn set_fill_style(&mut self, style: FillOrStrokeStyle) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_fill_style(style),
}
}
fn fill(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.fill(),
}
}
fn fill_text(&mut self, text: String, x: f64, y: f64, max_width: Option<f64>, is_rtl: bool) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.fill_text(text, x, y, max_width, is_rtl),
}
}
fn fill_rect(&mut self, rect: &Rect<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.fill_rect(rect),
}
}
fn set_stroke_style(&mut self, style: FillOrStrokeStyle) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_stroke_style(style),
}
}
fn stroke_rect(&mut self, rect: &Rect<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.stroke_rect(rect),
}
}
fn begin_path(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.begin_path(),
}
}
fn close_path(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.close_path(),
}
}
fn fill_path(&mut self, path: &[PathSegment]) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.fill_path(path),
}
}
fn stroke(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.stroke(),
}
}
fn stroke_path(&mut self, path: &[PathSegment]) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.stroke_path(path),
}
}
fn clip(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.clip(),
}
}
fn is_point_in_path(&mut self, x: f64, y: f64, fill_rule: FillRule, chan: IpcSender<bool>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.is_point_in_path(x, y, fill_rule, chan),
}
}
fn is_point_in_path_(
&mut self,
path: &[PathSegment],
x: f64,
y: f64,
fill_rule: FillRule,
chan: IpcSender<bool>,
) {
match self {
Canvas::Raqote(canvas_data) => {
canvas_data.is_point_in_path_(path, x, y, fill_rule, chan)
},
}
}
fn clear_rect(&mut self, rect: &Rect<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.clear_rect(rect),
}
}
fn draw_image(
&mut self,
data: &[u8],
size: Size2D<u64>,
dest_rect: Rect<f64>,
source_rect: Rect<f64>,
smoothing_enabled: bool,
is_premultiplied: bool,
) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.draw_image(
data,
size,
dest_rect,
source_rect,
smoothing_enabled,
is_premultiplied,
),
}
}
fn read_pixels(
&mut self,
read_rect: Option<Rect<u64>>,
canvas_size: Option<Size2D<u64>>,
) -> snapshot::Snapshot {
match self {
Canvas::Raqote(canvas_data) => canvas_data.read_pixels(read_rect, canvas_size),
}
}
fn move_to(&mut self, point: &Point2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.move_to(point),
}
}
fn line_to(&mut self, point: &Point2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.line_to(point),
}
}
fn rect(&mut self, rect: &Rect<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.rect(rect),
}
}
fn quadratic_curve_to(&mut self, cp: &Point2D<f32>, pt: &Point2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.quadratic_curve_to(cp, pt),
}
}
fn bezier_curve_to(&mut self, cp1: &Point2D<f32>, cp2: &Point2D<f32>, pt: &Point2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.bezier_curve_to(cp1, cp2, pt),
}
}
fn arc(&mut self, center: &Point2D<f32>, radius: f32, start: f32, end: f32, ccw: bool) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.arc(center, radius, start, end, ccw),
}
}
fn arc_to(&mut self, cp1: &Point2D<f32>, cp2: &Point2D<f32>, radius: f32) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.arc_to(cp1, cp2, radius),
}
}
#[allow(clippy::too_many_arguments)]
fn ellipse(
&mut self,
center: &Point2D<f32>,
radius_x: f32,
radius_y: f32,
rotation: f32,
start: f32,
end: f32,
ccw: bool,
) {
match self {
Canvas::Raqote(canvas_data) => {
canvas_data.ellipse(center, radius_x, radius_y, rotation, start, end, ccw)
},
}
}
fn restore_context_state(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.restore_context_state(),
}
}
fn save_context_state(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.save_context_state(),
}
}
fn set_line_width(&mut self, width: f32) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_line_width(width),
}
}
fn set_line_cap(&mut self, cap: LineCapStyle) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_line_cap(cap),
}
}
fn set_line_join(&mut self, join: LineJoinStyle) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_line_join(join),
}
}
fn set_miter_limit(&mut self, limit: f32) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_miter_limit(limit),
}
}
fn set_line_dash(&mut self, items: Vec<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_line_dash(items),
}
}
fn set_line_dash_offset(&mut self, offset: f32) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_line_dash_offset(offset),
}
}
fn set_transform(&mut self, matrix: &Transform2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_transform(matrix),
}
}
fn set_global_alpha(&mut self, alpha: f32) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_global_alpha(alpha),
}
}
fn set_global_composition(&mut self, op: CompositionOrBlending) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_global_composition(op),
}
}
fn set_shadow_offset_x(&mut self, value: f64) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_shadow_offset_x(value),
}
}
fn set_shadow_offset_y(&mut self, value: f64) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_shadow_offset_y(value),
}
}
fn set_shadow_blur(&mut self, value: f64) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_shadow_blur(value),
}
}
fn set_shadow_color(&mut self, color: AbsoluteColor) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_shadow_color(color),
}
}
fn set_font(&mut self, font_style: FontStyleStruct) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_font(font_style),
}
}
fn set_text_align(&mut self, text_align: TextAlign) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_text_align(text_align),
}
}
fn set_text_baseline(&mut self, text_baseline: TextBaseline) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.set_text_baseline(text_baseline),
}
}
fn measure_text(&mut self, text: String) -> TextMetrics {
match self {
Canvas::Raqote(canvas_data) => canvas_data.measure_text(text),
}
}
fn clip_path(&mut self, path: &[PathSegment]) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.clip_path(path),
}
}
fn get_transform(&self) -> Transform2D<f32> {
match self {
Canvas::Raqote(canvas_data) => canvas_data.get_transform(),
}
}
fn put_image_data(&mut self, unwrap: Vec<u8>, rect: Rect<u64>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.put_image_data(unwrap, rect),
}
}
fn update_image_rendering(&mut self) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.update_image_rendering(),
}
}
fn recreate(&mut self, size: Option<Size2D<u64>>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.recreate(size),
}
}
}

View file

@ -4,14 +4,8 @@
#![deny(unsafe_code)]
mod backend;
mod raqote_backend;
pub use webgl_mode::WebGLComm;
pub mod canvas_data;
pub mod canvas_paint_thread;
mod webgl_limits;
mod webgl_mode;
pub mod webgl_thread;
#[cfg(feature = "webxr")]
mod webxr;

View file

@ -7,20 +7,19 @@ use std::collections::HashMap;
use canvas_traits::canvas::*;
use cssparser::color::clamp_unit_f32;
use euclid::Angle;
use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D};
use font_kit::font::Font;
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods};
use log::warn;
use lyon_geom::Arc;
use range::Range;
use raqote::PathOp;
use style::color::AbsoluteColor;
use crate::canvas_data::{
self, Backend, CanvasPaintState, Color, CompositionOp, DrawOptions, Filter, GenericDrawTarget,
GenericPathBuilder, GradientStop, GradientStops, Path, SourceSurface, StrokeOptions, TextRun,
use crate::backend::{
Backend, DrawOptionsHelpers, GenericDrawTarget, GenericPathBuilder, PathHelpers,
PatternHelpers, StrokeOptionsHelpers,
};
use crate::canvas_data::{CanvasPaintState, Filter, TextRun};
thread_local! {
/// The shared font cache used by all canvases that render on a thread. It would be nicer
@ -30,80 +29,85 @@ thread_local! {
static SHARED_FONT_CACHE: RefCell<HashMap<FontIdentifier, Font>> = RefCell::default();
}
#[derive(Default)]
pub struct RaqoteBackend;
#[derive(Clone, Default)]
pub(crate) struct RaqoteBackend;
impl Backend for RaqoteBackend {
fn get_composition_op(&self, opts: &DrawOptions) -> CompositionOp {
CompositionOp::Raqote(opts.as_raqote().blend_mode)
type Pattern<'a> = Pattern<'a>;
type StrokeOptions = raqote::StrokeStyle;
type Color = raqote::SolidSource;
type DrawOptions = raqote::DrawOptions;
type CompositionOp = raqote::BlendMode;
type DrawTarget = raqote::DrawTarget;
type PathBuilder = PathBuilder;
type SourceSurface = Vec<u8>; // TODO: See if we can avoid the alloc (probably?)
type Bytes<'a> = &'a [u8];
type Path = raqote::Path;
type GradientStop = raqote::GradientStop;
type GradientStops = Vec<raqote::GradientStop>;
fn get_composition_op(&self, opts: &Self::DrawOptions) -> Self::CompositionOp {
opts.blend_mode
}
fn need_to_draw_shadow(&self, color: &Color) -> bool {
color.as_raqote().a != 0
fn need_to_draw_shadow(&self, color: &Self::Color) -> bool {
color.a != 0
}
fn set_shadow_color(&mut self, color: AbsoluteColor, state: &mut CanvasPaintState<'_>) {
state.shadow_color = Color::Raqote(color.to_raqote_style());
fn set_shadow_color(&mut self, color: AbsoluteColor, state: &mut CanvasPaintState<'_, Self>) {
state.shadow_color = color.to_raqote_style();
}
fn set_fill_style(
&mut self,
style: FillOrStrokeStyle,
state: &mut CanvasPaintState<'_>,
_drawtarget: &dyn GenericDrawTarget,
state: &mut CanvasPaintState<'_, Self>,
_drawtarget: &Self::DrawTarget,
) {
if let Some(pattern) = style.to_raqote_pattern() {
state.fill_style = canvas_data::Pattern::Raqote(pattern);
state.fill_style = pattern;
}
}
fn set_stroke_style(
&mut self,
style: FillOrStrokeStyle,
state: &mut CanvasPaintState<'_>,
_drawtarget: &dyn GenericDrawTarget,
state: &mut CanvasPaintState<'_, Self>,
_drawtarget: &Self::DrawTarget,
) {
if let Some(pattern) = style.to_raqote_pattern() {
state.stroke_style = canvas_data::Pattern::Raqote(pattern);
state.stroke_style = pattern;
}
}
fn set_global_composition(
&mut self,
op: CompositionOrBlending,
state: &mut CanvasPaintState<'_>,
state: &mut CanvasPaintState<'_, Self>,
) {
state.draw_options.as_raqote_mut().blend_mode = op.to_raqote_style();
state.draw_options.blend_mode = op.to_raqote_style();
}
fn create_drawtarget(&self, size: Size2D<u64>) -> Box<dyn GenericDrawTarget> {
Box::new(raqote::DrawTarget::new(
size.width as i32,
size.height as i32,
))
fn create_drawtarget(&self, size: Size2D<u64>) -> Self::DrawTarget {
raqote::DrawTarget::new(size.width as i32, size.height as i32)
}
fn recreate_paint_state<'a>(&self, _state: &CanvasPaintState<'a>) -> CanvasPaintState<'a> {
CanvasPaintState::default()
}
}
impl Default for CanvasPaintState<'_> {
fn default() -> Self {
fn new_paint_state<'a>(&self) -> CanvasPaintState<'a, Self> {
let pattern = Pattern::Color(255, 0, 0, 0);
CanvasPaintState {
draw_options: DrawOptions::Raqote(raqote::DrawOptions::new()),
fill_style: canvas_data::Pattern::Raqote(pattern.clone()),
stroke_style: canvas_data::Pattern::Raqote(pattern),
stroke_opts: StrokeOptions::Raqote(Default::default()),
draw_options: raqote::DrawOptions::new(),
fill_style: pattern.clone(),
stroke_style: pattern,
stroke_opts: Default::default(),
transform: Transform2D::identity(),
shadow_offset_x: 0.0,
shadow_offset_y: 0.0,
shadow_blur: 0.0,
shadow_color: Color::Raqote(raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0)),
shadow_color: raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0),
font_style: None,
text_align: TextAlign::default(),
text_baseline: TextBaseline::default(),
_backend: std::marker::PhantomData,
}
}
}
@ -230,136 +234,122 @@ impl Repetition {
}
}
impl canvas_data::Pattern<'_> {
pub fn source(&self) -> raqote::Source {
pub fn source<'a>(pattern: &Pattern<'a>) -> raqote::Source<'a> {
match pattern {
Pattern::Color(a, r, g, b) => raqote::Source::Solid(
raqote::SolidSource::from_unpremultiplied_argb(*a, *r, *g, *b),
),
Pattern::LinearGradient(pattern) => raqote::Source::new_linear_gradient(
pattern.gradient.clone(),
pattern.start,
pattern.end,
raqote::Spread::Pad,
),
Pattern::RadialGradient(pattern) => raqote::Source::new_two_circle_radial_gradient(
pattern.gradient.clone(),
pattern.center1,
pattern.radius1,
pattern.center2,
pattern.radius2,
raqote::Spread::Pad,
),
Pattern::Surface(pattern) => raqote::Source::Image(
pattern.image,
pattern.extend,
pattern.filter,
pattern.transform,
),
}
}
impl PatternHelpers for Pattern<'_> {
fn is_zero_size_gradient(&self) -> bool {
match self {
canvas_data::Pattern::Raqote(pattern) => match pattern {
Pattern::Color(a, r, g, b) => raqote::Source::Solid(
raqote::SolidSource::from_unpremultiplied_argb(*a, *r, *g, *b),
),
Pattern::LinearGradient(pattern) => raqote::Source::new_linear_gradient(
pattern.gradient.clone(),
pattern.start,
pattern.end,
raqote::Spread::Pad,
),
Pattern::RadialGradient(pattern) => raqote::Source::new_two_circle_radial_gradient(
pattern.gradient.clone(),
pattern.center1,
pattern.radius1,
pattern.center2,
pattern.radius2,
raqote::Spread::Pad,
),
Pattern::Surface(pattern) => raqote::Source::Image(
pattern.image,
pattern.extend,
pattern.filter,
pattern.transform,
),
Pattern::RadialGradient(pattern) => {
let centers_equal = pattern.center1 == pattern.center2;
let radii_equal = pattern.radius1 == pattern.radius2;
(centers_equal && radii_equal) || pattern.gradient.stops.is_empty()
},
}
}
pub fn is_zero_size_gradient(&self) -> bool {
match self {
canvas_data::Pattern::Raqote(pattern) => match pattern {
Pattern::RadialGradient(pattern) => {
let centers_equal = pattern.center1 == pattern.center2;
let radii_equal = pattern.radius1 == pattern.radius2;
(centers_equal && radii_equal) || pattern.gradient.stops.is_empty()
},
Pattern::LinearGradient(pattern) => {
(pattern.start == pattern.end) || pattern.gradient.stops.is_empty()
},
Pattern::Color(..) | Pattern::Surface(..) => false,
Pattern::LinearGradient(pattern) => {
(pattern.start == pattern.end) || pattern.gradient.stops.is_empty()
},
Pattern::Color(..) | Pattern::Surface(..) => false,
}
}
fn draw_rect(&self, rect: &Rect<f32>) -> Rect<f32> {
match self {
Pattern::Surface(pattern) => {
let pattern_rect = Rect::new(Point2D::origin(), pattern.size());
let mut draw_rect = rect.intersection(&pattern_rect).unwrap_or(Rect::zero());
match pattern.repetition() {
Repetition::NoRepeat => {
draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width);
draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height);
},
Repetition::RepeatX => {
draw_rect.size.width = rect.size.width;
draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height);
},
Repetition::RepeatY => {
draw_rect.size.height = rect.size.height;
draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width);
},
Repetition::Repeat => {
draw_rect = *rect;
},
}
draw_rect
},
Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => *rect,
}
}
}
impl StrokeOptions {
pub fn set_line_width(&mut self, _val: f32) {
match self {
StrokeOptions::Raqote(options) => options.width = _val,
}
impl StrokeOptionsHelpers for raqote::StrokeStyle {
fn set_line_width(&mut self, _val: f32) {
self.width = _val;
}
pub fn set_miter_limit(&mut self, _val: f32) {
match self {
StrokeOptions::Raqote(options) => options.miter_limit = _val,
}
fn set_miter_limit(&mut self, _val: f32) {
self.miter_limit = _val;
}
pub fn set_line_join(&mut self, val: LineJoinStyle) {
match self {
StrokeOptions::Raqote(options) => options.join = val.to_raqote_style(),
}
fn set_line_join(&mut self, val: LineJoinStyle) {
self.join = val.to_raqote_style();
}
pub fn set_line_cap(&mut self, val: LineCapStyle) {
match self {
StrokeOptions::Raqote(options) => options.cap = val.to_raqote_style(),
}
fn set_line_cap(&mut self, val: LineCapStyle) {
self.cap = val.to_raqote_style();
}
pub fn set_line_dash(&mut self, items: Vec<f32>) {
match self {
StrokeOptions::Raqote(options) => options.dash_array = items,
}
fn set_line_dash(&mut self, items: Vec<f32>) {
self.dash_array = items;
}
pub fn set_line_dash_offset(&mut self, offset: f32) {
match self {
StrokeOptions::Raqote(options) => options.dash_offset = offset,
}
}
pub fn as_raqote(&self) -> &raqote::StrokeStyle {
match self {
StrokeOptions::Raqote(options) => options,
}
fn set_line_dash_offset(&mut self, offset: f32) {
self.dash_offset = offset;
}
}
impl DrawOptions {
pub fn set_alpha(&mut self, val: f32) {
match self {
DrawOptions::Raqote(draw_options) => draw_options.alpha = val,
}
}
pub fn as_raqote(&self) -> &raqote::DrawOptions {
match self {
DrawOptions::Raqote(options) => options,
}
}
fn as_raqote_mut(&mut self) -> &mut raqote::DrawOptions {
match self {
DrawOptions::Raqote(options) => options,
}
impl DrawOptionsHelpers for raqote::DrawOptions {
fn set_alpha(&mut self, val: f32) {
self.alpha = val;
}
}
impl Path {
pub fn transformed_copy_to_builder(
&self,
transform: &Transform2D<f32>,
) -> Box<dyn GenericPathBuilder> {
Box::new(PathBuilder(Some(raqote::PathBuilder::from(
self.as_raqote().clone().transform(transform),
))))
impl PathHelpers<RaqoteBackend> for raqote::Path {
fn transformed_copy_to_builder(&self, transform: &Transform2D<f32>) -> PathBuilder {
PathBuilder(Some(raqote::PathBuilder::from(
self.clone().transform(transform),
)))
}
pub fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D<f32>) -> bool {
self.as_raqote()
.clone()
fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D<f32>) -> bool {
self.clone()
.transform(path_transform)
.contains_point(0.1, x as f32, y as f32)
}
pub fn copy_to_builder(&self) -> Box<dyn GenericPathBuilder> {
Box::new(PathBuilder(Some(raqote::PathBuilder::from(
self.as_raqote().clone(),
))))
}
pub fn as_raqote(&self) -> &raqote::Path {
match self {
Path::Raqote(p) => p,
}
fn copy_to_builder(&self) -> PathBuilder {
PathBuilder(Some(raqote::PathBuilder::from(self.clone())))
}
}
@ -373,7 +363,7 @@ fn create_gradient_stops(gradient_stops: Vec<CanvasGradientStop>) -> Vec<raqote:
stops
}
impl GenericDrawTarget for raqote::DrawTarget {
impl GenericDrawTarget<RaqoteBackend> for raqote::DrawTarget {
fn clear_rect(&mut self, rect: &Rect<f32>) {
let mut pb = raqote::PathBuilder::new();
pb.rect(
@ -385,59 +375,47 @@ impl GenericDrawTarget for raqote::DrawTarget {
let mut options = raqote::DrawOptions::new();
options.blend_mode = raqote::BlendMode::Clear;
let pattern = Pattern::Color(0, 0, 0, 0);
GenericDrawTarget::fill(
self,
&Path::Raqote(pb.finish()),
canvas_data::Pattern::Raqote(pattern),
&DrawOptions::Raqote(options),
);
<Self as GenericDrawTarget<RaqoteBackend>>::fill(self, &pb.finish(), pattern, &options);
}
#[allow(unsafe_code)]
fn copy_surface(
&mut self,
surface: SourceSurface,
surface: <RaqoteBackend as Backend>::SourceSurface,
source: Rect<i32>,
destination: Point2D<i32>,
) {
let mut dt = raqote::DrawTarget::new(source.size.width, source.size.height);
let data = surface.as_raqote();
let data = surface;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u32, data.len() / 4) };
dt.get_data_mut().copy_from_slice(s);
raqote::DrawTarget::copy_surface(self, &dt, source.to_box2d(), destination);
}
// TODO(pylbrecht)
// Somehow a duplicate of `create_gradient_stops()` with different types.
// It feels cumbersome to convert GradientStop back and forth just to use
// `create_gradient_stops()`, so I'll leave this here for now.
fn create_gradient_stops(&self, gradient_stops: Vec<GradientStop>) -> GradientStops {
let mut stops = gradient_stops
.into_iter()
.map(|item| *item.as_raqote())
.collect::<Vec<raqote::GradientStop>>();
// https://www.w3.org/html/test/results/2dcontext/annotated-spec/canvas.html#testrefs.2d.gradient.interpolate.overlap
stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
GradientStops::Raqote(stops)
}
fn create_path_builder(&self) -> Box<dyn GenericPathBuilder> {
Box::new(PathBuilder::new())
fn create_path_builder(&self) -> <RaqoteBackend as Backend>::PathBuilder {
PathBuilder::new()
}
fn create_similar_draw_target(&self, size: &Size2D<i32>) -> Box<dyn GenericDrawTarget> {
Box::new(raqote::DrawTarget::new(size.width, size.height))
fn create_similar_draw_target(
&self,
size: &Size2D<i32>,
) -> <RaqoteBackend as Backend>::DrawTarget {
raqote::DrawTarget::new(size.width, size.height)
}
fn create_source_surface_from_data(&self, data: &[u8]) -> Option<SourceSurface> {
Some(SourceSurface::Raqote(data.to_vec()))
fn create_source_surface_from_data(
&self,
data: &[u8],
) -> Option<<RaqoteBackend as Backend>::SourceSurface> {
Some(data.to_vec())
}
#[allow(unsafe_code)]
fn draw_surface(
&mut self,
surface: SourceSurface,
surface: <RaqoteBackend as Backend>::SourceSurface,
dest: Rect<f64>,
source: Rect<f64>,
filter: Filter,
draw_options: &DrawOptions,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
let surface_data = surface.as_raqote();
let surface_data = surface;
let image = raqote::Image {
width: source.size.width as i32,
height: source.size.height as i32,
@ -470,33 +448,29 @@ impl GenericDrawTarget for raqote::DrawTarget {
dest.size.height as f32,
);
GenericDrawTarget::fill(
self,
&Path::Raqote(pb.finish()),
canvas_data::Pattern::Raqote(pattern),
draw_options,
);
<Self as GenericDrawTarget<RaqoteBackend>>::fill(self, &pb.finish(), pattern, draw_options);
}
fn draw_surface_with_shadow(
&self,
_surface: SourceSurface,
_surface: <RaqoteBackend as Backend>::SourceSurface,
_dest: &Point2D<f32>,
_color: &Color,
_color: &<RaqoteBackend as Backend>::Color,
_offset: &Vector2D<f32>,
_sigma: f32,
_operator: CompositionOp,
_operator: <RaqoteBackend as Backend>::CompositionOp,
) {
warn!("no support for drawing shadows");
}
fn fill(&mut self, path: &Path, pattern: canvas_data::Pattern, draw_options: &DrawOptions) {
match draw_options.as_raqote().blend_mode {
fn fill(
&mut self,
path: &<RaqoteBackend as Backend>::Path,
pattern: <RaqoteBackend as Backend>::Pattern<'_>,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
match draw_options.blend_mode {
raqote::BlendMode::Src => {
self.clear(raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0));
self.fill(
path.as_raqote(),
&pattern.source(),
draw_options.as_raqote(),
);
self.fill(path, &source(&pattern), draw_options);
},
raqote::BlendMode::Clear |
raqote::BlendMode::SrcAtop |
@ -505,26 +479,19 @@ impl GenericDrawTarget for raqote::DrawTarget {
raqote::BlendMode::Xor |
raqote::BlendMode::DstOver |
raqote::BlendMode::SrcOver => {
self.fill(
path.as_raqote(),
&pattern.source(),
draw_options.as_raqote(),
);
self.fill(path, &source(&pattern), draw_options);
},
raqote::BlendMode::SrcIn |
raqote::BlendMode::SrcOut |
raqote::BlendMode::DstIn |
raqote::BlendMode::DstAtop => {
let mut options = *draw_options.as_raqote();
let mut options = *draw_options;
self.push_layer_with_blend(1., options.blend_mode);
options.blend_mode = raqote::BlendMode::SrcOver;
self.fill(path.as_raqote(), &pattern.source(), &options);
self.fill(path, &source(&pattern), &options);
self.pop_layer();
},
_ => warn!(
"unrecognized blend mode: {:?}",
draw_options.as_raqote().blend_mode
),
_ => warn!("unrecognized blend mode: {:?}", draw_options.blend_mode),
}
}
@ -532,8 +499,8 @@ impl GenericDrawTarget for raqote::DrawTarget {
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
pattern: &canvas_data::Pattern,
draw_options: &DrawOptions,
pattern: &<RaqoteBackend as Backend>::Pattern<'_>,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
let mut advance = 0.;
for run in text_runs.iter() {
@ -581,8 +548,8 @@ impl GenericDrawTarget for raqote::DrawTarget {
run.font.descriptor.pt_size.to_f32_px(),
&ids,
&positions,
&pattern.source(),
draw_options.as_raqote(),
&source(pattern),
draw_options,
);
})
}
@ -591,8 +558,8 @@ impl GenericDrawTarget for raqote::DrawTarget {
fn fill_rect(
&mut self,
rect: &Rect<f32>,
pattern: canvas_data::Pattern,
draw_options: Option<&DrawOptions>,
pattern: <RaqoteBackend as Backend>::Pattern<'_>,
draw_options: Option<&<RaqoteBackend as Backend>::DrawOptions>,
) {
let mut pb = raqote::PathBuilder::new();
pb.rect(
@ -602,16 +569,16 @@ impl GenericDrawTarget for raqote::DrawTarget {
rect.size.height,
);
let draw_options = if let Some(options) = draw_options {
*options.as_raqote()
*options
} else {
raqote::DrawOptions::new()
};
GenericDrawTarget::fill(
<Self as GenericDrawTarget<RaqoteBackend>>::fill(
self,
&Path::Raqote(pb.finish()),
&pb.finish(),
pattern,
&DrawOptions::Raqote(draw_options),
&draw_options,
);
}
fn get_size(&self) -> Size2D<i32> {
@ -623,41 +590,36 @@ impl GenericDrawTarget for raqote::DrawTarget {
fn pop_clip(&mut self) {
self.pop_clip();
}
fn push_clip(&mut self, path: &Path) {
self.push_clip(path.as_raqote());
fn push_clip(&mut self, path: &<RaqoteBackend as Backend>::Path) {
self.push_clip(path);
}
fn set_transform(&mut self, matrix: &Transform2D<f32>) {
self.set_transform(matrix);
}
fn snapshot(&self) -> SourceSurface {
SourceSurface::Raqote(self.snapshot_data_owned())
fn surface(&self) -> <RaqoteBackend as Backend>::SourceSurface {
self.bytes().to_vec()
}
fn stroke(
&mut self,
path: &Path,
pattern: canvas_data::Pattern,
stroke_options: &StrokeOptions,
draw_options: &DrawOptions,
path: &<RaqoteBackend as Backend>::Path,
pattern: Pattern<'_>,
stroke_options: &<RaqoteBackend as Backend>::StrokeOptions,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
self.stroke(
path.as_raqote(),
&pattern.source(),
stroke_options.as_raqote(),
draw_options.as_raqote(),
);
self.stroke(path, &source(&pattern), stroke_options, draw_options);
}
fn stroke_line(
&mut self,
start: Point2D<f32>,
end: Point2D<f32>,
pattern: canvas_data::Pattern,
stroke_options: &StrokeOptions,
draw_options: &DrawOptions,
pattern: <RaqoteBackend as Backend>::Pattern<'_>,
stroke_options: &<RaqoteBackend as Backend>::StrokeOptions,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
let mut pb = raqote::PathBuilder::new();
pb.move_to(start.x, start.y);
pb.line_to(end.x, end.y);
let mut stroke_options = stroke_options.as_raqote().clone();
let mut stroke_options = stroke_options.clone();
let cap = match stroke_options.join {
raqote::LineJoin::Round => raqote::LineCap::Round,
_ => raqote::LineCap::Butt,
@ -666,17 +628,17 @@ impl GenericDrawTarget for raqote::DrawTarget {
self.stroke(
&pb.finish(),
&pattern.source(),
&source(&pattern),
&stroke_options,
draw_options.as_raqote(),
draw_options,
);
}
fn stroke_rect(
&mut self,
rect: &Rect<f32>,
pattern: canvas_data::Pattern,
stroke_options: &StrokeOptions,
draw_options: &DrawOptions,
pattern: <RaqoteBackend as Backend>::Pattern<'_>,
stroke_options: &<RaqoteBackend as Backend>::StrokeOptions,
draw_options: &<RaqoteBackend as Backend>::DrawOptions,
) {
let mut pb = raqote::PathBuilder::new();
pb.rect(
@ -688,26 +650,15 @@ impl GenericDrawTarget for raqote::DrawTarget {
self.stroke(
&pb.finish(),
&pattern.source(),
stroke_options.as_raqote(),
draw_options.as_raqote(),
&source(&pattern),
stroke_options,
draw_options,
);
}
#[allow(unsafe_code)]
fn snapshot_data(&self, f: &dyn Fn(&[u8]) -> Vec<u8>) -> Vec<u8> {
fn bytes(&self) -> &[u8] {
let v = self.get_data();
f(
unsafe {
std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v))
},
)
}
#[allow(unsafe_code)]
fn snapshot_data_owned(&self) -> Vec<u8> {
let v = self.get_data();
unsafe {
std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)).into()
}
unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) }
}
}
@ -720,7 +671,7 @@ impl Filter {
}
}
struct PathBuilder(Option<raqote::PathBuilder>);
pub(crate) struct PathBuilder(Option<raqote::PathBuilder>);
impl PathBuilder {
fn new() -> PathBuilder {
@ -728,25 +679,7 @@ impl PathBuilder {
}
}
impl GenericPathBuilder for PathBuilder {
fn arc(
&mut self,
origin: Point2D<f32>,
radius: f32,
start_angle: f32,
end_angle: f32,
anticlockwise: bool,
) {
self.ellipse(
origin,
radius,
radius,
0.,
start_angle,
end_angle,
anticlockwise,
);
}
impl GenericPathBuilder<RaqoteBackend> for PathBuilder {
fn bezier_curve_to(
&mut self,
control_point1: &Point2D<f32>,
@ -762,98 +695,16 @@ impl GenericPathBuilder for PathBuilder {
control_point3.y,
);
}
fn close(&mut self) {
self.0.as_mut().unwrap().close();
}
fn ellipse(
&mut self,
origin: Point2D<f32>,
radius_x: f32,
radius_y: f32,
rotation_angle: f32,
start_angle: f32,
end_angle: f32,
anticlockwise: bool,
) {
let mut start = Angle::radians(start_angle);
let mut end = Angle::radians(end_angle);
// Wrap angles mod 2 * PI if necessary
if !anticlockwise && start > end + Angle::two_pi() ||
anticlockwise && end > start + Angle::two_pi()
{
start = start.positive();
end = end.positive();
}
// Calculate the total arc we're going to sweep.
let sweep = match anticlockwise {
true => {
if end - start == Angle::two_pi() {
-Angle::two_pi()
} else if end > start {
-(Angle::two_pi() - (end - start))
} else {
-(start - end)
}
},
false => {
if start - end == Angle::two_pi() {
Angle::two_pi()
} else if start > end {
Angle::two_pi() - (start - end)
} else {
end - start
}
},
};
let arc: Arc<f32> = Arc {
center: origin,
radii: Vector2D::new(radius_x, radius_y),
start_angle: start,
sweep_angle: sweep,
x_rotation: Angle::radians(rotation_angle),
};
self.line_to(arc.from());
arc.for_each_quadratic_bezier(&mut |q| {
self.quadratic_curve_to(&q.ctrl, &q.to);
});
}
fn svg_arc(
&mut self,
radius_x: f32,
radius_y: f32,
rotation_angle: f32,
large_arc: bool,
sweep: bool,
end_point: Point2D<f32>,
) {
let Some(start) = self.get_current_point() else {
return;
};
let arc = lyon_geom::SvgArc {
from: start,
to: end_point,
radii: lyon_geom::vector(radius_x, radius_y),
x_rotation: lyon_geom::Angle::degrees(rotation_angle),
flags: lyon_geom::ArcFlags { large_arc, sweep },
};
arc.for_each_quadratic_bezier(&mut |q| {
self.quadratic_curve_to(&q.ctrl, &q.to);
});
}
fn get_current_point(&mut self) -> Option<Point2D<f32>> {
let path = self.finish();
self.0 = Some(path.as_raqote().clone().into());
self.0 = Some(path.clone().into());
path.as_raqote().ops.iter().last().and_then(|op| match op {
path.ops.iter().last().and_then(|op| match op {
PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)),
PathOp::CubicTo(_, _, point) => Some(Point2D::new(point.x, point.y)),
PathOp::QuadTo(_, point) => Some(Point2D::new(point.x, point.y)),
@ -875,8 +726,8 @@ impl GenericPathBuilder for PathBuilder {
end_point.y,
);
}
fn finish(&mut self) -> Path {
Path::Raqote(self.0.take().unwrap().finish())
fn finish(&mut self) -> raqote::Path {
self.0.take().unwrap().finish()
}
}
@ -988,14 +839,6 @@ impl ToRaqotePattern<'_> for FillOrStrokeStyle {
}
}
impl Color {
fn as_raqote(&self) -> &raqote::SolidSource {
match self {
Color::Raqote(s) => s,
}
}
}
impl ToRaqoteStyle for AbsoluteColor {
type Target = raqote::SolidSource;
@ -1065,19 +908,3 @@ impl ToRaqoteStyle for CompositionStyle {
}
}
}
impl SourceSurface {
fn as_raqote(&self) -> &Vec<u8> {
match self {
SourceSurface::Raqote(s) => s,
}
}
}
impl GradientStop {
fn as_raqote(&self) -> &raqote::GradientStop {
match self {
GradientStop::Raqote(s) => s,
}
}
}

View file

@ -13,12 +13,12 @@ path = "lib.rs"
[features]
default = []
multiview = []
tracing = ["dep:tracing"]
webxr = ["dep:webxr"]
[dependencies]
base = { workspace = true }
bincode = { workspace = true }
bitflags = { workspace = true }
compositing_traits = { workspace = true }
constellation_traits = { workspace = true }
@ -34,15 +34,16 @@ log = { workspace = true }
net = { path = "../net" }
pixels = { path = "../pixels" }
profile_traits = { workspace = true }
script_traits = { workspace = true }
servo_allocator = { path = "../allocator" }
servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" }
stylo_traits = { workspace = true }
timers = { path = "../timers" }
tracing = { workspace = true, optional = true }
webrender = { workspace = true }
webrender_api = { workspace = true }
webrender_traits = { workspace = true }
webxr = { path = "../webxr", optional = true }
wr_malloc_size_of = { workspace = true }
[dev-dependencies]
surfman = { workspace = true }

File diff suppressed because it is too large Load diff

View file

@ -7,32 +7,32 @@
use std::cell::Cell;
use std::rc::Rc;
use compositing_traits::{CompositorProxy, CompositorReceiver};
use compositing_traits::rendering_context::RenderingContext;
use compositing_traits::{CompositorMsg, CompositorProxy};
use constellation_traits::EmbedderToConstellationMessage;
use crossbeam_channel::Sender;
use embedder_traits::ShutdownState;
use crossbeam_channel::{Receiver, Sender};
use embedder_traits::{EventLoopWaker, ShutdownState};
use profile_traits::{mem, time};
use webrender::RenderApi;
use webrender_api::DocumentId;
use webrender_traits::rendering_context::RenderingContext;
pub use crate::compositor::IOCompositor;
pub use crate::compositor::{IOCompositor, WebRenderDebugOption};
#[macro_use]
mod tracing;
mod compositor;
mod refresh_driver;
mod touch;
pub mod webview;
pub mod webview_manager;
pub mod windowing;
mod webview_manager;
mod webview_renderer;
/// Data used to construct a compositor.
pub struct InitialCompositorState {
/// A channel to the compositor.
pub sender: CompositorProxy,
/// A port on which messages inbound to the compositor can be received.
pub receiver: CompositorReceiver,
pub receiver: Receiver<CompositorMsg>,
/// A channel to the constellation.
pub constellation_chan: Sender<EmbedderToConstellationMessage>,
/// A channel to the time profiler thread.
@ -50,4 +50,7 @@ pub struct InitialCompositorState {
pub webrender_gl: Rc<dyn gleam::gl::Gl>,
#[cfg(feature = "webxr")]
pub webxr_main_thread: webxr::MainThreadRegistry,
/// An [`EventLoopWaker`] used in order to wake up the embedder when it is
/// time to paint.
pub event_loop_waker: Box<dyn EventLoopWaker>,
}

View file

@ -0,0 +1,234 @@
/* 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::cell::Cell;
use std::collections::hash_map::Values;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use base::id::WebViewId;
use constellation_traits::EmbedderToConstellationMessage;
use crossbeam_channel::{Sender, select};
use embedder_traits::EventLoopWaker;
use log::warn;
use timers::{BoxedTimerCallback, TimerEventId, TimerEventRequest, TimerScheduler, TimerSource};
use crate::compositor::RepaintReason;
use crate::webview_renderer::WebViewRenderer;
const FRAME_DURATION: Duration = Duration::from_millis(1000 / 120);
/// The [`RefreshDriver`] is responsible for controlling updates to aall `WebView`s
/// onscreen presentation. Currently, it only manages controlling animation update
/// requests.
///
/// The implementation is very basic at the moment, only requesting new animation
/// frames at a constant time after a repaint.
pub(crate) struct RefreshDriver {
/// The channel on which messages can be sent to the Constellation.
pub(crate) constellation_sender: Sender<EmbedderToConstellationMessage>,
/// Whether or not we are currently animating via a timer.
pub(crate) animating: Cell<bool>,
/// Whether or not we are waiting for our frame timeout to trigger
pub(crate) waiting_for_frame_timeout: Arc<AtomicBool>,
/// A [`TimerThread`] which is used to schedule frame timeouts in the future.
timer_thread: TimerThread,
/// An [`EventLoopWaker`] to be used to wake up the embedder when it is
/// time to paint a frame.
event_loop_waker: Box<dyn EventLoopWaker>,
}
impl RefreshDriver {
pub(crate) fn new(
constellation_sender: Sender<EmbedderToConstellationMessage>,
event_loop_waker: Box<dyn EventLoopWaker>,
) -> Self {
Self {
constellation_sender,
animating: Default::default(),
waiting_for_frame_timeout: Default::default(),
timer_thread: Default::default(),
event_loop_waker,
}
}
fn timer_callback(&self) -> BoxedTimerCallback {
let waiting_for_frame_timeout = self.waiting_for_frame_timeout.clone();
let event_loop_waker = self.event_loop_waker.clone_box();
Box::new(move |_| {
waiting_for_frame_timeout.store(false, Ordering::Relaxed);
event_loop_waker.wake();
})
}
/// Notify the [`RefreshDriver`] that a paint is about to happen. This will trigger
/// new animation frames for all active `WebView`s and schedule a new frame deadline.
pub(crate) fn notify_will_paint(
&self,
webview_renderers: Values<'_, WebViewId, WebViewRenderer>,
) {
// If we are still waiting for the frame to timeout this paint was caused for some
// non-animation related reason and we should wait until the frame timeout to trigger
// the next one.
if self.waiting_for_frame_timeout.load(Ordering::Relaxed) {
return;
}
// If any WebViews are animating ask them to paint again for another animation tick.
let animating_webviews: Vec<_> = webview_renderers
.filter_map(|webview_renderer| {
if webview_renderer.animating() {
Some(webview_renderer.id)
} else {
None
}
})
.collect();
// If nothing is animating any longer, update our state and exit early without requesting
// any noew frames nor triggering a new animation deadline.
if animating_webviews.is_empty() {
self.animating.set(false);
return;
}
if let Err(error) =
self.constellation_sender
.send(EmbedderToConstellationMessage::TickAnimation(
animating_webviews,
))
{
warn!("Sending tick to constellation failed ({error:?}).");
}
// Queue the next frame deadline.
self.animating.set(true);
self.waiting_for_frame_timeout
.store(true, Ordering::Relaxed);
self.timer_thread
.queue_timer(FRAME_DURATION, self.timer_callback());
}
/// Notify the [`RefreshDriver`] that the animation state of a particular `WebView`
/// via its associated [`WebViewRenderer`] has changed. In the case that a `WebView`
/// has started animating, the [`RefreshDriver`] will request a new frame from it
/// immediately, but only render that frame at the next frame deadline.
pub(crate) fn notify_animation_state_changed(&self, webview_renderer: &WebViewRenderer) {
if !webview_renderer.animating() {
// If no other WebView is animating we will officially stop animated once the
// next frame has been painted.
return;
}
if let Err(error) =
self.constellation_sender
.send(EmbedderToConstellationMessage::TickAnimation(vec![
webview_renderer.id,
]))
{
warn!("Sending tick to constellation failed ({error:?}).");
}
if self.animating.get() {
return;
}
self.animating.set(true);
self.waiting_for_frame_timeout
.store(true, Ordering::Relaxed);
self.timer_thread
.queue_timer(FRAME_DURATION, self.timer_callback());
}
/// Whether or not the renderer should trigger a message to the embedder to request a
/// repaint. This might be false if: we are animating and the repaint reason is just
/// for a new frame. In that case, the renderer should wait until the frame timeout to
/// ask the embedder to repaint.
pub(crate) fn wait_to_paint(&self, repaint_reason: RepaintReason) -> bool {
if !self.animating.get() || repaint_reason != RepaintReason::NewWebRenderFrame {
return false;
}
self.waiting_for_frame_timeout.load(Ordering::Relaxed)
}
}
enum TimerThreadMessage {
Request(TimerEventRequest),
Quit,
}
/// A thread that manages a [`TimerScheduler`] running in the background of the
/// [`RefreshDriver`]. This is necessary because we need a reliable way of waking up the
/// embedder's main thread, which may just be sleeping until the `EventLoopWaker` asks it
/// to wake up.
///
/// It would be nice to integrate this somehow into the embedder thread, but it would
/// require both some communication with the embedder and for all embedders to be well
/// behave respecting wakeup timeouts -- a bit too much to ask at the moment.
struct TimerThread {
sender: Sender<TimerThreadMessage>,
join_handle: Option<JoinHandle<()>>,
}
impl Drop for TimerThread {
fn drop(&mut self) {
let _ = self.sender.send(TimerThreadMessage::Quit);
if let Some(join_handle) = self.join_handle.take() {
let _ = join_handle.join();
}
}
}
impl Default for TimerThread {
fn default() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded::<TimerThreadMessage>();
let join_handle = thread::Builder::new()
.name(String::from("CompositorTimerThread"))
.spawn(move || {
let mut scheduler = TimerScheduler::default();
loop {
select! {
recv(receiver) -> message => {
match message {
Ok(TimerThreadMessage::Request(request)) => {
scheduler.schedule_timer(request);
},
_ => return,
}
},
recv(scheduler.wait_channel()) -> _message => {
scheduler.dispatch_completed_timers();
},
};
}
})
.expect("Could not create RefreshDriver timer thread.");
Self {
sender,
join_handle: Some(join_handle),
}
}
}
impl TimerThread {
fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) {
let _ = self
.sender
.send(TimerThreadMessage::Request(TimerEventRequest {
callback,
source: TimerSource::FromWorker,
id: TimerEventId(0),
duration,
}));
}
}

View file

@ -282,6 +282,10 @@ impl TouchHandler {
debug_assert!(old.is_some(), "Sequence already removed?");
}
pub fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
self.touch_sequence_map.get(&self.current_sequence_id)
}
pub fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
self.touch_sequence_map
.get_mut(&self.current_sequence_id)

View file

@ -42,7 +42,23 @@ mod from_constellation {
Self::LoadComplete(..) => target!("LoadComplete"),
Self::WebDriverMouseButtonEvent(..) => target!("WebDriverMouseButtonEvent"),
Self::WebDriverMouseMoveEvent(..) => target!("WebDriverMouseMoveEvent"),
Self::CrossProcess(_) => target!("CrossProcess"),
Self::WebDriverWheelScrollEvent(..) => target!("WebDriverWheelScrollEvent"),
Self::SendInitialTransaction(..) => target!("SendInitialTransaction"),
Self::SendScrollNode(..) => target!("SendScrollNode"),
Self::SendDisplayList { .. } => target!("SendDisplayList"),
Self::HitTest(..) => target!("HitTest"),
Self::GenerateImageKey(..) => target!("GenerateImageKey"),
Self::AddImage(..) => target!("AddImage"),
Self::UpdateImages(..) => target!("UpdateImages"),
Self::GenerateFontKeys(..) => target!("GenerateFontKeys"),
Self::AddFont(..) => target!("AddFont"),
Self::AddSystemFont(..) => target!("AddSystemFont"),
Self::AddFontInstance(..) => target!("AddFontInstance"),
Self::RemoveFonts(..) => target!("RemoveFonts"),
Self::GetClientWindowRect(..) => target!("GetClientWindowRect"),
Self::GetScreenSize(..) => target!("GetScreenSize"),
Self::GetAvailableScreenSize(..) => target!("GetAvailableScreenSize"),
Self::CollectMemoryReport(..) => target!("CollectMemoryReport"),
}
}
}

View file

@ -1,831 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::Keys;
use std::rc::Rc;
use base::id::{PipelineId, WebViewId};
use compositing_traits::SendableFrameTree;
use constellation_traits::{CompositorHitTestResult, EmbedderToConstellationMessage, ScrollState};
use embedder_traits::{
InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, ShutdownState,
TouchEvent, TouchEventType, TouchId,
};
use euclid::{Point2D, Scale, Vector2D};
use fnv::FnvHashSet;
use log::{debug, warn};
use script_traits::{AnimationState, TouchEventResult};
use webrender::Transaction;
use webrender_api::units::{DeviceIntPoint, DevicePoint, DeviceRect, LayoutVector2D};
use webrender_api::{
ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation,
};
use webrender_traits::RendererWebView;
use crate::IOCompositor;
use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
#[derive(Clone, Copy)]
struct ScrollEvent {
/// Scroll by this offset, or to Start or End
scroll_location: ScrollLocation,
/// Apply changes to the frame at this location
cursor: DeviceIntPoint,
/// The number of OS events that have been coalesced together into this one event.
event_count: u32,
}
#[derive(Clone, Copy)]
enum ScrollZoomEvent {
/// An pinch zoom event that magnifies the view by the given factor.
PinchZoom(f32),
/// A scroll event that scrolls the scroll node at the given location by the
/// given amount.
Scroll(ScrollEvent),
}
pub(crate) struct WebView {
/// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`].
pub id: WebViewId,
/// The renderer's view of the embedding layer `WebView` as a trait implementation,
/// so that the renderer doesn't need to depend on the embedding layer. This avoids
/// a dependency cycle.
pub renderer_webview: Box<dyn RendererWebView>,
/// The root [`PipelineId`] of the currently displayed page in this WebView.
pub root_pipeline_id: Option<PipelineId>,
pub rect: DeviceRect,
/// Tracks details about each active pipeline that the compositor knows about.
pub pipelines: HashMap<PipelineId, PipelineDetails>,
/// Data that is shared by all WebView renderers.
pub(crate) global: Rc<RefCell<ServoRenderer>>,
/// Pending scroll/zoom events.
pending_scroll_zoom_events: Vec<ScrollZoomEvent>,
/// Touch input state machine
touch_handler: TouchHandler,
}
impl Drop for WebView {
fn drop(&mut self) {
self.global
.borrow_mut()
.pipeline_to_webview_map
.retain(|_, webview_id| self.id != *webview_id);
}
}
impl WebView {
pub(crate) fn new(
renderer_webview: Box<dyn RendererWebView>,
rect: DeviceRect,
global: Rc<RefCell<ServoRenderer>>,
) -> Self {
Self {
id: renderer_webview.id(),
renderer_webview,
root_pipeline_id: None,
rect,
pipelines: Default::default(),
touch_handler: TouchHandler::new(),
global,
pending_scroll_zoom_events: Default::default(),
}
}
pub(crate) fn animations_or_animation_callbacks_running(&self) -> bool {
self.pipelines
.values()
.any(PipelineDetails::animations_or_animation_callbacks_running)
}
pub(crate) fn animation_callbacks_running(&self) -> bool {
self.pipelines
.values()
.any(PipelineDetails::animation_callbacks_running)
}
pub(crate) fn pipeline_ids(&self) -> Keys<'_, PipelineId, PipelineDetails> {
self.pipelines.keys()
}
/// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed.
pub(crate) fn ensure_pipeline_details(
&mut self,
pipeline_id: PipelineId,
) -> &mut PipelineDetails {
self.pipelines.entry(pipeline_id).or_insert_with(|| {
self.global
.borrow_mut()
.pipeline_to_webview_map
.insert(pipeline_id, self.id);
PipelineDetails::new(pipeline_id)
})
}
pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) {
self.ensure_pipeline_details(pipeline_id).throttled = throttled;
}
pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
self.global
.borrow_mut()
.pipeline_to_webview_map
.remove(&pipeline_id);
self.pipelines.remove(&pipeline_id);
}
pub(crate) fn set_frame_tree(&mut self, frame_tree: &SendableFrameTree) {
let pipeline_id = frame_tree.pipeline.id;
let old_pipeline_id = std::mem::replace(&mut self.root_pipeline_id, Some(pipeline_id));
if old_pipeline_id != self.root_pipeline_id {
debug!(
"Updating webview ({:?}) from pipeline {:?} to {:?}",
3, old_pipeline_id, self.root_pipeline_id
);
}
self.set_frame_tree_on_pipeline_details(frame_tree, None);
self.reset_scroll_tree_for_unattached_pipelines(frame_tree);
self.send_scroll_positions_to_layout_for_pipeline(pipeline_id);
}
pub(crate) fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: PipelineId) {
let Some(details) = self.pipelines.get(&pipeline_id) else {
return;
};
let mut scroll_states = Vec::new();
details.scroll_tree.nodes.iter().for_each(|node| {
if let (Some(scroll_id), Some(scroll_offset)) = (node.external_id(), node.offset()) {
scroll_states.push(ScrollState {
scroll_id,
scroll_offset,
});
}
});
let _ = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states),
);
}
pub(crate) fn set_frame_tree_on_pipeline_details(
&mut self,
frame_tree: &SendableFrameTree,
parent_pipeline_id: Option<PipelineId>,
) {
let pipeline_id = frame_tree.pipeline.id;
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
pipeline_details.pipeline = Some(frame_tree.pipeline.clone());
pipeline_details.parent_pipeline_id = parent_pipeline_id;
for kid in &frame_tree.children {
self.set_frame_tree_on_pipeline_details(kid, Some(pipeline_id));
}
}
pub(crate) fn reset_scroll_tree_for_unattached_pipelines(
&mut self,
frame_tree: &SendableFrameTree,
) {
// TODO(mrobinson): Eventually this can selectively preserve the scroll trees
// state for some unattached pipelines in order to preserve scroll position when
// navigating backward and forward.
fn collect_pipelines(
pipelines: &mut FnvHashSet<PipelineId>,
frame_tree: &SendableFrameTree,
) {
pipelines.insert(frame_tree.pipeline.id);
for kid in &frame_tree.children {
collect_pipelines(pipelines, kid);
}
}
let mut attached_pipelines: FnvHashSet<PipelineId> = FnvHashSet::default();
collect_pipelines(&mut attached_pipelines, frame_tree);
self.pipelines
.iter_mut()
.filter(|(id, _)| !attached_pipelines.contains(id))
.for_each(|(_, details)| {
details.scroll_tree.nodes.iter_mut().for_each(|node| {
node.set_offset(LayoutVector2D::zero());
})
})
}
/// Sets or unsets the animations-running flag for the given pipeline, and schedules a
/// recomposite if necessary. Returns true if the pipeline is throttled.
pub(crate) fn change_running_animations_state(
&mut self,
pipeline_id: PipelineId,
animation_state: AnimationState,
) -> bool {
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
match animation_state {
AnimationState::AnimationsPresent => {
pipeline_details.animations_running = true;
},
AnimationState::AnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = true;
},
AnimationState::NoAnimationsPresent => {
pipeline_details.animations_running = false;
},
AnimationState::NoAnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = false;
},
}
pipeline_details.throttled
}
pub(crate) fn tick_all_animations(&self, compositor: &IOCompositor) -> bool {
let mut ticked_any = false;
for pipeline_details in self.pipelines.values() {
ticked_any = pipeline_details.tick_animations(compositor) || ticked_any;
}
ticked_any
}
pub(crate) fn tick_animations_for_pipeline(
&self,
pipeline_id: PipelineId,
compositor: &IOCompositor,
) {
if let Some(pipeline_details) = self.pipelines.get(&pipeline_id) {
pipeline_details.tick_animations(compositor);
}
}
/// On a Window refresh tick (e.g. vsync)
pub fn on_vsync(&mut self) {
if let Some(fling_action) = self.touch_handler.on_vsync() {
self.on_scroll_window_event(
ScrollLocation::Delta(fling_action.delta),
fling_action.cursor,
);
}
}
pub(crate) fn dispatch_input_event(&mut self, event: InputEvent) {
// Events that do not need to do hit testing are sent directly to the
// constellation to filter down.
let Some(point) = event.point() else {
return;
};
// If we can't find a pipeline to send this event to, we cannot continue.
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let Some(result) = self
.global
.borrow()
.hit_test_at_point(point, get_pipeline_details)
else {
return;
};
self.global.borrow_mut().update_cursor(point, &result);
if let Err(error) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)),
) {
warn!("Sending event to constellation failed ({error:?}).");
}
}
pub fn notify_input_event(&mut self, event: InputEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
if let InputEvent::Touch(event) = event {
self.on_touch_event(event);
return;
}
if self.global.borrow().convert_mouse_to_touch {
match event {
InputEvent::MouseButton(event) => {
match event.action {
MouseButtonAction::Click => {},
MouseButtonAction::Down => self.on_touch_down(TouchEvent::new(
TouchEventType::Down,
TouchId(0),
event.point,
)),
MouseButtonAction::Up => self.on_touch_up(TouchEvent::new(
TouchEventType::Up,
TouchId(0),
event.point,
)),
}
return;
},
InputEvent::MouseMove(event) => {
self.on_touch_move(TouchEvent::new(
TouchEventType::Move,
TouchId(0),
event.point,
));
return;
},
_ => {},
}
}
self.dispatch_input_event(event);
}
fn send_touch_event(&self, mut event: TouchEvent) -> bool {
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let Some(result) = self
.global
.borrow()
.hit_test_at_point(event.point, get_pipeline_details)
else {
return false;
};
event.init_sequence_id(self.touch_handler.current_sequence_id);
let event = InputEvent::Touch(event);
if let Err(e) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)),
) {
warn!("Sending event to constellation failed ({:?}).", e);
false
} else {
true
}
}
pub fn on_touch_event(&mut self, event: TouchEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
match event.event_type {
TouchEventType::Down => self.on_touch_down(event),
TouchEventType::Move => self.on_touch_move(event),
TouchEventType::Up => self.on_touch_up(event),
TouchEventType::Cancel => self.on_touch_cancel(event),
}
}
fn on_touch_down(&mut self, event: TouchEvent) {
self.touch_handler.on_touch_down(event.id, event.point);
self.send_touch_event(event);
}
fn on_touch_move(&mut self, mut event: TouchEvent) {
let action: TouchMoveAction = self.touch_handler.on_touch_move(event.id, event.point);
if TouchMoveAction::NoAction != action {
// if first move processed and allowed, we directly process the move event,
// without waiting for the script handler.
if self
.touch_handler
.move_allowed(self.touch_handler.current_sequence_id)
{
// https://w3c.github.io/touch-events/#cancelability
event.disable_cancelable();
match action {
TouchMoveAction::Scroll(delta, point) => self.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(delta.to_untyped())),
point.cast(),
),
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(scroll_delta.to_untyped()),
),
cursor,
event_count: 1,
}));
},
_ => {},
}
}
// When the event is touchmove, if the script thread is processing the touch
// move event, we skip sending the event to the script thread.
// This prevents the script thread from stacking up for a large amount of time.
if !self
.touch_handler
.is_handling_touch_move(self.touch_handler.current_sequence_id) &&
self.send_touch_event(event) &&
event.is_cancelable()
{
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, true);
}
}
}
fn on_touch_up(&mut self, event: TouchEvent) {
self.touch_handler.on_touch_up(event.id, event.point);
self.send_touch_event(event);
}
fn on_touch_cancel(&mut self, event: TouchEvent) {
// Send the event to script.
self.touch_handler.on_touch_cancel(event.id, event.point);
self.send_touch_event(event);
}
pub(crate) fn on_touch_event_processed(&mut self, result: TouchEventResult) {
match result {
TouchEventResult::DefaultPrevented(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} prevented!",
event_type, sequence_id
);
match event_type {
TouchEventType::Down => {
// prevents both click and move
self.touch_handler.prevent_click(sequence_id);
self.touch_handler.prevent_move(sequence_id);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
},
TouchEventType::Move => {
// script thread processed the touch move event, mark this false.
if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) {
info.prevent_move = TouchMoveAllowed::Prevented;
if let TouchSequenceState::PendingFling { .. } = info.state {
info.state = TouchSequenceState::Finished;
}
self.touch_handler.set_handling_touch_move(
self.touch_handler.current_sequence_id,
false,
);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
}
},
TouchEventType::Up => {
// Note: We don't have to consider PendingFling here, since we handle that
// in the DefaultAllowed case of the touch_move event.
// Note: Removing can and should fail, if we still have an active Fling,
let Some(info) =
&mut self.touch_handler.get_touch_sequence_mut(sequence_id)
else {
// The sequence ID could already be removed, e.g. if Fling finished,
// before the touch_up event was handled (since fling can start
// immediately if move was previously allowed, and clicks are anyway not
// happening from fling).
return;
};
match info.state {
TouchSequenceState::PendingClick(_) => {
info.state = TouchSequenceState::Finished;
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Touching |
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::MultiTouch |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
}
},
TouchEventType::Cancel => {
// We could still have pending event handlers, so we remove the pending
// actions, and try to remove the touch sequence.
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
self.touch_handler.try_remove_touch_sequence(sequence_id);
},
}
},
TouchEventResult::DefaultAllowed(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} allowed",
event_type, sequence_id
);
match event_type {
TouchEventType::Down => {},
TouchEventType::Move => {
if let Some(action) =
self.touch_handler.pending_touch_move_action(sequence_id)
{
match action {
TouchMoveAction::Scroll(delta, point) => self
.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(
delta.to_untyped(),
)),
point.cast(),
),
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1);
// Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(
scroll_delta.to_untyped(),
),
),
cursor,
event_count: 1,
}));
},
TouchMoveAction::NoAction => {
// This shouldn't happen, but we can also just ignore it.
},
}
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
}
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, false);
if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) {
if info.prevent_move == TouchMoveAllowed::Pending {
info.prevent_move = TouchMoveAllowed::Allowed;
if let TouchSequenceState::PendingFling { velocity, cursor } =
info.state
{
info.state = TouchSequenceState::Flinging { velocity, cursor }
}
}
}
},
TouchEventType::Up => {
let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id)
else {
// The sequence was already removed because there is no default action.
return;
};
match info.state {
TouchSequenceState::PendingClick(point) => {
info.state = TouchSequenceState::Finished;
// PreventDefault from touch_down may have been processed after
// touch_up already occurred.
if !info.prevent_click {
self.simulate_mouse_click(point);
}
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
TouchSequenceState::MultiTouch | TouchSequenceState::Touching => {
// We transitioned to touching from multi-touch or pinching.
},
}
},
TouchEventType::Cancel => {
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
self.touch_handler.try_remove_touch_sequence(sequence_id);
},
}
},
}
}
/// <http://w3c.github.io/touch-events/#mouse-events>
fn simulate_mouse_click(&mut self, point: DevicePoint) {
let button = MouseButton::Left;
self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
button,
action: MouseButtonAction::Down,
point,
}));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
button,
action: MouseButtonAction::Up,
point,
}));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
button,
action: MouseButtonAction::Click,
point,
}));
}
pub fn notify_scroll_event(
&mut self,
scroll_location: ScrollLocation,
cursor: DeviceIntPoint,
event_type: TouchEventType,
) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
match event_type {
TouchEventType::Move => self.on_scroll_window_event(scroll_location, cursor),
TouchEventType::Up | TouchEventType::Cancel => {
self.on_scroll_window_event(scroll_location, cursor);
},
TouchEventType::Down => {
self.on_scroll_window_event(scroll_location, cursor);
},
}
}
fn on_scroll_window_event(&mut self, scroll_location: ScrollLocation, cursor: DeviceIntPoint) {
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location,
cursor,
event_count: 1,
}));
}
pub(crate) fn process_pending_scroll_events(&mut self, compositor: &mut IOCompositor) {
if self.pending_scroll_zoom_events.is_empty() {
return;
}
// Batch up all scroll events into one, or else we'll do way too much painting.
let mut combined_scroll_event: Option<ScrollEvent> = None;
let mut combined_magnification = 1.0;
for scroll_event in self.pending_scroll_zoom_events.drain(..) {
match scroll_event {
ScrollZoomEvent::PinchZoom(magnification) => {
combined_magnification *= magnification
},
ScrollZoomEvent::Scroll(scroll_event_info) => {
let combined_event = match combined_scroll_event.as_mut() {
None => {
combined_scroll_event = Some(scroll_event_info);
continue;
},
Some(combined_event) => combined_event,
};
match (
combined_event.scroll_location,
scroll_event_info.scroll_location,
) {
(ScrollLocation::Delta(old_delta), ScrollLocation::Delta(new_delta)) => {
// Mac OS X sometimes delivers scroll events out of vsync during a
// fling. This causes events to get bunched up occasionally, causing
// nasty-looking "pops". To mitigate this, during a fling we average
// deltas instead of summing them.
let old_event_count = Scale::new(combined_event.event_count as f32);
combined_event.event_count += 1;
let new_event_count = Scale::new(combined_event.event_count as f32);
combined_event.scroll_location = ScrollLocation::Delta(
(old_delta * old_event_count + new_delta) / new_event_count,
);
},
(ScrollLocation::Start, _) | (ScrollLocation::End, _) => {
// Once we see Start or End, we shouldn't process any more events.
break;
},
(_, ScrollLocation::Start) | (_, ScrollLocation::End) => {
// If this is an event which is scrolling to the start or end of the page,
// disregard other pending events and exit the loop.
*combined_event = scroll_event_info;
break;
},
}
},
}
}
let zoom_changed = compositor
.set_pinch_zoom_level(compositor.pinch_zoom_level().get() * combined_magnification);
let scroll_result = combined_scroll_event.and_then(|combined_event| {
self.scroll_node_at_device_point(
combined_event.cursor.to_f32(),
combined_event.scroll_location,
compositor,
)
});
if !zoom_changed && scroll_result.is_none() {
return;
}
let mut transaction = Transaction::new();
if zoom_changed {
compositor.send_root_pipeline_display_list_in_transaction(&mut transaction);
}
if let Some((pipeline_id, external_id, offset)) = scroll_result {
let offset = LayoutVector2D::new(-offset.x, -offset.y);
transaction.set_scroll_offsets(
external_id,
vec![SampledScrollOffset {
offset,
generation: 0,
}],
);
self.send_scroll_positions_to_layout_for_pipeline(pipeline_id);
}
compositor.generate_frame(&mut transaction, RenderReasons::APZ);
self.global.borrow_mut().send_transaction(transaction);
}
/// Perform a hit test at the given [`DevicePoint`] and apply the [`ScrollLocation`]
/// scrolling to the applicable scroll node under that point. If a scroll was
/// performed, returns the [`PipelineId`] of the node scrolled, the id, and the final
/// scroll delta.
fn scroll_node_at_device_point(
&mut self,
cursor: DevicePoint,
scroll_location: ScrollLocation,
compositor: &mut IOCompositor,
) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> {
let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => {
let device_pixels_per_page = compositor.device_pixels_per_page_pixel();
let scaled_delta = (Vector2D::from_untyped(delta.to_untyped()) /
device_pixels_per_page)
.to_untyped();
let calculated_delta = LayoutVector2D::from_untyped(scaled_delta);
ScrollLocation::Delta(calculated_delta)
},
// Leave ScrollLocation unchanged if it is Start or End location.
ScrollLocation::Start | ScrollLocation::End => scroll_location,
};
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let hit_test_results = self
.global
.borrow()
.hit_test_at_point_with_flags_and_pipeline(
cursor,
HitTestFlags::FIND_ALL,
None,
get_pipeline_details,
);
// Iterate through all hit test results, processing only the first node of each pipeline.
// This is needed to propagate the scroll events from a pipeline representing an iframe to
// its ancestor pipelines.
let mut previous_pipeline_id = None;
for CompositorHitTestResult {
pipeline_id,
scroll_tree_node,
..
} in hit_test_results.iter()
{
let pipeline_details = self.pipelines.get_mut(pipeline_id)?;
if previous_pipeline_id.replace(pipeline_id) != Some(pipeline_id) {
let scroll_result = pipeline_details
.scroll_tree
.scroll_node_or_ancestor(scroll_tree_node, scroll_location);
if let Some((external_id, offset)) = scroll_result {
return Some((*pipeline_id, external_id, offset));
}
}
}
None
}
/// Simulate a pinch zoom
pub fn set_pinch_zoom(&mut self, magnification: f32) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
// TODO: Scroll to keep the center in view?
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UnknownWebView(pub WebViewId);

View file

@ -7,7 +7,7 @@ use std::collections::hash_map::{Entry, Values, ValuesMut};
use base::id::WebViewId;
use crate::webview::UnknownWebView;
use crate::webview_renderer::UnknownWebView;
#[derive(Debug)]
pub struct WebViewManager<WebView> {
@ -111,19 +111,15 @@ impl<WebView> WebViewManager<WebView> {
#[cfg(test)]
mod test {
use std::num::NonZeroU32;
use base::id::{BrowsingContextId, Index, PipelineNamespace, PipelineNamespaceId, WebViewId};
use base::id::{
BrowsingContextId, BrowsingContextIndex, PipelineNamespace, PipelineNamespaceId, WebViewId,
};
use crate::webview::UnknownWebView;
use crate::webview_manager::WebViewManager;
use crate::webview_renderer::UnknownWebView;
fn top_level_id(namespace_id: u32, index: u32) -> WebViewId {
WebViewId(BrowsingContextId {
namespace_id: PipelineNamespaceId(namespace_id),
index: BrowsingContextIndex(NonZeroU32::new(index).unwrap()),
index: Index::new(index).unwrap(),
})
}

View file

@ -0,0 +1,999 @@
/* 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::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::Keys;
use std::rc::Rc;
use base::id::{PipelineId, WebViewId};
use compositing_traits::{SendableFrameTree, WebViewTrait};
use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType};
use embedder_traits::{
AnimationState, CompositorHitTestResult, InputEvent, MouseButton, MouseButtonAction,
MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEvent, TouchEventResult, TouchEventType,
TouchId, ViewportDetails,
};
use euclid::{Box2D, Point2D, Scale, Size2D, Vector2D};
use fnv::FnvHashSet;
use log::{debug, warn};
use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor};
use webrender_api::units::{
DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D,
};
use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation};
use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
// Default viewport constraints
const MAX_ZOOM: f32 = 8.0;
const MIN_ZOOM: f32 = 0.1;
#[derive(Clone, Copy)]
struct ScrollEvent {
/// Scroll by this offset, or to Start or End
scroll_location: ScrollLocation,
/// Apply changes to the frame at this location
cursor: DeviceIntPoint,
/// The number of OS events that have been coalesced together into this one event.
event_count: u32,
}
#[derive(Clone, Copy)]
enum ScrollZoomEvent {
/// An pinch zoom event that magnifies the view by the given factor.
PinchZoom(f32),
/// A scroll event that scrolls the scroll node at the given location by the
/// given amount.
Scroll(ScrollEvent),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ScrollResult {
pub pipeline_id: PipelineId,
pub external_scroll_id: ExternalScrollId,
pub offset: LayoutVector2D,
}
#[derive(PartialEq)]
pub(crate) enum PinchZoomResult {
DidPinchZoom,
DidNotPinchZoom,
}
/// A renderer for a libservo `WebView`. This is essentially the [`ServoRenderer`]'s interface to a
/// libservo `WebView`, but the code here cannot depend on libservo in order to prevent circular
/// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself.
pub(crate) struct WebViewRenderer {
/// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`].
pub id: WebViewId,
/// The renderer's view of the embedding layer `WebView` as a trait implementation,
/// so that the renderer doesn't need to depend on the embedding layer. This avoids
/// a dependency cycle.
pub webview: Box<dyn WebViewTrait>,
/// The root [`PipelineId`] of the currently displayed page in this WebView.
pub root_pipeline_id: Option<PipelineId>,
pub rect: DeviceRect,
/// Tracks details about each active pipeline that the compositor knows about.
pub pipelines: HashMap<PipelineId, PipelineDetails>,
/// Data that is shared by all WebView renderers.
pub(crate) global: Rc<RefCell<ServoRenderer>>,
/// Pending scroll/zoom events.
pending_scroll_zoom_events: Vec<ScrollZoomEvent>,
/// Touch input state machine
touch_handler: TouchHandler,
/// "Desktop-style" zoom that resizes the viewport to fit the window.
pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>,
/// "Mobile-style" zoom that does not reflow the page.
viewport_zoom: PinchZoomFactor,
/// Viewport zoom constraints provided by @viewport.
min_viewport_zoom: Option<PinchZoomFactor>,
max_viewport_zoom: Option<PinchZoomFactor>,
/// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled
/// by the embedding layer.
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
/// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with
/// active animations or animation frame callbacks.
animating: bool,
}
impl Drop for WebViewRenderer {
fn drop(&mut self) {
self.global
.borrow_mut()
.pipeline_to_webview_map
.retain(|_, webview_id| self.id != *webview_id);
}
}
impl WebViewRenderer {
pub(crate) fn new(
global: Rc<RefCell<ServoRenderer>>,
renderer_webview: Box<dyn WebViewTrait>,
viewport_details: ViewportDetails,
) -> Self {
let hidpi_scale_factor = viewport_details.hidpi_scale_factor;
let size = viewport_details.size * viewport_details.hidpi_scale_factor;
Self {
id: renderer_webview.id(),
webview: renderer_webview,
root_pipeline_id: None,
rect: DeviceRect::from_origin_and_size(DevicePoint::origin(), size),
pipelines: Default::default(),
touch_handler: TouchHandler::new(),
global,
pending_scroll_zoom_events: Default::default(),
page_zoom: Scale::new(1.0),
viewport_zoom: PinchZoomFactor::new(1.0),
min_viewport_zoom: Some(PinchZoomFactor::new(1.0)),
max_viewport_zoom: None,
hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
animating: false,
}
}
pub(crate) fn animations_or_animation_callbacks_running(&self) -> bool {
self.pipelines
.values()
.any(PipelineDetails::animations_or_animation_callbacks_running)
}
pub(crate) fn animation_callbacks_running(&self) -> bool {
self.pipelines
.values()
.any(PipelineDetails::animation_callbacks_running)
}
pub(crate) fn pipeline_ids(&self) -> Keys<'_, PipelineId, PipelineDetails> {
self.pipelines.keys()
}
pub(crate) fn animating(&self) -> bool {
self.animating
}
/// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed.
pub(crate) fn ensure_pipeline_details(
&mut self,
pipeline_id: PipelineId,
) -> &mut PipelineDetails {
self.pipelines.entry(pipeline_id).or_insert_with(|| {
self.global
.borrow_mut()
.pipeline_to_webview_map
.insert(pipeline_id, self.id);
PipelineDetails::new()
})
}
pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
self.global
.borrow_mut()
.pipeline_to_webview_map
.remove(&pipeline_id);
self.pipelines.remove(&pipeline_id);
}
pub(crate) fn set_frame_tree(&mut self, frame_tree: &SendableFrameTree) {
let pipeline_id = frame_tree.pipeline.id;
let old_pipeline_id = std::mem::replace(&mut self.root_pipeline_id, Some(pipeline_id));
if old_pipeline_id != self.root_pipeline_id {
debug!(
"Updating webview ({:?}) from pipeline {:?} to {:?}",
3, old_pipeline_id, self.root_pipeline_id
);
}
self.set_frame_tree_on_pipeline_details(frame_tree, None);
self.reset_scroll_tree_for_unattached_pipelines(frame_tree);
self.send_scroll_positions_to_layout_for_pipeline(pipeline_id);
}
pub(crate) fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: PipelineId) {
let Some(details) = self.pipelines.get(&pipeline_id) else {
return;
};
let mut scroll_states = Vec::new();
details.scroll_tree.nodes.iter().for_each(|node| {
if let (Some(scroll_id), Some(scroll_offset)) = (node.external_id(), node.offset()) {
scroll_states.push(ScrollState {
scroll_id,
scroll_offset,
});
}
});
let _ = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states),
);
}
pub(crate) fn set_frame_tree_on_pipeline_details(
&mut self,
frame_tree: &SendableFrameTree,
parent_pipeline_id: Option<PipelineId>,
) {
let pipeline_id = frame_tree.pipeline.id;
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
pipeline_details.pipeline = Some(frame_tree.pipeline.clone());
pipeline_details.parent_pipeline_id = parent_pipeline_id;
for kid in &frame_tree.children {
self.set_frame_tree_on_pipeline_details(kid, Some(pipeline_id));
}
}
pub(crate) fn reset_scroll_tree_for_unattached_pipelines(
&mut self,
frame_tree: &SendableFrameTree,
) {
// TODO(mrobinson): Eventually this can selectively preserve the scroll trees
// state for some unattached pipelines in order to preserve scroll position when
// navigating backward and forward.
fn collect_pipelines(
pipelines: &mut FnvHashSet<PipelineId>,
frame_tree: &SendableFrameTree,
) {
pipelines.insert(frame_tree.pipeline.id);
for kid in &frame_tree.children {
collect_pipelines(pipelines, kid);
}
}
let mut attached_pipelines: FnvHashSet<PipelineId> = FnvHashSet::default();
collect_pipelines(&mut attached_pipelines, frame_tree);
self.pipelines
.iter_mut()
.filter(|(id, _)| !attached_pipelines.contains(id))
.for_each(|(_, details)| {
details.scroll_tree.nodes.iter_mut().for_each(|node| {
node.set_offset(LayoutVector2D::zero());
})
})
}
/// Sets or unsets the animations-running flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
pub(crate) fn change_pipeline_running_animations_state(
&mut self,
pipeline_id: PipelineId,
animation_state: AnimationState,
) -> bool {
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
match animation_state {
AnimationState::AnimationsPresent => {
pipeline_details.animations_running = true;
},
AnimationState::AnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = true;
},
AnimationState::NoAnimationsPresent => {
pipeline_details.animations_running = false;
},
AnimationState::NoAnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = false;
},
}
self.update_animation_state()
}
/// Sets or unsets the throttled flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) -> bool {
self.ensure_pipeline_details(pipeline_id).throttled = throttled;
// Throttling a pipeline can cause it to be taken into the "not-animating" state.
self.update_animation_state()
}
pub(crate) fn update_animation_state(&mut self) -> bool {
let animating = self.pipelines.values().any(PipelineDetails::animating);
let old_animating = std::mem::replace(&mut self.animating, animating);
self.webview.set_animating(self.animating);
old_animating != self.animating
}
/// On a Window refresh tick (e.g. vsync)
pub(crate) fn on_vsync(&mut self) {
if let Some(fling_action) = self.touch_handler.on_vsync() {
self.on_scroll_window_event(
ScrollLocation::Delta(fling_action.delta),
fling_action.cursor,
);
}
}
pub(crate) fn dispatch_input_event(&mut self, event: InputEvent) {
// Events that do not need to do hit testing are sent directly to the
// constellation to filter down.
let Some(point) = event.point() else {
return;
};
// If we can't find a pipeline to send this event to, we cannot continue.
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let Some(result) = self
.global
.borrow()
.hit_test_at_point(point, get_pipeline_details)
else {
return;
};
self.global.borrow_mut().update_cursor(point, &result);
if let Err(error) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)),
) {
warn!("Sending event to constellation failed ({error:?}).");
}
}
pub(crate) fn notify_input_event(&mut self, event: InputEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
if let InputEvent::Touch(event) = event {
self.on_touch_event(event);
return;
}
if self.global.borrow().convert_mouse_to_touch {
match event {
InputEvent::MouseButton(event) => {
match (event.button, event.action) {
(MouseButton::Left, MouseButtonAction::Down) => self.on_touch_down(
TouchEvent::new(TouchEventType::Down, TouchId(0), event.point),
),
(MouseButton::Left, MouseButtonAction::Up) => self.on_touch_up(
TouchEvent::new(TouchEventType::Up, TouchId(0), event.point),
),
_ => {},
}
return;
},
InputEvent::MouseMove(event) => {
if let Some(state) = self.touch_handler.try_get_current_touch_sequence() {
// We assume that the debug option `-Z convert-mouse-to-touch` will only
// be used on devices without native touch input, so we can directly
// reuse the touch handler for tracking the state of pressed buttons.
match state.state {
TouchSequenceState::Touching | TouchSequenceState::Panning { .. } => {
self.on_touch_move(TouchEvent::new(
TouchEventType::Move,
TouchId(0),
event.point,
));
},
TouchSequenceState::MultiTouch => {
// Multitouch simulation currently is not implemented.
// Since we only get one mouse move event, we would need to
// dispatch one mouse move event per currently pressed mouse button.
},
TouchSequenceState::Pinching => {
// We only have one mouse button, so Pinching should be impossible.
#[cfg(debug_assertions)]
log::error!(
"Touch handler is in Pinching state, which should be unreachable with \
-Z convert-mouse-to-touch debug option."
);
},
TouchSequenceState::PendingFling { .. } |
TouchSequenceState::Flinging { .. } |
TouchSequenceState::PendingClick(_) |
TouchSequenceState::Finished => {
// Mouse movement without a button being pressed is not
// translated to touch events.
},
}
}
// We don't want to (directly) dispatch mouse events when simulating touch input.
return;
},
_ => {},
}
}
self.dispatch_input_event(event);
}
fn send_touch_event(&self, mut event: TouchEvent) -> bool {
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let Some(result) = self
.global
.borrow()
.hit_test_at_point(event.point, get_pipeline_details)
else {
return false;
};
event.init_sequence_id(self.touch_handler.current_sequence_id);
let event = InputEvent::Touch(event);
if let Err(e) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)),
) {
warn!("Sending event to constellation failed ({:?}).", e);
false
} else {
true
}
}
pub(crate) fn on_touch_event(&mut self, event: TouchEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
match event.event_type {
TouchEventType::Down => self.on_touch_down(event),
TouchEventType::Move => self.on_touch_move(event),
TouchEventType::Up => self.on_touch_up(event),
TouchEventType::Cancel => self.on_touch_cancel(event),
}
}
fn on_touch_down(&mut self, event: TouchEvent) {
self.touch_handler.on_touch_down(event.id, event.point);
self.send_touch_event(event);
}
fn on_touch_move(&mut self, mut event: TouchEvent) {
let action: TouchMoveAction = self.touch_handler.on_touch_move(event.id, event.point);
if TouchMoveAction::NoAction != action {
// if first move processed and allowed, we directly process the move event,
// without waiting for the script handler.
if self
.touch_handler
.move_allowed(self.touch_handler.current_sequence_id)
{
// https://w3c.github.io/touch-events/#cancelability
event.disable_cancelable();
match action {
TouchMoveAction::Scroll(delta, point) => self.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(delta.to_untyped())),
point.cast(),
),
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(scroll_delta.to_untyped()),
),
cursor,
event_count: 1,
}));
},
_ => {},
}
}
// When the event is touchmove, if the script thread is processing the touch
// move event, we skip sending the event to the script thread.
// This prevents the script thread from stacking up for a large amount of time.
if !self
.touch_handler
.is_handling_touch_move(self.touch_handler.current_sequence_id) &&
self.send_touch_event(event) &&
event.is_cancelable()
{
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, true);
}
}
}
fn on_touch_up(&mut self, event: TouchEvent) {
self.touch_handler.on_touch_up(event.id, event.point);
self.send_touch_event(event);
}
fn on_touch_cancel(&mut self, event: TouchEvent) {
// Send the event to script.
self.touch_handler.on_touch_cancel(event.id, event.point);
self.send_touch_event(event);
}
pub(crate) fn on_touch_event_processed(&mut self, result: TouchEventResult) {
match result {
TouchEventResult::DefaultPrevented(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} prevented!",
event_type, sequence_id
);
match event_type {
TouchEventType::Down => {
// prevents both click and move
self.touch_handler.prevent_click(sequence_id);
self.touch_handler.prevent_move(sequence_id);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
},
TouchEventType::Move => {
// script thread processed the touch move event, mark this false.
if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) {
info.prevent_move = TouchMoveAllowed::Prevented;
if let TouchSequenceState::PendingFling { .. } = info.state {
info.state = TouchSequenceState::Finished;
}
self.touch_handler.set_handling_touch_move(
self.touch_handler.current_sequence_id,
false,
);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
}
},
TouchEventType::Up => {
// Note: We don't have to consider PendingFling here, since we handle that
// in the DefaultAllowed case of the touch_move event.
// Note: Removing can and should fail, if we still have an active Fling,
let Some(info) =
&mut self.touch_handler.get_touch_sequence_mut(sequence_id)
else {
// The sequence ID could already be removed, e.g. if Fling finished,
// before the touch_up event was handled (since fling can start
// immediately if move was previously allowed, and clicks are anyway not
// happening from fling).
return;
};
match info.state {
TouchSequenceState::PendingClick(_) => {
info.state = TouchSequenceState::Finished;
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Touching |
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::MultiTouch |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
}
},
TouchEventType::Cancel => {
// We could still have pending event handlers, so we remove the pending
// actions, and try to remove the touch sequence.
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
self.touch_handler.try_remove_touch_sequence(sequence_id);
},
}
},
TouchEventResult::DefaultAllowed(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} allowed",
event_type, sequence_id
);
match event_type {
TouchEventType::Down => {},
TouchEventType::Move => {
if let Some(action) =
self.touch_handler.pending_touch_move_action(sequence_id)
{
match action {
TouchMoveAction::Scroll(delta, point) => self
.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(
delta.to_untyped(),
)),
point.cast(),
),
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1);
// Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(
scroll_delta.to_untyped(),
),
),
cursor,
event_count: 1,
}));
},
TouchMoveAction::NoAction => {
// This shouldn't happen, but we can also just ignore it.
},
}
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
}
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, false);
if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) {
if info.prevent_move == TouchMoveAllowed::Pending {
info.prevent_move = TouchMoveAllowed::Allowed;
if let TouchSequenceState::PendingFling { velocity, cursor } =
info.state
{
info.state = TouchSequenceState::Flinging { velocity, cursor }
}
}
}
},
TouchEventType::Up => {
let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id)
else {
// The sequence was already removed because there is no default action.
return;
};
match info.state {
TouchSequenceState::PendingClick(point) => {
info.state = TouchSequenceState::Finished;
// PreventDefault from touch_down may have been processed after
// touch_up already occurred.
if !info.prevent_click {
self.simulate_mouse_click(point);
}
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
TouchSequenceState::MultiTouch | TouchSequenceState::Touching => {
// We transitioned to touching from multi-touch or pinching.
},
}
},
TouchEventType::Cancel => {
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
self.touch_handler.try_remove_touch_sequence(sequence_id);
},
}
},
}
}
/// <http://w3c.github.io/touch-events/#mouse-events>
fn simulate_mouse_click(&mut self, point: DevicePoint) {
let button = MouseButton::Left;
self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point)));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Down,
button,
point,
)));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Up,
button,
point,
)));
}
pub(crate) fn notify_scroll_event(
&mut self,
scroll_location: ScrollLocation,
cursor: DeviceIntPoint,
event_type: TouchEventType,
) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
match event_type {
TouchEventType::Move => self.on_scroll_window_event(scroll_location, cursor),
TouchEventType::Up | TouchEventType::Cancel => {
self.on_scroll_window_event(scroll_location, cursor);
},
TouchEventType::Down => {
self.on_scroll_window_event(scroll_location, cursor);
},
}
}
fn on_scroll_window_event(&mut self, scroll_location: ScrollLocation, cursor: DeviceIntPoint) {
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location,
cursor,
event_count: 1,
}));
}
/// Push scroll pending event when receiving wheel action from webdriver
pub(crate) fn on_webdriver_wheel_action(
&mut self,
scroll_delta: Vector2D<f32, DevicePixel>,
point: Point2D<f32, DevicePixel>,
) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
let scroll_location =
ScrollLocation::Delta(LayoutVector2D::from_untyped(scroll_delta.to_untyped()));
let cursor = DeviceIntPoint::new(point.x as i32, point.y as i32);
self.on_scroll_window_event(scroll_location, cursor)
}
/// Process pending scroll events for this [`WebViewRenderer`]. Returns a tuple containing:
///
/// - A boolean that is true if a zoom occurred.
/// - An optional [`ScrollResult`] if a scroll occurred.
///
/// It is up to the caller to ensure that these events update the rendering appropriately.
pub(crate) fn process_pending_scroll_and_pinch_zoom_events(
&mut self,
) -> (PinchZoomResult, Option<ScrollResult>) {
if self.pending_scroll_zoom_events.is_empty() {
return (PinchZoomResult::DidNotPinchZoom, None);
}
// Batch up all scroll events into one, or else we'll do way too much painting.
let mut combined_scroll_event: Option<ScrollEvent> = None;
let mut combined_magnification = 1.0;
for scroll_event in self.pending_scroll_zoom_events.drain(..) {
match scroll_event {
ScrollZoomEvent::PinchZoom(magnification) => {
combined_magnification *= magnification
},
ScrollZoomEvent::Scroll(scroll_event_info) => {
let combined_event = match combined_scroll_event.as_mut() {
None => {
combined_scroll_event = Some(scroll_event_info);
continue;
},
Some(combined_event) => combined_event,
};
match (
combined_event.scroll_location,
scroll_event_info.scroll_location,
) {
(ScrollLocation::Delta(old_delta), ScrollLocation::Delta(new_delta)) => {
// Mac OS X sometimes delivers scroll events out of vsync during a
// fling. This causes events to get bunched up occasionally, causing
// nasty-looking "pops". To mitigate this, during a fling we average
// deltas instead of summing them.
let old_event_count = Scale::new(combined_event.event_count as f32);
combined_event.event_count += 1;
let new_event_count = Scale::new(combined_event.event_count as f32);
combined_event.scroll_location = ScrollLocation::Delta(
(old_delta * old_event_count + new_delta) / new_event_count,
);
},
(ScrollLocation::Start, _) | (ScrollLocation::End, _) => {
// Once we see Start or End, we shouldn't process any more events.
break;
},
(_, ScrollLocation::Start) | (_, ScrollLocation::End) => {
// If this is an event which is scrolling to the start or end of the page,
// disregard other pending events and exit the loop.
*combined_event = scroll_event_info;
break;
},
}
},
}
}
let scroll_result = combined_scroll_event.and_then(|combined_event| {
self.scroll_node_at_device_point(
combined_event.cursor.to_f32(),
combined_event.scroll_location,
)
});
if let Some(scroll_result) = scroll_result {
self.send_scroll_positions_to_layout_for_pipeline(scroll_result.pipeline_id);
}
let pinch_zoom_result = match self
.set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification)
{
true => PinchZoomResult::DidPinchZoom,
false => PinchZoomResult::DidNotPinchZoom,
};
(pinch_zoom_result, scroll_result)
}
/// Perform a hit test at the given [`DevicePoint`] and apply the [`ScrollLocation`]
/// scrolling to the applicable scroll node under that point. If a scroll was
/// performed, returns the [`PipelineId`] of the node scrolled, the id, and the final
/// scroll delta.
fn scroll_node_at_device_point(
&mut self,
cursor: DevicePoint,
scroll_location: ScrollLocation,
) -> Option<ScrollResult> {
let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => {
let device_pixels_per_page = self.device_pixels_per_page_pixel();
let scaled_delta = (Vector2D::from_untyped(delta.to_untyped()) /
device_pixels_per_page)
.to_untyped();
let calculated_delta = LayoutVector2D::from_untyped(scaled_delta);
ScrollLocation::Delta(calculated_delta)
},
// Leave ScrollLocation unchanged if it is Start or End location.
ScrollLocation::Start | ScrollLocation::End => scroll_location,
};
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let hit_test_results = self
.global
.borrow()
.hit_test_at_point_with_flags_and_pipeline(
cursor,
HitTestFlags::FIND_ALL,
None,
get_pipeline_details,
);
// Iterate through all hit test results, processing only the first node of each pipeline.
// This is needed to propagate the scroll events from a pipeline representing an iframe to
// its ancestor pipelines.
let mut previous_pipeline_id = None;
for CompositorHitTestResult {
pipeline_id,
scroll_tree_node,
..
} in hit_test_results.iter()
{
let pipeline_details = self.pipelines.get_mut(pipeline_id)?;
if previous_pipeline_id.replace(pipeline_id) != Some(pipeline_id) {
let scroll_result = pipeline_details
.scroll_tree
.scroll_node_or_ancestor(scroll_tree_node, scroll_location);
if let Some((external_scroll_id, offset)) = scroll_result {
return Some(ScrollResult {
pipeline_id: *pipeline_id,
external_scroll_id,
offset,
});
}
}
}
None
}
pub(crate) fn pinch_zoom_level(&self) -> Scale<f32, DevicePixel, DevicePixel> {
Scale::new(self.viewport_zoom.get())
}
fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool {
if let Some(min) = self.min_viewport_zoom {
zoom = f32::max(min.get(), zoom);
}
if let Some(max) = self.max_viewport_zoom {
zoom = f32::min(max.get(), zoom);
}
let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom));
old_zoom != self.viewport_zoom
}
pub(crate) fn set_page_zoom(&mut self, magnification: f32) {
self.page_zoom =
Scale::new((self.page_zoom.get() * magnification).clamp(MIN_ZOOM, MAX_ZOOM));
}
pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
self.page_zoom * self.hidpi_scale_factor * self.pinch_zoom_level()
}
pub(crate) fn device_pixels_per_page_pixel_not_including_pinch_zoom(
&self,
) -> Scale<f32, CSSPixel, DevicePixel> {
self.page_zoom * self.hidpi_scale_factor
}
/// Simulate a pinch zoom
pub(crate) fn set_pinch_zoom(&mut self, magnification: f32) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
// TODO: Scroll to keep the center in view?
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
}
fn send_window_size_message(&self) {
// The device pixel ratio used by the style system should include the scale from page pixels
// to device pixels, but not including any pinch zoom.
let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom();
let initial_viewport = self.rect.size().to_f32() / device_pixel_ratio;
let msg = EmbedderToConstellationMessage::ChangeViewportDetails(
self.id,
ViewportDetails {
hidpi_scale_factor: device_pixel_ratio,
size: initial_viewport,
},
WindowSizeType::Resize,
);
if let Err(e) = self.global.borrow().constellation_sender.send(msg) {
warn!("Sending window resize to constellation failed ({:?}).", e);
}
}
/// Set the `hidpi_scale_factor` for this renderer, returning `true` if the value actually changed.
pub(crate) fn set_hidpi_scale_factor(
&mut self,
new_scale: Scale<f32, DeviceIndependentPixel, DevicePixel>,
) -> bool {
let old_scale_factor = std::mem::replace(&mut self.hidpi_scale_factor, new_scale);
if self.hidpi_scale_factor == old_scale_factor {
return false;
}
self.send_window_size_message();
true
}
/// Set the `rect` for this renderer, returning `true` if the value actually changed.
pub(crate) fn set_rect(&mut self, new_rect: DeviceRect) -> bool {
let old_rect = std::mem::replace(&mut self.rect, new_rect);
if old_rect.size() != self.rect.size() {
self.send_window_size_message();
}
old_rect != self.rect
}
pub(crate) fn client_window_rect(
&self,
rendering_context_size: Size2D<u32, DevicePixel>,
) -> Box2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.webview.screen_geometry().unwrap_or_default();
let rect = DeviceIntRect::from_origin_and_size(
screen_geometry.offset,
rendering_context_size.to_i32(),
)
.to_f32() /
self.hidpi_scale_factor;
rect.to_i32()
}
pub(crate) fn screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.webview.screen_geometry().unwrap_or_default();
(screen_geometry.size.to_f32() / self.hidpi_scale_factor).to_i32()
}
pub(crate) fn available_screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.webview.screen_geometry().unwrap_or_default();
(screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UnknownWebView(pub WebViewId);

View file

@ -1,69 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Abstract windowing methods. The concrete implementations of these can be found in `platform/`.
use std::fmt::Debug;
use embedder_traits::{EventLoopWaker, MouseButton};
use euclid::Scale;
use net::protocols::ProtocolRegistry;
use servo_geometry::DeviceIndependentPixel;
use webrender_api::units::{DevicePixel, DevicePoint};
#[derive(Clone)]
pub enum MouseWindowEvent {
Click(MouseButton, DevicePoint),
MouseDown(MouseButton, DevicePoint),
MouseUp(MouseButton, DevicePoint),
}
/// Various debug and profiling flags that WebRender supports.
#[derive(Clone)]
pub enum WebRenderDebugOption {
Profiler,
TextureCacheDebug,
RenderTargetDebug,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationState {
Idle,
Animating,
}
// TODO: this trait assumes that the window is responsible
// for creating the GL context, making it current, buffer
// swapping, etc. Really that should all be done by surfman.
pub trait WindowMethods {
/// Get the HighDPI factor of the native window, the screen and the framebuffer.
/// TODO(martin): Move this to `RendererWebView` when possible.
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
/// Set whether the application is currently animating.
/// Typically, when animations are active, the window
/// will want to avoid blocking on UI events, and just
/// run the event loop at the vsync interval.
/// TODO(martin): Move this to `RendererWebView` when possible.
fn set_animation_state(&self, _state: AnimationState);
}
pub trait EmbedderMethods {
/// Returns a thread-safe object to wake up the window's event loop.
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker>;
#[cfg(feature = "webxr")]
/// Register services with a WebXR Registry.
fn register_webxr(
&mut self,
_: &mut webxr::MainThreadRegistry,
_: embedder_traits::EmbedderProxy,
) {
}
/// Returns the protocol handlers implemented by that embedder.
/// They will be merged with the default internal ones.
fn get_protocol_handlers(&self) -> ProtocolRegistry {
ProtocolRegistry::default()
}
}

View file

@ -104,7 +104,7 @@ pub struct DebugOptions {
/// Dumps the rule tree.
pub dump_rule_tree: bool,
/// Print the flow tree (Layout 2013) or fragment tree (Layout 2020) after each layout.
/// Print the fragment tree after each layout.
pub dump_flow_tree: bool,
/// Print the stacking context tree after each layout.

View file

@ -69,6 +69,7 @@ pub struct Preferences {
/// List of comma-separated backends to be used by wgpu.
pub dom_webgpu_wgpu_backend: String,
pub dom_abort_controller_enabled: bool,
pub dom_async_clipboard_enabled: bool,
pub dom_bluetooth_enabled: bool,
pub dom_bluetooth_testing_enabled: bool,
pub dom_allow_scripts_to_close_windows: bool,
@ -81,7 +82,6 @@ pub struct Preferences {
pub dom_document_dblclick_timeout: i64,
pub dom_document_dblclick_dist: i64,
pub dom_fontface_enabled: bool,
pub dom_forcetouch_enabled: bool,
pub dom_fullscreen_test: bool,
pub dom_gamepad_enabled: bool,
pub dom_imagebitmap_enabled: bool,
@ -99,7 +99,6 @@ pub struct Preferences {
pub dom_serviceworker_timeout_seconds: i64,
pub dom_servo_helpers_enabled: bool,
pub dom_servoparser_async_html_tokenizer_enabled: bool,
pub dom_shadowdom_enabled: bool,
pub dom_svg_enabled: bool,
pub dom_testable_crash_enabled: bool,
pub dom_testbinding_enabled: bool,
@ -116,10 +115,7 @@ pub struct Preferences {
pub dom_testperf_enabled: bool,
// https://testutils.spec.whatwg.org#availability
pub dom_testutils_enabled: bool,
/// Enable the [URLPattern] API.
///
/// [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
pub dom_urlpattern_enabled: bool,
pub dom_trusted_types_enabled: bool,
pub dom_xpath_enabled: bool,
/// Enable WebGL2 APIs.
pub dom_webgl2_enabled: bool,
@ -235,6 +231,8 @@ pub struct Preferences {
/// The user-agent to use for Servo. This can also be set via [`UserAgentPlatform`] in
/// order to set the value to the default value for the given platform.
pub user_agent: String,
pub log_filter: String,
}
impl Preferences {
@ -245,6 +243,7 @@ impl Preferences {
devtools_server_port: 0,
dom_abort_controller_enabled: false,
dom_allow_scripts_to_close_windows: false,
dom_async_clipboard_enabled: false,
dom_bluetooth_enabled: false,
dom_bluetooth_testing_enabled: false,
dom_canvas_capture_enabled: false,
@ -256,7 +255,6 @@ impl Preferences {
dom_document_dblclick_dist: 1,
dom_document_dblclick_timeout: 300,
dom_fontface_enabled: false,
dom_forcetouch_enabled: false,
dom_fullscreen_test: false,
dom_gamepad_enabled: true,
dom_imagebitmap_enabled: false,
@ -274,7 +272,6 @@ impl Preferences {
dom_serviceworker_timeout_seconds: 60,
dom_servo_helpers_enabled: false,
dom_servoparser_async_html_tokenizer_enabled: false,
dom_shadowdom_enabled: true,
dom_svg_enabled: false,
dom_testable_crash_enabled: false,
dom_testbinding_enabled: false,
@ -290,7 +287,7 @@ impl Preferences {
dom_testing_html_input_element_select_files_enabled: false,
dom_testperf_enabled: false,
dom_testutils_enabled: false,
dom_urlpattern_enabled: false,
dom_trusted_types_enabled: false,
dom_webgl2_enabled: false,
dom_webgpu_enabled: false,
dom_webgpu_wgpu_backend: String::new(),
@ -396,6 +393,7 @@ impl Preferences {
threadpools_webrender_workers_max: 4,
webgl_testing_context_creation_error: false,
user_agent: String::new(),
log_filter: String::new(),
}
}
}

View file

@ -14,7 +14,6 @@ path = "lib.rs"
[features]
bluetooth = ["bluetooth_traits"]
default = []
multiview = []
tracing = ["dep:tracing"]
webgpu = ["script_traits/webgpu"]
@ -53,7 +52,6 @@ webgpu = { path = "../webgpu" }
webgpu_traits = { workspace = true }
webrender = { workspace = true }
webrender_api = { workspace = true }
webrender_traits = { workspace = true }
webxr-api = { workspace = true, features = ["ipc"] }
[target.'cfg(any(target_os="macos", all(not(target_os = "windows"), not(target_os = "ios"), not(target_os="android"), not(target_env="ohos"), not(target_arch="arm"), not(target_arch="aarch64"))))'.dependencies]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
/* 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 base::id::BrowsingContextId;
use embedder_traits::Theme;
use crate::session_history::JointSessionHistory;
/// The `Constellation`'s view of a `WebView` in the embedding layer. This tracks all of the
/// `Constellation` state for this `WebView`.
pub(crate) struct ConstellationWebView {
/// The currently focused browsing context in this webview for key events.
/// The focused pipeline is the current entry of the focused browsing
/// context.
pub focused_browsing_context_id: BrowsingContextId,
/// The joint session history for this webview.
pub session_history: JointSessionHistory,
/// The [`Theme`] that this [`ConstellationWebView`] uses. This is communicated to all
/// `ScriptThread`s so that they know how to render the contents of a particular `WebView.
theme: Theme,
}
impl ConstellationWebView {
pub(crate) fn new(focused_browsing_context_id: BrowsingContextId) -> Self {
Self {
focused_browsing_context_id,
session_history: JointSessionHistory::new(),
theme: Theme::Light,
}
}
/// Set the [`Theme`] on this [`ConstellationWebView`] returning true if the theme changed.
pub(crate) fn set_theme(&mut self, new_theme: Theme) -> bool {
let old_theme = std::mem::replace(&mut self.theme, new_theme);
old_theme != self.theme
}
/// Get the [`Theme`] of this [`ConstellationWebView`].
pub(crate) fn theme(&self) -> Theme {
self.theme
}
}

View file

@ -6,17 +6,36 @@
//! view of a script thread. When an `EventLoop` is dropped, an `ExitScriptThread`
//! message is sent to the script thread, asking it to shut down.
use std::hash::Hash;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use ipc_channel::Error;
use ipc_channel::ipc::IpcSender;
use script_traits::ScriptThreadMessage;
static CURRENT_EVENT_LOOP_ID: AtomicUsize = AtomicUsize::new(0);
/// <https://html.spec.whatwg.org/multipage/#event-loop>
pub struct EventLoop {
script_chan: IpcSender<ScriptThreadMessage>,
dont_send_or_sync: PhantomData<Rc<()>>,
id: usize,
}
impl PartialEq for EventLoop {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for EventLoop {}
impl Hash for EventLoop {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Drop for EventLoop {
@ -28,9 +47,11 @@ impl Drop for EventLoop {
impl EventLoop {
/// Create a new event loop from the channel to its script thread.
pub fn new(script_chan: IpcSender<ScriptThreadMessage>) -> Rc<EventLoop> {
let id = CURRENT_EVENT_LOOP_ID.fetch_add(1, Ordering::Relaxed);
Rc::new(EventLoop {
script_chan,
dont_send_or_sync: PhantomData,
id,
})
}

View file

@ -9,6 +9,7 @@ mod tracing;
mod browsingcontext;
mod constellation;
mod constellation_webview;
mod event_loop;
mod logging;
mod pipeline;

View file

@ -12,11 +12,13 @@ use std::thread;
use backtrace::Backtrace;
use base::id::WebViewId;
use constellation_traits::{EmbedderToConstellationMessage, LogEntry};
use constellation_traits::{
EmbedderToConstellationMessage, LogEntry, ScriptToConstellationChan,
ScriptToConstellationMessage,
};
use crossbeam_channel::Sender;
use log::{Level, LevelFilter, Log, Metadata, Record};
use parking_lot::ReentrantMutex;
use script_traits::{ScriptToConstellationChan, ScriptToConstellationMessage};
/// A logger directed at the constellation from content processes
/// #[derive(Clone)]

View file

@ -18,11 +18,14 @@ use base::id::{
#[cfg(feature = "bluetooth")]
use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLPipeline;
use compositing_traits::{CompositionPipeline, CompositorMsg, CompositorProxy};
use compositing_traits::{
CompositionPipeline, CompositorMsg, CompositorProxy, CrossProcessCompositorApi,
};
use constellation_traits::{LoadData, SWManagerMsg, ScriptToConstellationChan};
use crossbeam_channel::{Sender, unbounded};
use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
use embedder_traits::ViewportDetails;
use embedder_traits::user_content_manager::UserContentManager;
use embedder_traits::{AnimationState, FocusSequenceNumber, Theme, ViewportDetails};
use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender};
use ipc_channel::Error;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@ -37,15 +40,13 @@ use profile_traits::mem::{ProfilerMsg, Reporter};
use profile_traits::{mem as profile_mem, time};
use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::{
AnimationState, DiscardBrowsingContext, DocumentActivity, InitialScriptState, LoadData,
NewLayoutInfo, SWManagerMsg, ScriptThreadMessage, ScriptToConstellationChan,
DiscardBrowsingContext, DocumentActivity, InitialScriptState, NewLayoutInfo,
ScriptThreadMessage,
};
use serde::{Deserialize, Serialize};
use servo_config::opts::{self, Opts};
use servo_config::prefs::{self, Preferences};
use servo_url::ServoUrl;
use webrender_api::DocumentId;
use webrender_traits::CrossProcessCompositorApi;
use crate::event_loop::EventLoop;
use crate::process_manager::Process;
@ -60,7 +61,7 @@ pub struct Pipeline {
/// The ID of the browsing context that contains this Pipeline.
pub browsing_context_id: BrowsingContextId,
/// The ID of the top-level browsing context that contains this Pipeline.
/// The [`WebViewId`] of the `WebView` that contains this Pipeline.
pub webview_id: WebViewId,
pub opener: Option<BrowsingContextId>,
@ -101,6 +102,8 @@ pub struct Pipeline {
/// The last compositor [`Epoch`] that was laid out in this pipeline if "exit after load" is
/// enabled.
pub layout_epoch: Epoch,
pub focus_sequence: FocusSequenceNumber,
}
/// Initial setup data needed to construct a pipeline.
@ -167,6 +170,9 @@ pub struct InitialPipelineState {
/// The initial [`ViewportDetails`] to use when starting this new [`Pipeline`].
pub viewport_details: ViewportDetails,
/// The initial [`Theme`] to use when starting this new [`Pipeline`].
pub theme: Theme,
/// The ID of the pipeline namespace for this script thread.
pub pipeline_namespace_id: PipelineNamespaceId,
@ -182,9 +188,6 @@ pub struct InitialPipelineState {
/// compositor threads after spawning a pipeline.
pub prev_throttled: bool,
/// The ID of the document processed by this script thread.
pub webrender_document: DocumentId,
/// A channel to the WebGL thread.
pub webgl_chan: Option<WebGLPipeline>,
@ -224,6 +227,7 @@ impl Pipeline {
opener: state.opener,
load_data: state.load_data.clone(),
viewport_details: state.viewport_details,
theme: state.theme,
};
if let Err(e) = script_chan.send(ScriptThreadMessage::AttachLayout(new_layout_info))
@ -280,13 +284,13 @@ impl Pipeline {
time_profiler_chan: state.time_profiler_chan,
mem_profiler_chan: state.mem_profiler_chan,
viewport_details: state.viewport_details,
theme: state.theme,
script_chan: script_chan.clone(),
load_data: state.load_data.clone(),
script_port,
opts: (*opts::get()).clone(),
prefs: Box::new(prefs::get().clone()),
pipeline_namespace_id: state.pipeline_namespace_id,
webrender_document: state.webrender_document,
cross_process_compositor_api: state
.compositor_proxy
.cross_process_compositor_api
@ -373,6 +377,7 @@ impl Pipeline {
completely_loaded: false,
title: String::new(),
layout_epoch: Epoch(0),
focus_sequence: FocusSequenceNumber::default(),
};
pipeline.set_throttled(throttled);
@ -494,6 +499,7 @@ pub struct UnprivilegedPipelineContent {
time_profiler_chan: time::ProfilerChan,
mem_profiler_chan: profile_mem::ProfilerChan,
viewport_details: ViewportDetails,
theme: Theme,
script_chan: IpcSender<ScriptThreadMessage>,
load_data: LoadData,
script_port: IpcReceiver<ScriptThreadMessage>,
@ -501,7 +507,6 @@ pub struct UnprivilegedPipelineContent {
prefs: Box<Preferences>,
pipeline_namespace_id: PipelineNamespaceId,
cross_process_compositor_api: CrossProcessCompositorApi,
webrender_document: DocumentId,
webgl_chan: Option<WebGLPipeline>,
webxr_registry: Option<webxr_api::Registry>,
player_context: WindowGLContext,
@ -545,11 +550,11 @@ impl UnprivilegedPipelineContent {
memory_profiler_sender: self.mem_profiler_chan.clone(),
devtools_server_sender: self.devtools_ipc_sender,
viewport_details: self.viewport_details,
theme: self.theme,
pipeline_namespace_id: self.pipeline_namespace_id,
content_process_shutdown_sender: content_process_shutdown_chan,
webgl_chan: self.webgl_chan,
webxr_registry: self.webxr_registry,
webrender_document: self.webrender_document,
compositor_api: self.cross_process_compositor_api.clone(),
player_context: self.player_context.clone(),
inherited_secure_context: self.load_data.inherited_secure_context,

View file

@ -159,6 +159,7 @@ pub fn spawn_multiprocess(content: UnprivilegedContent) -> Result<Process, Error
let mut child_process = process::Command::new(path_to_self);
setup_common(&mut child_process, token);
#[allow(clippy::zombie_processes)]
let child = child_process
.spawn()
.expect("Failed to start unsandboxed child process!");

View file

@ -2,9 +2,9 @@
* 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 constellation_traits::{SWManagerSenders, ServiceWorkerManagerFactory};
use ipc_channel::Error;
use ipc_channel::ipc::IpcSender;
use script_traits::{SWManagerSenders, ServiceWorkerManagerFactory};
use serde::{Deserialize, Serialize};
use servo_config::opts::{self, Opts};
use servo_config::prefs;

View file

@ -6,9 +6,9 @@ use std::cmp::PartialEq;
use std::fmt;
use base::id::{BrowsingContextId, HistoryStateId, PipelineId, WebViewId};
use constellation_traits::LoadData;
use embedder_traits::ViewportDetails;
use log::debug;
use script_traits::LoadData;
use servo_url::ServoUrl;
use crate::browsingcontext::NewBrowsingContextInfo;

View file

@ -77,6 +77,8 @@ mod from_compositor {
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
Self::SetScrollStates(..) => target!("SetScrollStates"),
Self::PaintMetric(..) => target!("PaintMetric"),
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),
Self::CreateMemoryReport(..) => target!("CreateMemoryReport"),
}
}
}
@ -113,7 +115,7 @@ mod from_script {
};
}
impl LogTarget for script_traits::ScriptToConstellationMessage {
impl LogTarget for constellation_traits::ScriptToConstellationMessage {
fn log_target(&self) -> &'static str {
match self {
Self::CompleteMessagePortTransfer(..) => target!("CompleteMessagePortTransfer"),
@ -123,8 +125,8 @@ mod from_script {
Self::RemoveMessagePortRouter(..) => target!("RemoveMessagePortRouter"),
Self::RerouteMessagePort(..) => target!("RerouteMessagePort"),
Self::MessagePortShipped(..) => target!("MessagePortShipped"),
Self::RemoveMessagePort(..) => target!("RemoveMessagePort"),
Self::EntanglePorts(..) => target!("EntanglePorts"),
Self::DisentanglePorts(..) => target!("DisentanglePorts"),
Self::NewBroadcastChannelRouter(..) => target!("NewBroadcastChannelRouter"),
Self::RemoveBroadcastChannelRouter(..) => target!("RemoveBroadcastChannelRouter"),
Self::NewBroadcastChannelNameInRouter(..) => {
@ -138,7 +140,8 @@ mod from_script {
Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"),
Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"),
Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"),
Self::Focus => target!("Focus"),
Self::Focus(..) => target!("Focus"),
Self::FocusRemoteDocument(..) => target!("FocusRemoteDocument"),
Self::GetTopForBrowsingContext(..) => target!("GetTopForBrowsingContext"),
Self::GetBrowsingContextInfo(..) => target!("GetBrowsingContextInfo"),
Self::GetChildBrowsingContextId(..) => target!("GetChildBrowsingContextId"),
@ -175,6 +178,8 @@ mod from_script {
Self::TitleChanged(..) => target!("TitleChanged"),
Self::IFrameSizes(..) => target!("IFrameSizes"),
Self::ReportMemory(..) => target!("ReportMemory"),
Self::WebDriverInputComplete(..) => target!("WebDriverInputComplete"),
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
}
}
}
@ -236,7 +241,10 @@ mod from_script {
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
Self::ShowNotification(..) => target_variant!("ShowNotification"),
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
Self::ShowFormControl(..) => target_variant!("ShowFormControl"),
Self::FinishJavaScriptEvaluation(..) => {
target_variant!("FinishJavaScriptEvaluation")
},
}
}
}

View file

@ -81,18 +81,14 @@ impl<WebView> WebViewManager<WebView> {
#[cfg(test)]
mod test {
use std::num::NonZeroU32;
use base::id::{
BrowsingContextId, BrowsingContextIndex, PipelineNamespace, PipelineNamespaceId, WebViewId,
};
use base::id::{BrowsingContextId, Index, PipelineNamespace, PipelineNamespaceId, WebViewId};
use crate::webview_manager::WebViewManager;
fn id(namespace_id: u32, index: u32) -> WebViewId {
WebViewId(BrowsingContextId {
namespace_id: PipelineNamespaceId(namespace_id),
index: BrowsingContextIndex(NonZeroU32::new(index).expect("Incorrect test case")),
index: Index::new(index).expect("Incorrect test case"),
})
}

View file

@ -0,0 +1,55 @@
/* 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 serde::Serialize;
use crate::EmptyReplyMsg;
use crate::actor::{Actor, ActorMessageStatus};
use crate::protocol::JsonPacketStream;
#[derive(Serialize)]
pub struct BreakpointListActorMsg {
actor: String,
}
pub struct BreakpointListActor {
name: String,
}
impl Actor for BreakpointListActor {
fn name(&self) -> String {
self.name.clone()
}
fn handle_message(
&self,
_registry: &crate::actor::ActorRegistry,
msg_type: &str,
_msg: &serde_json::Map<String, serde_json::Value>,
stream: &mut std::net::TcpStream,
_stream_id: crate::StreamId,
) -> Result<crate::actor::ActorMessageStatus, ()> {
Ok(match msg_type {
"setBreakpoint" => {
let msg = EmptyReplyMsg { from: self.name() };
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
"setActiveEventBreakpoints" => {
let msg = EmptyReplyMsg { from: self.name() };
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
}
impl BreakpointListActor {
pub fn new(name: String) -> Self {
Self { name }
}
}

View file

@ -11,8 +11,11 @@ use std::collections::HashMap;
use std::net::TcpStream;
use base::id::PipelineId;
use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, WantsLiveNotifications};
use devtools_traits::DevtoolScriptControlMsg::{
self, GetCssDatabase, SimulateColorScheme, WantsLiveNotifications,
};
use devtools_traits::{DevtoolsPageInfo, NavigationState};
use embedder_traits::Theme;
use ipc_channel::ipc::{self, IpcSender};
use serde::Serialize;
use serde_json::{Map, Value};
@ -28,8 +31,15 @@ use crate::actors::thread::ThreadActor;
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
use crate::protocol::JsonPacketStream;
use crate::resource::ResourceAvailable;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
struct ListWorkersReply {
from: String,
workers: Vec<()>,
}
#[derive(Serialize)]
struct FrameUpdateReply {
from: String,
@ -47,14 +57,6 @@ struct FrameUpdateMsg {
title: String,
}
#[derive(Serialize)]
struct ResourceAvailableReply<T: Serialize> {
from: String,
#[serde(rename = "type")]
type_: String,
array: Vec<(String, Vec<T>)>,
}
#[derive(Serialize)]
struct TabNavigated {
from: String,
@ -143,6 +145,12 @@ pub(crate) struct BrowsingContextActor {
pub watcher: String,
}
impl ResourceAvailable for BrowsingContextActor {
fn actor_name(&self) -> String {
self.name.clone()
}
}
impl Actor for BrowsingContextActor {
fn name(&self) -> String {
self.name.clone()
@ -163,6 +171,14 @@ impl Actor for BrowsingContextActor {
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
"listWorkers" => {
let _ = stream.write_json_packet(&ListWorkersReply {
from: self.name(),
// TODO: Find out what needs to be listed here
workers: vec![],
});
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}
@ -341,15 +357,9 @@ impl BrowsingContextActor {
});
}
pub(crate) fn resource_available<T: Serialize>(&self, message: T, resource_type: String) {
let msg = ResourceAvailableReply::<T> {
from: self.name(),
type_: "resources-available-array".into(),
array: vec![(resource_type, vec![message])],
};
for stream in self.streams.borrow_mut().values_mut() {
let _ = stream.write_json_packet(&msg);
}
pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
self.script_chan
.send(SimulateColorScheme(self.active_pipeline_id.get(), theme))
.map_err(|_| ())
}
}

View file

@ -30,6 +30,7 @@ use crate::actors::browsing_context::BrowsingContextActor;
use crate::actors::object::ObjectActor;
use crate::actors::worker::WorkerActor;
use crate::protocol::JsonPacketStream;
use crate::resource::ResourceAvailable;
use crate::{StreamId, UniqueId};
trait EncodableConsoleMessage {
@ -251,6 +252,7 @@ impl ConsoleActor {
page_error: PageError,
id: UniqueId,
registry: &ActorRegistry,
stream: &mut TcpStream,
) {
self.cached_events
.borrow_mut()
@ -261,7 +263,11 @@ impl ConsoleActor {
if let Root::BrowsingContext(bc) = &self.root {
registry
.find::<BrowsingContextActor>(bc)
.resource_available(PageErrorWrapper { page_error }, "error-message".into())
.resource_available(
PageErrorWrapper { page_error },
"error-message".into(),
stream,
)
};
}
}
@ -271,6 +277,7 @@ impl ConsoleActor {
console_message: ConsoleMessage,
id: UniqueId,
registry: &ActorRegistry,
stream: &mut TcpStream,
) {
let log_message: ConsoleLog = console_message.into();
self.cached_events
@ -282,7 +289,7 @@ impl ConsoleActor {
if let Root::BrowsingContext(bc) = &self.root {
registry
.find::<BrowsingContextActor>(bc)
.resource_available(log_message, "console-message".into())
.resource_available(log_message, "console-message".into(), stream)
};
}
}

View file

@ -166,6 +166,8 @@ impl Actor for InspectorActor {
if self.highlighter.borrow().is_none() {
let highlighter_actor = HighlighterActor {
name: registry.new_name("highlighter"),
pipeline,
script_sender: self.script_chan.clone(),
};
let mut highlighter = self.highlighter.borrow_mut();
*highlighter = Some(highlighter_actor.name());

View file

@ -7,6 +7,9 @@
use std::net::TcpStream;
use base::id::PipelineId;
use devtools_traits::DevtoolScriptControlMsg;
use ipc_channel::ipc::IpcSender;
use serde::Serialize;
use serde_json::{self, Map, Value};
@ -21,6 +24,8 @@ pub struct HighlighterMsg {
pub struct HighlighterActor {
pub name: String,
pub script_sender: IpcSender<DevtoolScriptControlMsg>,
pub pipeline: PipelineId,
}
#[derive(Serialize)]
@ -41,14 +46,39 @@ impl Actor for HighlighterActor {
/// - `hide`: Disables highlighting for the selected node
fn handle_message(
&self,
_registry: &ActorRegistry,
registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
msg: &Map<String, Value>,
stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"show" => {
let Some(node_actor) = msg.get("node") else {
// TODO: send missing parameter error
return Ok(ActorMessageStatus::Ignored);
};
let Some(node_actor_name) = node_actor.as_str() else {
// TODO: send invalid parameter error
return Ok(ActorMessageStatus::Ignored);
};
if node_actor_name.starts_with("inspector") {
// TODO: For some reason, the client initially asks us to highlight
// the inspector? Investigate what this is supposed to mean.
let msg = ShowReply {
from: self.name(),
value: false,
};
let _ = stream.write_json_packet(&msg);
return Ok(ActorMessageStatus::Processed);
}
self.instruct_script_thread_to_highlight_node(
Some(node_actor_name.to_owned()),
registry,
);
let msg = ShowReply {
from: self.name(),
value: true,
@ -58,6 +88,8 @@ impl Actor for HighlighterActor {
},
"hide" => {
self.instruct_script_thread_to_highlight_node(None, registry);
let msg = EmptyReplyMsg { from: self.name() };
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
@ -67,3 +99,19 @@ impl Actor for HighlighterActor {
})
}
}
impl HighlighterActor {
fn instruct_script_thread_to_highlight_node(
&self,
node_actor: Option<String>,
registry: &ActorRegistry,
) {
let node_id = node_actor.map(|node_actor| registry.actor_to_script(node_actor));
self.script_sender
.send(DevtoolScriptControlMsg::HighlightDomNode(
self.pipeline,
node_id,
))
.unwrap();
}
}

View file

@ -78,6 +78,18 @@ pub struct NodeActorMsg {
shadow_root_mode: Option<String>,
traits: HashMap<String, ()>,
attrs: Vec<AttrMsg>,
/// The `DOCTYPE` name if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
/// The `DOCTYPE` public identifier if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
public_id: Option<String>,
/// The `DOCTYPE` system identifier if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
system_id: Option<String>,
}
pub struct NodeActor {
@ -276,6 +288,9 @@ impl NodeInfoToProtocol for NodeInfo {
value: attr.value,
})
.collect(),
name: self.doctype_name,
public_id: self.doctype_public_identifier,
system_id: self.doctype_system_identifier,
}
}
}

View file

@ -0,0 +1,50 @@
/* 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::cell::{Ref, RefCell};
use std::collections::BTreeSet;
use serde::Serialize;
use servo_url::ServoUrl;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SourceData {
pub actor: String,
/// URL of the script, or URL of the page for inline scripts.
pub url: String,
pub is_black_boxed: bool,
}
#[derive(Serialize)]
pub(crate) struct SourcesReply {
pub from: String,
pub sources: Vec<SourceData>,
}
pub(crate) struct Source {
actor_name: String,
source_urls: RefCell<BTreeSet<SourceData>>,
}
impl Source {
pub fn new(actor_name: String) -> Self {
Self {
actor_name,
source_urls: RefCell::new(BTreeSet::default()),
}
}
pub fn add_source(&self, url: ServoUrl) {
self.source_urls.borrow_mut().insert(SourceData {
actor: self.actor_name.clone(),
url: url.to_string(),
is_black_boxed: false,
});
}
pub fn sources(&self) -> Ref<BTreeSet<SourceData>> {
self.source_urls.borrow()
}
}

View file

@ -7,6 +7,7 @@ use std::net::TcpStream;
use serde::Serialize;
use serde_json::{Map, Value};
use super::source::{Source, SourcesReply};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::protocol::JsonPacketStream;
use crate::{EmptyReplyMsg, StreamId};
@ -49,22 +50,17 @@ struct ThreadInterruptedReply {
type_: String,
}
#[derive(Serialize)]
struct SourcesReply {
from: String,
sources: Vec<Source>,
}
#[derive(Serialize)]
enum Source {}
pub struct ThreadActor {
name: String,
pub name: String,
pub source_manager: Source,
}
impl ThreadActor {
pub fn new(name: String) -> ThreadActor {
ThreadActor { name }
ThreadActor {
name: name.clone(),
source_manager: Source::new(name),
}
}
}
@ -125,6 +121,8 @@ impl Actor for ThreadActor {
ActorMessageStatus::Processed
},
// Client has attached to the thread and wants to load script sources.
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
"sources" => {
let msg = SourcesReply {
from: self.name(),

View file

@ -19,8 +19,12 @@ use serde::Serialize;
use serde_json::{Map, Value};
use self::network_parent::{NetworkParentActor, NetworkParentActorMsg};
use super::breakpoint::BreakpointListActor;
use super::thread::ThreadActor;
use super::worker::WorkerMsg;
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::root::RootActor;
use crate::actors::watcher::target_configuration::{
TargetConfigurationActor, TargetConfigurationActorMsg,
};
@ -28,7 +32,8 @@ use crate::actors::watcher::thread_configuration::{
ThreadConfigurationActor, ThreadConfigurationActorMsg,
};
use crate::protocol::JsonPacketStream;
use crate::{EmptyReplyMsg, StreamId};
use crate::resource::ResourceAvailable;
use crate::{EmptyReplyMsg, StreamId, WorkerActor};
pub mod network_parent;
pub mod target_configuration;
@ -53,7 +58,7 @@ impl SessionContext {
supported_targets: HashMap::from([
("frame", true),
("process", false),
("worker", false),
("worker", true),
("service_worker", false),
("shared_worker", false),
]),
@ -78,7 +83,7 @@ impl SessionContext {
("network-event-stacktrace", false),
("reflow", false),
("stylesheet", false),
("source", false),
("source", true),
("thread-state", false),
("server-sent-event", false),
("websocket", false),
@ -100,12 +105,19 @@ pub enum SessionContextType {
_All,
}
#[derive(Serialize)]
#[serde(untagged)]
enum TargetActorMsg {
BrowsingContext(BrowsingContextActorMsg),
Worker(WorkerMsg),
}
#[derive(Serialize)]
struct WatchTargetsReply {
from: String,
#[serde(rename = "type")]
type_: String,
target: BrowsingContextActorMsg,
target: TargetActorMsg,
}
#[derive(Serialize)]
@ -133,6 +145,18 @@ struct GetThreadConfigurationActorReply {
configuration: ThreadConfigurationActorMsg,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetBreakpointListActorReply {
from: String,
breakpoint_list: GetBreakpointListActorReplyInner,
}
#[derive(Serialize)]
struct GetBreakpointListActorReplyInner {
actor: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DocumentEvent {
@ -198,16 +222,38 @@ impl Actor for WatcherActor {
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let root = registry.find::<RootActor>("root");
Ok(match msg_type {
"watchTargets" => {
let msg = WatchTargetsReply {
from: self.name(),
type_: "target-available-form".into(),
target: target.encodable(),
};
let _ = stream.write_json_packet(&msg);
// As per logs we either get targetType as "frame" or "worker"
let target_type = msg
.get("targetType")
.and_then(Value::as_str)
.unwrap_or("frame"); // default to "frame"
target.frame_update(stream);
if target_type == "frame" {
let msg = WatchTargetsReply {
from: self.name(),
type_: "target-available-form".into(),
target: TargetActorMsg::BrowsingContext(target.encodable()),
};
let _ = stream.write_json_packet(&msg);
target.frame_update(stream);
} else if target_type == "worker" {
for worker_name in &root.workers {
let worker = registry.find::<WorkerActor>(worker_name);
let worker_msg = WatchTargetsReply {
from: self.name(),
type_: "target-available-form".into(),
target: TargetActorMsg::Worker(worker.encodable()),
};
let _ = stream.write_json_packet(&worker_msg);
}
} else {
warn!("Unexpected target_type: {}", target_type);
return Ok(ActorMessageStatus::Ignored);
}
// Messages that contain a `type` field are used to send event callbacks, but they
// don't count as a reply. Since every message needs to be responded, we send an
@ -246,7 +292,28 @@ impl Actor for WatcherActor {
title: Some(target.title.borrow().clone()),
url: Some(target.url.borrow().clone()),
};
target.resource_available(event, "document-event".into());
target.resource_available(event, "document-event".into(), stream);
}
},
"source" => {
let thread_actor = registry.find::<ThreadActor>(&target.thread);
let sources = thread_actor.source_manager.sources();
target.resources_available(
sources.iter().collect(),
"source".into(),
stream,
);
for worker_name in &root.workers {
let worker = registry.find::<WorkerActor>(worker_name);
let thread = registry.find::<ThreadActor>(&worker.thread);
let worker_sources = thread.source_manager.sources();
worker.resources_available(
worker_sources.iter().collect(),
"source".into(),
stream,
);
}
},
"console-message" | "error-message" => {},
@ -295,6 +362,19 @@ impl Actor for WatcherActor {
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
},
"getBreakpointListActor" => {
let breakpoint_list_name = registry.new_name("breakpoint-list");
let breakpoint_list = BreakpointListActor::new(breakpoint_list_name.clone());
registry.register_later(Box::new(breakpoint_list));
let _ = stream.write_json_packet(&GetBreakpointListActorReply {
from: self.name(),
breakpoint_list: GetBreakpointListActorReplyInner {
actor: breakpoint_list_name,
},
});
ActorMessageStatus::Processed
},
_ => ActorMessageStatus::Ignored,
})
}

View file

@ -8,12 +8,16 @@
use std::collections::HashMap;
use std::net::TcpStream;
use embedder_traits::Theme;
use log::warn;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::actors::browsing_context::BrowsingContextActor;
use crate::actors::tab::TabDescriptorActor;
use crate::protocol::JsonPacketStream;
use crate::{EmptyReplyMsg, StreamId};
use crate::{EmptyReplyMsg, RootActor, StreamId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@ -44,15 +48,39 @@ impl Actor for TargetConfigurationActor {
/// - `updateConfiguration`: Receives new configuration flags from the devtools host.
fn handle_message(
&self,
_registry: &ActorRegistry,
registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
msg: &Map<String, Value>,
stream: &mut TcpStream,
_id: StreamId,
) -> Result<ActorMessageStatus, ()> {
Ok(match msg_type {
"updateConfiguration" => {
// TODO: Actually update configuration
let config = match msg.get("configuration").and_then(|v| v.as_object()) {
Some(config) => config,
None => {
let msg = EmptyReplyMsg { from: self.name() };
let _ = stream.write_json_packet(&msg);
return Ok(ActorMessageStatus::Processed);
},
};
if let Some(scheme) = config.get("colorSchemeSimulation").and_then(|v| v.as_str()) {
let theme = match scheme {
"dark" => Theme::Dark,
"light" => Theme::Light,
_ => Theme::Light,
};
let root_actor = registry.find::<RootActor>("root");
if let Some(tab_name) = root_actor.active_tab() {
let tab_actor = registry.find::<TabDescriptorActor>(&tab_name);
let browsing_context_name = tab_actor.browsing_context();
let browsing_context_actor =
registry.find::<BrowsingContextActor>(&browsing_context_name);
browsing_context_actor.simulate_color_scheme(theme)?;
} else {
warn!("No active tab for updateConfiguration");
}
}
let msg = EmptyReplyMsg { from: self.name() };
let _ = stream.write_json_packet(&msg);
ActorMessageStatus::Processed
@ -69,7 +97,7 @@ impl TargetConfigurationActor {
configuration: HashMap::new(),
supported_options: HashMap::from([
("cacheDisabled", false),
("colorSchemeSimulation", false),
("colorSchemeSimulation", true),
("customFormatters", false),
("customUserAgent", false),
("javascriptEnabled", false),

View file

@ -17,6 +17,7 @@ use servo_url::ServoUrl;
use crate::StreamId;
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
use crate::protocol::JsonPacketStream;
use crate::resource::ResourceAvailable;
#[derive(Clone, Copy)]
#[allow(dead_code)]
@ -43,16 +44,24 @@ impl WorkerActor {
actor: self.name.clone(),
console_actor: self.console.clone(),
thread_actor: self.thread.clone(),
worker_id: self.worker_id.0.to_string(),
id: self.worker_id.0.to_string(),
url: self.url.to_string(),
traits: WorkerTraits {
is_parent_intercept_enabled: false,
supports_top_level_target_flag: false,
},
type_: self.type_ as u32,
target_type: "worker".to_string(),
}
}
}
impl ResourceAvailable for WorkerActor {
fn actor_name(&self) -> String {
self.name.clone()
}
}
impl Actor for WorkerActor {
fn name(&self) -> String {
self.name.clone()
@ -149,6 +158,7 @@ struct ConnectReply {
#[serde(rename_all = "camelCase")]
struct WorkerTraits {
is_parent_intercept_enabled: bool,
supports_top_level_target_flag: bool,
}
#[derive(Serialize)]
@ -157,9 +167,11 @@ pub(crate) struct WorkerMsg {
actor: String,
console_actor: String,
thread_actor: String,
worker_id: String,
id: String,
url: String,
traits: WorkerTraits,
#[serde(rename = "type")]
type_: u32,
#[serde(rename = "targetType")]
target_type: String,
}

View file

@ -19,16 +19,18 @@ use std::net::{Shutdown, TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::thread;
use actors::source::SourceData;
use base::id::{BrowsingContextId, PipelineId, WebViewId};
use crossbeam_channel::{Receiver, Sender, unbounded};
use devtools_traits::{
ChromeToDevtoolsControlMsg, ConsoleMessage, ConsoleMessageBuilder, DevtoolScriptControlMsg,
DevtoolsControlMsg, DevtoolsPageInfo, LogLevel, NavigationState, NetworkEvent, PageError,
ScriptToDevtoolsControlMsg, WorkerId,
ScriptToDevtoolsControlMsg, SourceInfo, WorkerId,
};
use embedder_traits::{AllowOrDeny, EmbedderMsg, EmbedderProxy};
use ipc_channel::ipc::{self, IpcSender};
use log::trace;
use resource::ResourceAvailable;
use serde::Serialize;
use servo_rand::RngCore;
@ -51,6 +53,7 @@ use crate::protocol::JsonPacketStream;
mod actor;
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors>
mod actors {
pub mod breakpoint;
pub mod browsing_context;
pub mod console;
pub mod device;
@ -64,6 +67,7 @@ mod actors {
pub mod process;
pub mod reflow;
pub mod root;
pub mod source;
pub mod stylesheets;
pub mod tab;
pub mod thread;
@ -74,6 +78,7 @@ mod actors {
mod id;
mod network_handler;
mod protocol;
mod resource;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum UniqueId {
@ -247,6 +252,10 @@ impl DevtoolsInstance {
console_message,
worker_id,
)) => self.handle_console_message(pipeline_id, worker_id, console_message),
DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::ScriptSourceLoaded(
pipeline_id,
source_info,
)) => self.handle_script_source_info(pipeline_id, source_info),
DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::ReportPageError(
pipeline_id,
page_error,
@ -329,7 +338,7 @@ impl DevtoolsInstance {
assert!(self.pipelines.contains_key(&pipeline_id));
assert!(self.browsing_contexts.contains_key(&browsing_context_id));
let thread = ThreadActor::new(actors.new_name("context"));
let thread = ThreadActor::new(actors.new_name("thread"));
let thread_name = thread.name();
actors.register(Box::new(thread));
@ -406,7 +415,7 @@ impl DevtoolsInstance {
}
fn handle_page_error(
&self,
&mut self,
pipeline_id: PipelineId,
worker_id: Option<WorkerId>,
page_error: PageError,
@ -418,11 +427,13 @@ impl DevtoolsInstance {
let actors = self.actors.lock().unwrap();
let console_actor = actors.find::<ConsoleActor>(&console_actor_name);
let id = worker_id.map_or(UniqueId::Pipeline(pipeline_id), UniqueId::Worker);
console_actor.handle_page_error(page_error, id, &actors);
for stream in self.connections.values_mut() {
console_actor.handle_page_error(page_error.clone(), id.clone(), &actors, stream);
}
}
fn handle_console_message(
&self,
&mut self,
pipeline_id: PipelineId,
worker_id: Option<WorkerId>,
console_message: ConsoleMessage,
@ -434,7 +445,9 @@ impl DevtoolsInstance {
let actors = self.actors.lock().unwrap();
let console_actor = actors.find::<ConsoleActor>(&console_actor_name);
let id = worker_id.map_or(UniqueId::Pipeline(pipeline_id), UniqueId::Worker);
console_actor.handle_console_api(console_message, id, &actors);
for stream in self.connections.values_mut() {
console_actor.handle_console_api(console_message.clone(), id.clone(), &actors, stream);
}
}
fn find_console_actor(
@ -498,6 +511,65 @@ impl DevtoolsInstance {
},
}
}
fn handle_script_source_info(&mut self, pipeline_id: PipelineId, source_info: SourceInfo) {
let mut actors = self.actors.lock().unwrap();
if let Some(worker_id) = source_info.worker_id {
let Some(worker_actor_name) = self.actor_workers.get(&worker_id) else {
return;
};
let thread_actor_name = actors.find::<WorkerActor>(worker_actor_name).thread.clone();
let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name);
thread_actor
.source_manager
.add_source(source_info.url.clone());
let source = SourceData {
actor: thread_actor_name.clone(),
url: source_info.url.to_string(),
is_black_boxed: false,
};
let worker_actor = actors.find::<WorkerActor>(worker_actor_name);
for stream in self.connections.values_mut() {
worker_actor.resource_available(&source, "source".into(), stream);
}
} else {
let Some(browsing_context_id) = self.pipelines.get(&pipeline_id) else {
return;
};
let Some(actor_name) = self.browsing_contexts.get(browsing_context_id) else {
return;
};
let thread_actor_name = actors
.find::<BrowsingContextActor>(actor_name)
.thread
.clone();
let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name);
thread_actor
.source_manager
.add_source(source_info.url.clone());
let source = SourceData {
actor: thread_actor_name.clone(),
url: source_info.url.to_string(),
is_black_boxed: false,
};
// Notify browsing context about the new source
let browsing_context = actors.find::<BrowsingContextActor>(actor_name);
for stream in self.connections.values_mut() {
browsing_context.resource_available(&source, "source".into(), stream);
}
}
}
}
fn allow_devtools_client(stream: &mut TcpStream, embedder: &EmbedderProxy, token: &str) -> bool {

View file

@ -0,0 +1,45 @@
/* 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::net::TcpStream;
use serde::Serialize;
use crate::protocol::JsonPacketStream;
#[derive(Serialize)]
pub(crate) struct ResourceAvailableReply<T: Serialize> {
pub from: String,
#[serde(rename = "type")]
pub type_: String,
pub array: Vec<(String, Vec<T>)>,
}
pub(crate) trait ResourceAvailable {
fn actor_name(&self) -> String;
fn resource_available<T: Serialize>(
&self,
resource: T,
resource_type: String,
stream: &mut TcpStream,
) {
self.resources_available(vec![resource], resource_type, stream);
}
fn resources_available<T: Serialize>(
&self,
resources: Vec<T>,
resource_type: String,
stream: &mut TcpStream,
) {
let msg = ResourceAvailableReply::<T> {
from: self.actor_name(),
type_: "resources-available-array".into(),
array: vec![(resource_type, resources)],
};
let _ = stream.write_json_packet(&msg);
}
}

View file

@ -21,6 +21,7 @@ app_units = { workspace = true }
atomic_refcell = { workspace = true }
base = { workspace = true }
bitflags = { workspace = true }
compositing_traits = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
fonts_traits = { workspace = true }
@ -37,6 +38,7 @@ memmap2 = { workspace = true }
net_traits = { workspace = true }
num-traits = { workspace = true }
parking_lot = { workspace = true }
profile_traits = { workspace = true }
range = { path = "../range" }
serde = { workspace = true }
servo_arc = { workspace = true }
@ -50,7 +52,6 @@ unicode-properties = { workspace = true }
unicode-script = { workspace = true }
url = { workspace = true }
webrender_api = { workspace = true }
webrender_traits = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
byteorder = { workspace = true }

View file

@ -10,6 +10,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use app_units::Au;
use base::id::WebViewId;
use compositing_traits::CrossProcessCompositorApi;
use fnv::FnvHasher;
use fonts_traits::StylesheetWebFontLoadFinishedCallback;
use log::{debug, trace};
@ -32,7 +33,6 @@ use style::stylesheets::{CssRule, DocumentStyleSheet, FontFaceRule, StylesheetIn
use style::values::computed::font::{FamilyName, FontFamilyNameSyntax, SingleFontFamily};
use url::Url;
use webrender_api::{FontInstanceFlags, FontInstanceKey, FontKey};
use webrender_traits::CrossProcessCompositorApi;
use crate::font::{
Font, FontDescriptor, FontFamilyDescriptor, FontGroup, FontRef, FontSearchScope,
@ -900,9 +900,9 @@ impl RemoteWebFontDownloader {
response_message: FetchResponseMsg,
) -> DownloaderResponseResult {
match response_message {
FetchResponseMsg::ProcessRequestBody(..) | FetchResponseMsg::ProcessRequestEOF(..) => {
DownloaderResponseResult::InProcess
},
FetchResponseMsg::ProcessRequestBody(..) |
FetchResponseMsg::ProcessRequestEOF(..) |
FetchResponseMsg::ProcessCspViolations(..) => DownloaderResponseResult::InProcess,
FetchResponseMsg::ProcessResponse(_, meta_result) => {
trace!(
"@font-face {} metadata ok={:?}",

View file

@ -5,8 +5,8 @@
use std::collections::HashMap;
use std::sync::Arc;
use atomic_refcell::AtomicRefCell;
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use parking_lot::RwLock;
use style::stylesheets::DocumentStyleSheet;
use style::values::computed::{FontStyle, FontWeight};
@ -15,7 +15,7 @@ use crate::font::FontDescriptor;
use crate::font_template::{FontTemplate, FontTemplateRef, FontTemplateRefMethods, IsOblique};
use crate::system_font_service::{FontIdentifier, LowercaseFontFamilyName};
#[derive(Default)]
#[derive(Default, MallocSizeOf)]
pub struct FontStore {
pub(crate) families: HashMap<LowercaseFontFamilyName, FontTemplates>,
web_fonts_loading_for_stylesheets: Vec<(DocumentStyleSheet, usize)>,
@ -134,7 +134,7 @@ impl FontStore {
///
/// This optimization is taken from:
/// <https://searchfox.org/mozilla-central/source/gfx/thebes/gfxFontEntry.cpp>.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, MallocSizeOf)]
struct SimpleFamily {
regular: Option<FontTemplateRef>,
bold: Option<FontTemplateRef>,
@ -190,7 +190,7 @@ impl SimpleFamily {
}
}
/// A list of font templates that make up a given font family.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, MallocSizeOf)]
pub struct FontTemplates {
pub(crate) templates: Vec<FontTemplateRef>,
simple_family: Option<SimpleFamily>,
@ -263,7 +263,7 @@ impl FontTemplates {
}
}
let new_template = Arc::new(AtomicRefCell::new(new_template));
let new_template = FontTemplateRef::new(new_template);
self.templates.push(new_template.clone());
self.update_simple_family(new_template);
}

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fmt::{Debug, Error, Formatter};
use std::ops::RangeInclusive;
use std::ops::{Deref, RangeInclusive};
use std::sync::Arc;
use atomic_refcell::AtomicRefCell;
@ -20,7 +20,21 @@ use crate::system_font_service::{
};
/// A reference to a [`FontTemplate`] with shared ownership and mutability.
pub type FontTemplateRef = Arc<AtomicRefCell<FontTemplate>>;
#[derive(Clone, Debug, MallocSizeOf)]
pub struct FontTemplateRef(#[conditional_malloc_size_of] Arc<AtomicRefCell<FontTemplate>>);
impl FontTemplateRef {
pub fn new(template: FontTemplate) -> Self {
Self(Arc::new(AtomicRefCell::new(template)))
}
}
impl Deref for FontTemplateRef {
type Target = Arc<AtomicRefCell<FontTemplate>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Describes how to select a font from a given family. This is very basic at the moment and needs
/// to be expanded or refactored when we support more of the font styling parameters.

View file

@ -741,9 +741,10 @@ impl fmt::Debug for GlyphStore {
}
/// A single series of glyphs within a text run.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct GlyphRun {
/// The glyphs.
#[conditional_malloc_size_of]
pub glyph_store: Arc<GlyphStore>,
/// The byte range of characters in the containing run.
pub range: Range<ByteIndex>,

View file

@ -287,7 +287,7 @@ impl PlatformFontMethods for PlatformFont {
.unwrap_or(average_advance);
let metrics = FontMetrics {
underline_size: Au::from_f64_au(underline_thickness),
underline_size: Au::from_f64_px(underline_thickness),
// TODO(Issue #201): underline metrics are not reliable. Have to pull out of font table
// directly.
//

View file

@ -132,7 +132,9 @@ impl PlatformFontMethods for PlatformFont {
pt_size: Option<Au>,
) -> Result<PlatformFont, &'static str> {
let font_face = FontCollection::system()
.get_font_from_descriptor(&font_identifier.font_descriptor)
.font_from_descriptor(&font_identifier.font_descriptor)
.ok()
.flatten()
.ok_or("Could not create Font from descriptor")?
.create_font_face();
Self::new(font_face, pt_size)

View file

@ -25,7 +25,9 @@ where
{
let system_fc = FontCollection::system();
for family in system_fc.families_iter() {
callback(family.name());
if let Ok(family_name) = family.family_name() {
callback(family_name);
}
}
}
@ -40,13 +42,17 @@ pub struct LocalFontIdentifier {
impl LocalFontIdentifier {
pub fn index(&self) -> u32 {
FontCollection::system()
.get_font_from_descriptor(&self.font_descriptor)
.font_from_descriptor(&self.font_descriptor)
.ok()
.flatten()
.map_or(0, |font| font.create_font_face().get_index())
}
pub(crate) fn native_font_handle(&self) -> NativeFontHandle {
let face = FontCollection::system()
.get_font_from_descriptor(&self.font_descriptor)
.font_from_descriptor(&self.font_descriptor)
.ok()
.flatten()
.expect("Could not create Font from FontDescriptor")
.create_font_face();
let path = face
@ -62,7 +68,9 @@ impl LocalFontIdentifier {
}
pub(crate) fn read_data_from_file(&self) -> Option<Vec<u8>> {
let font = FontCollection::system().get_font_from_descriptor(&self.font_descriptor)?;
let font = FontCollection::system()
.font_from_descriptor(&self.font_descriptor)
.ok()??;
let face = font.create_font_face();
let files = face.get_files();
assert!(!files.is_empty());
@ -86,10 +94,12 @@ where
F: FnMut(FontTemplate),
{
let system_fc = FontCollection::system();
if let Some(family) = system_fc.get_font_family_by_name(family_name) {
if let Ok(Some(family)) = system_fc.font_family_by_name(family_name) {
let count = family.get_font_count();
for i in 0..count {
let font = family.get_font(i);
let Ok(font) = family.font(i) else {
continue;
};
let template_descriptor = (&font).into();
let local_font_identifier = LocalFontIdentifier {
font_descriptor: Arc::new(font.to_descriptor()),

View file

@ -6,15 +6,19 @@ use std::borrow::ToOwned;
use std::cell::OnceCell;
use std::collections::HashMap;
use std::ops::{Deref, RangeInclusive};
use std::sync::Arc;
use std::{fmt, thread};
use app_units::Au;
use atomic_refcell::AtomicRefCell;
use compositing_traits::CrossProcessCompositorApi;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use log::debug;
use malloc_size_of::MallocSizeOf as MallocSizeOfTrait;
use malloc_size_of_derive::MallocSizeOf;
use parking_lot::{Mutex, RwLock};
use profile_traits::mem::{
ProcessReports, ProfilerChan, Report, ReportKind, ReportsChan, perform_memory_report,
};
use profile_traits::path;
use serde::{Deserialize, Serialize};
use servo_config::pref;
use servo_url::ServoUrl;
@ -25,7 +29,6 @@ use style::values::computed::font::{
use style::values::computed::{FontStretch, FontWeight};
use style::values::specified::FontStretch as SpecifiedFontStretch;
use webrender_api::{FontInstanceFlags, FontInstanceKey, FontKey};
use webrender_traits::CrossProcessCompositorApi;
use crate::font::FontDescriptor;
use crate::font_store::FontStore;
@ -66,11 +69,12 @@ pub enum SystemFontServiceMessage {
),
GetFontKey(IpcSender<FontKey>),
GetFontInstanceKey(IpcSender<FontInstanceKey>),
CollectMemoryReport(ReportsChan),
Exit(IpcSender<()>),
Ping,
}
#[derive(Default)]
#[derive(Default, MallocSizeOf)]
struct ResolvedGenericFontFamilies {
default: OnceCell<LowercaseFontFamilyName>,
serif: OnceCell<LowercaseFontFamilyName>,
@ -84,6 +88,7 @@ struct ResolvedGenericFontFamilies {
/// The system font service. There is one of these for every Servo instance. This is a thread,
/// responsible for reading the list of system fonts, handling requests to match against
/// them, and ensuring that only one copy of system font data is loaded at a time.
#[derive(MallocSizeOf)]
pub struct SystemFontService {
port: IpcReceiver<SystemFontServiceMessage>,
local_families: FontStore,
@ -118,8 +123,12 @@ impl SystemFontServiceProxySender {
}
impl SystemFontService {
pub fn spawn(compositor_api: CrossProcessCompositorApi) -> SystemFontServiceProxySender {
pub fn spawn(
compositor_api: CrossProcessCompositorApi,
memory_profiler_sender: ProfilerChan,
) -> SystemFontServiceProxySender {
let (sender, receiver) = ipc::channel().unwrap();
let memory_reporter_sender = sender.clone();
thread::Builder::new()
.name("SystemFontService".to_owned())
@ -138,7 +147,13 @@ impl SystemFontService {
cache.fetch_new_keys();
cache.refresh_local_families();
cache.run();
memory_profiler_sender.run_with_memory_reporting(
|| cache.run(),
"system-fonts".to_owned(),
memory_reporter_sender,
SystemFontServiceMessage::CollectMemoryReport,
);
})
.expect("Thread spawning failed");
@ -172,6 +187,9 @@ impl SystemFontService {
self.fetch_new_keys();
let _ = result_sender.send(self.free_font_instance_keys.pop().unwrap());
},
SystemFontServiceMessage::CollectMemoryReport(report_sender) => {
self.collect_memory_report(report_sender);
},
SystemFontServiceMessage::Ping => (),
SystemFontServiceMessage::Exit(result) => {
let _ = result.send(());
@ -181,6 +199,17 @@ impl SystemFontService {
}
}
fn collect_memory_report(&self, report_sender: ReportsChan) {
perform_memory_report(|ops| {
let reports = vec![Report {
path: path!["system-fonts"],
kind: ReportKind::ExplicitSystemHeapSize,
size: self.size_of(ops),
}];
report_sender.send(ProcessReports::new(reports));
});
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
@ -528,11 +557,7 @@ impl SystemFontServiceProxy {
panic!("SystemFontService has already exited.");
};
let templates: Vec<_> = templates
.into_iter()
.map(AtomicRefCell::new)
.map(Arc::new)
.collect();
let templates: Vec<_> = templates.into_iter().map(FontTemplateRef::new).collect();
self.templates.write().insert(cache_key, templates.clone());
templates

View file

@ -5,14 +5,13 @@
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use app_units::Au;
use euclid::num::Zero;
use fonts::platform::font::PlatformFont;
use fonts::{
Font, FontData, FontDescriptor, FontIdentifier, FontTemplate, PlatformFontMethods,
ShapingFlags, ShapingOptions,
Font, FontData, FontDescriptor, FontIdentifier, FontTemplate, FontTemplateRef,
PlatformFontMethods, ShapingFlags, ShapingOptions,
};
use servo_url::ServoUrl;
use style::properties::longhands::font_variant_caps::computed_value::T as FontVariantCaps;
@ -42,13 +41,7 @@ fn make_font(path: PathBuf) -> Font {
variant: FontVariantCaps::Normal,
pt_size: Au::from_px(24),
};
Font::new(
Arc::new(atomic_refcell::AtomicRefCell::new(template)),
descriptor,
Some(data),
None,
)
.unwrap()
Font::new(FontTemplateRef::new(template), descriptor, Some(data), None).unwrap()
}
#[test]

View file

@ -14,6 +14,7 @@ mod font_context {
use std::thread;
use app_units::Au;
use compositing_traits::CrossProcessCompositorApi;
use fonts::platform::font::PlatformFont;
use fonts::{
FallbackFontSelectionOptions, FontContext, FontDescriptor, FontFamilyDescriptor,
@ -34,7 +35,6 @@ mod font_context {
};
use stylo_atoms::Atom;
use webrender_api::{FontInstanceKey, FontKey, IdNamespace};
use webrender_traits::CrossProcessCompositorApi;
struct TestContext {
context: FontContext,
@ -137,6 +137,7 @@ mod font_context {
break;
},
SystemFontServiceMessage::Ping => {},
SystemFontServiceMessage::CollectMemoryReport(..) => {},
}
}
}

View file

@ -0,0 +1,68 @@
[package]
name = "layout"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[lib]
name = "layout"
path = "lib.rs"
test = true
doctest = false
[features]
tracing = ["dep:tracing"]
[dependencies]
app_units = { workspace = true }
atomic_refcell = { workspace = true }
base = { workspace = true }
bitflags = { workspace = true }
compositing_traits = { workspace = true }
constellation_traits = { workspace = true }
data-url = { workspace = true }
embedder_traits = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
fonts = { path = "../fonts" }
fonts_traits = { workspace = true }
fxhash = { workspace = true }
html5ever = { workspace = true }
icu_locid = { workspace = true }
icu_segmenter = { workspace = true }
ipc-channel = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true }
net_traits = { workspace = true }
parking_lot = { workspace = true }
pixels = { path = "../pixels" }
profile_traits = { workspace = true }
range = { path = "../range" }
rayon = { workspace = true }
script = { path = "../script" }
script_layout_interface = { workspace = true }
script_traits = { workspace = true }
selectors = { workspace = true }
servo_arc = { workspace = true }
servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" }
servo_url = { path = "../url" }
stylo = { workspace = true }
stylo_atoms = { workspace = true }
stylo_traits = { workspace = true }
taffy = { workspace = true }
tracing = { workspace = true, optional = true }
unicode-bidi = { workspace = true }
unicode-script = { workspace = true }
url = { workspace = true }
webrender_api = { workspace = true }
xi-unicode = { workspace = true }
[dev-dependencies]
num-traits = { workspace = true }
quickcheck = "1"

View file

@ -6,9 +6,12 @@ use std::fmt;
use std::ops::Deref;
use atomic_refcell::AtomicRefCell;
use malloc_size_of_derive::MallocSizeOf;
use servo_arc::Arc;
pub(crate) struct ArcRefCell<T> {
#[derive(MallocSizeOf)]
pub struct ArcRefCell<T> {
#[conditional_malloc_size_of]
value: Arc<AtomicRefCell<T>>,
}

View file

@ -12,7 +12,7 @@ use style::selector_parser::PseudoElement;
use crate::PropagatedBoxTreeData;
use crate::context::LayoutContext;
use crate::dom::{BoxSlot, NodeExt};
use crate::dom::BoxSlot;
use crate::dom_traversal::{Contents, NodeAndStyleInfo, TraversalHandler};
use crate::flow::inline::construct::InlineFormattingContextBuilder;
use crate::flow::{BlockContainer, BlockFormattingContext};
@ -24,32 +24,32 @@ use crate::layout_box_base::LayoutBoxBase;
use crate::style_ext::DisplayGeneratingBox;
/// A builder used for both flex and grid containers.
pub(crate) struct ModernContainerBuilder<'a, 'dom, Node> {
pub(crate) struct ModernContainerBuilder<'a, 'dom> {
context: &'a LayoutContext<'a>,
info: &'a NodeAndStyleInfo<Node>,
info: &'a NodeAndStyleInfo<'dom>,
propagated_data: PropagatedBoxTreeData,
contiguous_text_runs: Vec<ModernContainerTextRun<'dom, Node>>,
contiguous_text_runs: Vec<ModernContainerTextRun<'dom>>,
/// To be run in parallel with rayon in `finish`
jobs: Vec<ModernContainerJob<'dom, Node>>,
jobs: Vec<ModernContainerJob<'dom>>,
has_text_runs: bool,
}
enum ModernContainerJob<'dom, Node> {
enum ModernContainerJob<'dom> {
ElementOrPseudoElement {
info: NodeAndStyleInfo<Node>,
info: NodeAndStyleInfo<'dom>,
display: DisplayGeneratingBox,
contents: Contents,
box_slot: BoxSlot<'dom>,
},
TextRuns(Vec<ModernContainerTextRun<'dom, Node>>),
TextRuns(Vec<ModernContainerTextRun<'dom>>),
}
struct ModernContainerTextRun<'dom, Node> {
info: NodeAndStyleInfo<Node>,
struct ModernContainerTextRun<'dom> {
info: NodeAndStyleInfo<'dom>,
text: Cow<'dom, str>,
}
impl<Node> ModernContainerTextRun<'_, Node> {
impl ModernContainerTextRun<'_> {
/// <https://drafts.csswg.org/css-text/#white-space>
fn is_only_document_white_space(&self) -> bool {
// FIXME: is this the right definition? See
@ -73,11 +73,8 @@ pub(crate) struct ModernItem<'dom> {
pub formatting_context: IndependentFormattingContext,
}
impl<'dom, Node: 'dom> TraversalHandler<'dom, Node> for ModernContainerBuilder<'_, 'dom, Node>
where
Node: NodeExt<'dom>,
{
fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>) {
impl<'dom> TraversalHandler<'dom> for ModernContainerBuilder<'_, 'dom> {
fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) {
self.contiguous_text_runs.push(ModernContainerTextRun {
info: info.clone(),
text,
@ -87,7 +84,7 @@ where
/// Or pseudo-element
fn handle_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
info: &NodeAndStyleInfo<'dom>,
display: DisplayGeneratingBox,
contents: Contents,
box_slot: BoxSlot<'dom>,
@ -103,13 +100,10 @@ where
}
}
impl<'a, 'dom, Node: 'dom> ModernContainerBuilder<'a, 'dom, Node>
where
Node: NodeExt<'dom>,
{
impl<'a, 'dom> ModernContainerBuilder<'a, 'dom> {
pub fn new(
context: &'a LayoutContext<'a>,
info: &'a NodeAndStyleInfo<Node>,
info: &'a NodeAndStyleInfo<'dom>,
propagated_data: PropagatedBoxTreeData,
) -> Self {
ModernContainerBuilder {
@ -148,7 +142,7 @@ where
.filter_map(|job| match job {
ModernContainerJob::TextRuns(runs) => {
let mut inline_formatting_context_builder =
InlineFormattingContextBuilder::new();
InlineFormattingContextBuilder::new(self.info);
for flex_text_run in runs.into_iter() {
inline_formatting_context_builder
.push_text(flex_text_run.text, &flex_text_run.info);
@ -156,7 +150,6 @@ where
let inline_formatting_context = inline_formatting_context_builder.finish(
self.context,
self.propagated_data,
true, /* has_first_formatted_line */
false, /* is_single_line_text_box */
self.info.style.writing_mode.to_bidi_level(),
@ -165,7 +158,7 @@ where
let block_formatting_context = BlockFormattingContext::from_block_container(
BlockContainer::InlineFormattingContext(inline_formatting_context),
);
let info: &NodeAndStyleInfo<_> = &*anonymous_info;
let info: &NodeAndStyleInfo = &anonymous_info;
let formatting_context = IndependentFormattingContext {
base: LayoutBoxBase::new(info.into(), info.style.clone()),
contents: IndependentFormattingContextContents::NonReplaced(

View file

@ -0,0 +1,316 @@
/* 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::Arc;
use base::id::PipelineId;
use euclid::Size2D;
use fnv::FnvHashMap;
use fonts::FontContext;
use fxhash::FxHashMap;
use net_traits::image_cache::{
Image as CachedImage, ImageCache, ImageCacheResult, ImageOrMetadataAvailable, PendingImageId,
UsePlaceholder,
};
use parking_lot::{Mutex, RwLock};
use pixels::RasterImage;
use script_layout_interface::{
IFrameSizes, ImageAnimationState, PendingImage, PendingImageState, PendingRasterizationImage,
};
use servo_url::{ImmutableOrigin, ServoUrl};
use style::context::SharedStyleContext;
use style::dom::OpaqueNode;
use style::values::computed::image::{Gradient, Image};
use webrender_api::units::{DeviceIntSize, DeviceSize};
pub(crate) type CachedImageOrError = Result<CachedImage, ResolveImageError>;
pub struct LayoutContext<'a> {
pub id: PipelineId,
pub use_rayon: bool,
pub origin: ImmutableOrigin,
/// Bits shared by the layout and style system.
pub style_context: SharedStyleContext<'a>,
/// A FontContext to be used during layout.
pub font_context: Arc<FontContext>,
/// Reference to the script thread image cache.
pub image_cache: Arc<dyn ImageCache>,
/// A list of in-progress image loads to be shared with the script thread.
pub pending_images: Mutex<Vec<PendingImage>>,
/// A list of fully loaded vector images that need to be rasterized to a specific
/// size determined by layout. This will be shared with the script thread.
pub pending_rasterization_images: Mutex<Vec<PendingRasterizationImage>>,
/// A collection of `<iframe>` sizes to send back to script.
pub iframe_sizes: Mutex<IFrameSizes>,
// A cache that maps image resources used in CSS (e.g as the `url()` value
// for `background-image` or `content` property) to the final resolved image data.
pub resolved_images_cache:
Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), CachedImageOrError>>>,
pub node_image_animation_map: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>,
/// The DOM node that is highlighted by the devtools inspector, if any
pub highlighted_dom_node: Option<OpaqueNode>,
}
pub enum ResolvedImage<'a> {
Gradient(&'a Gradient),
// The size is tracked explicitly as image-set images can specify their
// natural resolution which affects the final size for raster images.
Image {
image: CachedImage,
size: DeviceSize,
},
}
impl Drop for LayoutContext<'_> {
fn drop(&mut self) {
if !std::thread::panicking() {
assert!(self.pending_images.lock().is_empty());
assert!(self.pending_rasterization_images.lock().is_empty());
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ResolveImageError {
LoadError,
ImagePending,
ImageRequested,
OnlyMetadata,
InvalidUrl,
MissingNode,
ImageMissingFromImageSet,
FailedToResolveImageFromImageSet,
NotImplementedYet(&'static str),
None,
}
pub(crate) enum LayoutImageCacheResult {
Pending,
DataAvailable(ImageOrMetadataAvailable),
LoadError,
}
impl LayoutContext<'_> {
#[inline(always)]
pub fn shared_context(&self) -> &SharedStyleContext {
&self.style_context
}
pub(crate) fn get_or_request_image_or_meta(
&self,
node: OpaqueNode,
url: ServoUrl,
use_placeholder: UsePlaceholder,
) -> LayoutImageCacheResult {
// Check for available image or start tracking.
let cache_result = self.image_cache.get_cached_image_status(
url.clone(),
self.origin.clone(),
None,
use_placeholder,
);
match cache_result {
ImageCacheResult::Available(img_or_meta) => {
LayoutImageCacheResult::DataAvailable(img_or_meta)
},
// Image has been requested, is still pending. Return no image for this paint loop.
// When the image loads it will trigger a reflow and/or repaint.
ImageCacheResult::Pending(id) => {
let image = PendingImage {
state: PendingImageState::PendingResponse,
node: node.into(),
id,
origin: self.origin.clone(),
};
self.pending_images.lock().push(image);
LayoutImageCacheResult::Pending
},
// Not yet requested - request image or metadata from the cache
ImageCacheResult::ReadyForRequest(id) => {
let image = PendingImage {
state: PendingImageState::Unrequested(url),
node: node.into(),
id,
origin: self.origin.clone(),
};
self.pending_images.lock().push(image);
LayoutImageCacheResult::Pending
},
// Image failed to load, so just return the same error.
ImageCacheResult::LoadError => LayoutImageCacheResult::LoadError,
}
}
pub fn handle_animated_image(&self, node: OpaqueNode, image: Arc<RasterImage>) {
let mut store = self.node_image_animation_map.write();
// 1. first check whether node previously being track for animated image.
if let Some(image_state) = store.get(&node) {
// a. if the node is not containing the same image as before.
if image_state.image_key() != image.id {
if image.should_animate() {
// i. Register/Replace tracking item in image_animation_manager.
store.insert(
node,
ImageAnimationState::new(
image,
self.shared_context().current_time_for_animations,
),
);
} else {
// ii. Cancel Action if the node's image is no longer animated.
store.remove(&node);
}
}
} else if image.should_animate() {
store.insert(
node,
ImageAnimationState::new(image, self.shared_context().current_time_for_animations),
);
}
}
fn get_cached_image_for_url(
&self,
node: OpaqueNode,
url: ServoUrl,
use_placeholder: UsePlaceholder,
) -> Result<CachedImage, ResolveImageError> {
if let Some(cached_image) = self
.resolved_images_cache
.read()
.get(&(url.clone(), use_placeholder))
{
return cached_image.clone();
}
let result = self.get_or_request_image_or_meta(node, url.clone(), use_placeholder);
match result {
LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
if let Some(image) = image.as_raster_image() {
self.handle_animated_image(node, image.clone());
}
let mut resolved_images_cache = self.resolved_images_cache.write();
resolved_images_cache.insert((url, use_placeholder), Ok(image.clone()));
Ok(image)
},
ImageOrMetadataAvailable::MetadataAvailable(..) => {
Result::Err(ResolveImageError::OnlyMetadata)
},
},
LayoutImageCacheResult::Pending => Result::Err(ResolveImageError::ImagePending),
LayoutImageCacheResult::LoadError => {
let error = Err(ResolveImageError::LoadError);
self.resolved_images_cache
.write()
.insert((url, use_placeholder), error.clone());
error
},
}
}
pub fn rasterize_vector_image(
&self,
image_id: PendingImageId,
size: DeviceIntSize,
node: OpaqueNode,
) -> Option<RasterImage> {
let result = self.image_cache.rasterize_vector_image(image_id, size);
if result.is_none() {
self.pending_rasterization_images
.lock()
.push(PendingRasterizationImage {
id: image_id,
node: node.into(),
size,
});
}
result
}
pub fn resolve_image<'a>(
&self,
node: Option<OpaqueNode>,
image: &'a Image,
) -> Result<ResolvedImage<'a>, ResolveImageError> {
match image {
// TODO: Add support for PaintWorklet and CrossFade rendering.
Image::None => Result::Err(ResolveImageError::None),
Image::CrossFade(_) => Result::Err(ResolveImageError::NotImplementedYet("CrossFade")),
Image::PaintWorklet(_) => {
Result::Err(ResolveImageError::NotImplementedYet("PaintWorklet"))
},
Image::Gradient(gradient) => Ok(ResolvedImage::Gradient(gradient)),
Image::Url(image_url) => {
// FIXME: images wont always have in intrinsic width or
// height when support for SVG is added, or a WebRender
// `ImageKey`, for that matter.
//
// FIXME: It feels like this should take into account the pseudo
// element and not just the node.
let image_url = image_url.url().ok_or(ResolveImageError::InvalidUrl)?;
let node = node.ok_or(ResolveImageError::MissingNode)?;
let image = self.get_cached_image_for_url(
node,
image_url.clone().into(),
UsePlaceholder::No,
)?;
let metadata = image.metadata();
let size = Size2D::new(metadata.width, metadata.height).to_f32();
Ok(ResolvedImage::Image { image, size })
},
Image::ImageSet(image_set) => {
image_set
.items
.get(image_set.selected_index)
.ok_or(ResolveImageError::ImageMissingFromImageSet)
.and_then(|image| {
self.resolve_image(node, &image.image)
.map(|info| match info {
ResolvedImage::Image {
image: cached_image,
..
} => {
// From <https://drafts.csswg.org/css-images-4/#image-set-notation>:
// > A <resolution> (optional). This is used to help the UA decide
// > which <image-set-option> to choose. If the image reference is
// > for a raster image, it also specifies the images natural
// > resolution, overriding any other source of data that might
// > supply a natural resolution.
let image_metadata = cached_image.metadata();
let size = if cached_image.as_raster_image().is_some() {
let scale_factor = image.resolution.dppx();
Size2D::new(
image_metadata.width as f32 / scale_factor,
image_metadata.height as f32 / scale_factor,
)
} else {
Size2D::new(image_metadata.width, image_metadata.height)
.to_f32()
};
ResolvedImage::Image {
image: cached_image,
size,
}
},
_ => info,
})
})
},
}
}
}

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use app_units::Au;
use euclid::{Point2D, Size2D, Vector2D};
use euclid::{Size2D, Vector2D};
use style::computed_values::background_attachment::SingleComputedValue as BackgroundAttachment;
use style::computed_values::background_clip::single_value::T as Clip;
use style::computed_values::background_origin::single_value::T as Origin;
@ -15,7 +15,6 @@ use style::values::specified::background::{
};
use webrender_api::{self as wr, units};
use wr::ClipChainId;
use wr::units::LayoutSize;
use crate::replaced::NaturalSizes;
@ -66,8 +65,7 @@ impl<'a> BackgroundPainter<'a> {
if &BackgroundAttachment::Fixed ==
get_cyclic(&background.background_attachment.0, layer_index)
{
let viewport_size = builder.display_list.compositor_info.viewport_size;
return units::LayoutRect::from_origin_and_size(Point2D::origin(), viewport_size);
return builder.compositor_info.viewport_size.into();
}
match get_cyclic(&background.background_clip.0, layer_index) {
@ -121,7 +119,7 @@ impl<'a> BackgroundPainter<'a> {
if &BackgroundAttachment::Fixed ==
get_cyclic(&style.get_background().background_attachment.0, layer_index)
{
common.spatial_id = builder.current_reference_frame_scroll_node_id.spatial_id;
common.spatial_id = builder.spatial_id(builder.current_reference_frame_scroll_node_id);
}
common
}
@ -132,6 +130,7 @@ impl<'a> BackgroundPainter<'a> {
pub(super) fn positioning_area(
&self,
fragment_builder: &'a super::BuilderForBoxFragment,
builder: &mut super::DisplayListBuilder,
layer_index: usize,
) -> units::LayoutRect {
if let Some(positioning_area_override) = self.positioning_area_override {
@ -150,14 +149,7 @@ impl<'a> BackgroundPainter<'a> {
Origin::PaddingBox => *fragment_builder.padding_rect(),
Origin::BorderBox => fragment_builder.border_rect,
},
BackgroundAttachment::Fixed => {
// This isn't the viewport size because that rects larger than the viewport might be
// transformed down into areas smaller than the viewport.
units::LayoutRect::from_origin_and_size(
Point2D::origin(),
LayoutSize::new(f32::MAX, f32::MAX),
)
},
BackgroundAttachment::Fixed => builder.compositor_info.viewport_size.into(),
}
}
}
@ -170,7 +162,7 @@ pub(super) fn layout_layer(
natural_sizes: NaturalSizes,
) -> Option<BackgroundLayer> {
let painting_area = painter.painting_area(fragment_builder, builder, layer_index);
let positioning_area = painter.positioning_area(fragment_builder, layer_index);
let positioning_area = painter.positioning_area(fragment_builder, builder, layer_index);
let common = painter.common_properties(fragment_builder, builder, layer_index, painting_area);
// https://drafts.csswg.org/css-backgrounds/#background-size

View file

@ -0,0 +1,276 @@
/* 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 app_units::Au;
use base::id::ScrollTreeNodeId;
use style::values::computed::basic_shape::{BasicShape, ClipPath};
use style::values::computed::length_percentage::NonNegativeLengthPercentage;
use style::values::computed::position::Position;
use style::values::generics::basic_shape::{GenericShapeRadius, ShapeBox, ShapeGeometryBox};
use style::values::generics::position::GenericPositionOrAuto;
use webrender_api::BorderRadius;
use webrender_api::units::{LayoutRect, LayoutSideOffsets, LayoutSize};
use super::{BuilderForBoxFragment, compute_margin_box_radius, normalize_radii};
/// An identifier for a clip used during StackingContextTree construction. This is a simple index in
/// a [`ClipStore`]s vector of clips.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ClipId(pub usize);
impl ClipId {
/// Equivalent to [`ClipChainId::INVALID`]. This means "no clip."
pub(crate) const INVALID: ClipId = ClipId(usize::MAX);
}
/// All the information needed to create a clip on a WebRender display list. These are created at
/// two times: during `StackingContextTree` creation and during WebRender display list construction.
/// Only the former are stored in a [`ClipStore`].
#[derive(Clone)]
pub(crate) struct Clip {
pub id: ClipId,
pub radii: BorderRadius,
pub rect: LayoutRect,
pub parent_scroll_node_id: ScrollTreeNodeId,
pub parent_clip_id: ClipId,
}
/// A simple vector of [`Clip`] that is built during `StackingContextTree` construction.
/// These are later turned into WebRender clips and clip chains during WebRender display
/// list construction.
#[derive(Clone, Default)]
pub(crate) struct StackingContextTreeClipStore(pub Vec<Clip>);
impl StackingContextTreeClipStore {
pub(crate) fn add(
&mut self,
radii: webrender_api::BorderRadius,
rect: LayoutRect,
parent_scroll_node_id: ScrollTreeNodeId,
parent_clip_id: ClipId,
) -> ClipId {
let id = ClipId(self.0.len());
self.0.push(Clip {
id,
radii,
rect,
parent_scroll_node_id,
parent_clip_id,
});
id
}
pub(super) fn add_for_clip_path(
&mut self,
clip_path: ClipPath,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipId,
fragment_builder: BuilderForBoxFragment,
) -> Option<ClipId> {
let geometry_box = match clip_path {
ClipPath::Shape(_, ShapeGeometryBox::ShapeBox(shape_box)) => shape_box,
ClipPath::Shape(_, ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
ClipPath::Box(ShapeGeometryBox::ShapeBox(shape_box)) => shape_box,
ClipPath::Box(ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
_ => return None,
};
let layout_rect = match geometry_box {
ShapeBox::BorderBox => fragment_builder.border_rect,
ShapeBox::ContentBox => *fragment_builder.content_rect(),
ShapeBox::PaddingBox => *fragment_builder.padding_rect(),
ShapeBox::MarginBox => *fragment_builder.margin_rect(),
};
if let ClipPath::Shape(shape, _) = clip_path {
match *shape {
BasicShape::Circle(_) | BasicShape::Ellipse(_) | BasicShape::Rect(_) => self
.add_for_basic_shape(
*shape,
layout_rect,
parent_scroll_node_id,
parent_clip_chain_id,
),
BasicShape::Polygon(_) | BasicShape::PathOrShape(_) => None,
}
} else {
Some(self.add(
match geometry_box {
ShapeBox::MarginBox => compute_margin_box_radius(
fragment_builder.border_radius,
layout_rect.size(),
fragment_builder.fragment,
),
_ => fragment_builder.border_radius,
},
layout_rect,
*parent_scroll_node_id,
*parent_clip_chain_id,
))
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "StackingContextClipStore::add_for_basic_shape",
skip_all,
fields(servo_profiling = true),
level = "trace",
)
)]
fn add_for_basic_shape(
&mut self,
shape: BasicShape,
layout_box: LayoutRect,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipId,
) -> Option<ClipId> {
match shape {
BasicShape::Rect(rect) => {
let box_height = Au::from_f32_px(layout_box.height());
let box_width = Au::from_f32_px(layout_box.width());
let insets = LayoutSideOffsets::new(
rect.rect.0.to_used_value(box_height).to_f32_px(),
rect.rect.1.to_used_value(box_width).to_f32_px(),
rect.rect.2.to_used_value(box_height).to_f32_px(),
rect.rect.3.to_used_value(box_width).to_f32_px(),
);
// `inner_rect()` will cause an assertion failure if the insets are larger than the
// rectangle dimension.
let shape_rect = if insets.left + insets.right >= layout_box.width() ||
insets.top + insets.bottom > layout_box.height()
{
LayoutRect::from_origin_and_size(layout_box.min, LayoutSize::zero())
} else {
layout_box.to_rect().inner_rect(insets).to_box2d()
};
let corner = |corner: &style::values::computed::BorderCornerRadius| {
LayoutSize::new(
corner.0.width.0.to_used_value(box_width).to_f32_px(),
corner.0.height.0.to_used_value(box_height).to_f32_px(),
)
};
let mut radii = webrender_api::BorderRadius {
top_left: corner(&rect.round.top_left),
top_right: corner(&rect.round.top_right),
bottom_left: corner(&rect.round.bottom_left),
bottom_right: corner(&rect.round.bottom_right),
};
normalize_radii(&layout_box, &mut radii);
Some(self.add(
radii,
shape_rect,
*parent_scroll_node_id,
*parent_clip_chain_id,
))
},
BasicShape::Circle(circle) => {
let center = match circle.position {
GenericPositionOrAuto::Position(position) => position,
GenericPositionOrAuto::Auto => Position::center(),
};
let anchor_x = center
.horizontal
.to_used_value(Au::from_f32_px(layout_box.width()));
let anchor_y = center
.vertical
.to_used_value(Au::from_f32_px(layout_box.height()));
let center = layout_box
.min
.add_size(&LayoutSize::new(anchor_x.to_f32_px(), anchor_y.to_f32_px()));
let horizontal = compute_shape_radius(
center.x,
&circle.radius,
layout_box.min.x,
layout_box.max.x,
);
let vertical = compute_shape_radius(
center.y,
&circle.radius,
layout_box.min.y,
layout_box.max.y,
);
// If the value is `Length` then both values should be the same at this point.
let radius = match circle.radius {
GenericShapeRadius::FarthestSide => horizontal.max(vertical),
GenericShapeRadius::ClosestSide => horizontal.min(vertical),
GenericShapeRadius::Length(_) => horizontal,
};
let radius = LayoutSize::new(radius, radius);
let mut radii = webrender_api::BorderRadius {
top_left: radius,
top_right: radius,
bottom_left: radius,
bottom_right: radius,
};
let start = center.add_size(&-radius);
let rect = LayoutRect::from_origin_and_size(start, radius * 2.);
normalize_radii(&layout_box, &mut radii);
Some(self.add(radii, rect, *parent_scroll_node_id, *parent_clip_chain_id))
},
BasicShape::Ellipse(ellipse) => {
let center = match ellipse.position {
GenericPositionOrAuto::Position(position) => position,
GenericPositionOrAuto::Auto => Position::center(),
};
let anchor_x = center
.horizontal
.to_used_value(Au::from_f32_px(layout_box.width()));
let anchor_y = center
.vertical
.to_used_value(Au::from_f32_px(layout_box.height()));
let center = layout_box
.min
.add_size(&LayoutSize::new(anchor_x.to_f32_px(), anchor_y.to_f32_px()));
let width = compute_shape_radius(
center.x,
&ellipse.semiaxis_x,
layout_box.min.x,
layout_box.max.x,
);
let height = compute_shape_radius(
center.y,
&ellipse.semiaxis_y,
layout_box.min.y,
layout_box.max.y,
);
let mut radii = webrender_api::BorderRadius {
top_left: LayoutSize::new(width, height),
top_right: LayoutSize::new(width, height),
bottom_left: LayoutSize::new(width, height),
bottom_right: LayoutSize::new(width, height),
};
let size = LayoutSize::new(width, height);
let start = center.add_size(&-size);
let rect = LayoutRect::from_origin_and_size(start, size * 2.);
normalize_radii(&rect, &mut radii);
Some(self.add(radii, rect, *parent_scroll_node_id, *parent_clip_chain_id))
},
_ => None,
}
}
}
fn compute_shape_radius(
center: f32,
radius: &GenericShapeRadius<NonNegativeLengthPercentage>,
min_edge: f32,
max_edge: f32,
) -> f32 {
let distance_from_min_edge = (min_edge - center).abs();
let distance_from_max_edge = (max_edge - center).abs();
match radius {
GenericShapeRadius::FarthestSide => distance_from_min_edge.max(distance_from_max_edge),
GenericShapeRadius::ClosestSide => distance_from_min_edge.min(distance_from_max_edge),
GenericShapeRadius::Length(length) => length
.0
.to_used_value(Au::from_f32_px(max_edge - min_edge))
.to_f32_px(),
}
}

View file

@ -125,11 +125,15 @@ impl ToWebRender for ComputedTextDecorationStyle {
type Type = LineStyle;
fn to_webrender(&self) -> Self::Type {
match *self {
ComputedTextDecorationStyle::Solid => LineStyle::Solid,
ComputedTextDecorationStyle::Solid | ComputedTextDecorationStyle::Double => {
LineStyle::Solid
},
ComputedTextDecorationStyle::Dotted => LineStyle::Dotted,
ComputedTextDecorationStyle::Dashed => LineStyle::Dashed,
ComputedTextDecorationStyle::Wavy => LineStyle::Wavy,
_ => LineStyle::Solid,
ComputedTextDecorationStyle::MozNone => {
unreachable!("Should never try to draw a moz-none text decoration")
},
}
}
}

File diff suppressed because it is too large Load diff

407
components/layout/dom.rs Normal file
View file

@ -0,0 +1,407 @@
/* 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::any::Any;
use std::marker::PhantomData;
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
use base::id::{BrowsingContextId, PipelineId};
use html5ever::{local_name, ns};
use malloc_size_of_derive::MallocSizeOf;
use net_traits::image_cache::Image;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{
LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use script_layout_interface::{
GenericLayoutDataTrait, LayoutElementType, LayoutNodeType as ScriptLayoutNodeType,
};
use servo_arc::Arc as ServoArc;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
use style::selector_parser::PseudoElement;
use crate::cell::ArcRefCell;
use crate::flexbox::FlexLevelBox;
use crate::flow::BlockLevelBox;
use crate::flow::inline::{InlineItem, SharedInlineStyles};
use crate::fragment_tree::Fragment;
use crate::geom::PhysicalSize;
use crate::replaced::CanvasInfo;
use crate::table::TableLevelBox;
use crate::taffy::TaffyItemBox;
/// The data that is stored in each DOM node that is used by layout.
#[derive(Default, MallocSizeOf)]
pub struct InnerDOMLayoutData {
pub(super) self_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_before_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_after_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_marker_box: ArcRefCell<Option<LayoutBox>>,
}
impl InnerDOMLayoutData {
pub(crate) fn for_pseudo(
&self,
pseudo_element: Option<PseudoElement>,
) -> AtomicRef<Option<LayoutBox>> {
match pseudo_element {
Some(PseudoElement::Before) => self.pseudo_before_box.borrow(),
Some(PseudoElement::After) => self.pseudo_after_box.borrow(),
Some(PseudoElement::Marker) => self.pseudo_marker_box.borrow(),
_ => self.self_box.borrow(),
}
}
}
/// A box that is stored in one of the `DOMLayoutData` slots.
#[derive(MallocSizeOf)]
pub(super) enum LayoutBox {
DisplayContents(SharedInlineStyles),
BlockLevel(ArcRefCell<BlockLevelBox>),
InlineLevel(Vec<ArcRefCell<InlineItem>>),
FlexLevel(ArcRefCell<FlexLevelBox>),
TableLevelBox(TableLevelBox),
TaffyItemBox(ArcRefCell<TaffyItemBox>),
}
impl LayoutBox {
fn invalidate_cached_fragment(&self) {
match self {
LayoutBox::DisplayContents(..) => {},
LayoutBox::BlockLevel(block_level_box) => {
block_level_box.borrow().invalidate_cached_fragment()
},
LayoutBox::InlineLevel(inline_items) => {
for inline_item in inline_items.iter() {
inline_item.borrow().invalidate_cached_fragment()
}
},
LayoutBox::FlexLevel(flex_level_box) => {
flex_level_box.borrow().invalidate_cached_fragment()
},
LayoutBox::TaffyItemBox(taffy_item_box) => {
taffy_item_box.borrow_mut().invalidate_cached_fragment()
},
LayoutBox::TableLevelBox(table_box) => table_box.invalidate_cached_fragment(),
}
}
pub(crate) fn fragments(&self) -> Vec<Fragment> {
match self {
LayoutBox::DisplayContents(..) => vec![],
LayoutBox::BlockLevel(block_level_box) => block_level_box.borrow().fragments(),
LayoutBox::InlineLevel(inline_items) => inline_items
.iter()
.flat_map(|inline_item| inline_item.borrow().fragments())
.collect(),
LayoutBox::FlexLevel(flex_level_box) => flex_level_box.borrow().fragments(),
LayoutBox::TaffyItemBox(taffy_item_box) => taffy_item_box.borrow().fragments(),
LayoutBox::TableLevelBox(table_box) => table_box.fragments(),
}
}
fn repair_style(
&self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &ServoArc<ComputedValues>,
) {
match self {
LayoutBox::DisplayContents(inline_shared_styles) => {
*inline_shared_styles.style.borrow_mut() = new_style.clone();
*inline_shared_styles.selected.borrow_mut() = node.to_threadsafe().selected_style();
},
LayoutBox::BlockLevel(block_level_box) => {
block_level_box
.borrow_mut()
.repair_style(context, node, new_style);
},
LayoutBox::InlineLevel(inline_items) => {
for inline_item in inline_items {
inline_item
.borrow_mut()
.repair_style(context, node, new_style);
}
},
LayoutBox::FlexLevel(flex_level_box) => flex_level_box
.borrow_mut()
.repair_style(context, node, new_style),
LayoutBox::TableLevelBox(table_level_box) => {
table_level_box.repair_style(context, node, new_style)
},
LayoutBox::TaffyItemBox(taffy_item_box) => taffy_item_box
.borrow_mut()
.repair_style(context, node, new_style),
}
}
}
/// A wrapper for [`InnerDOMLayoutData`]. This is necessary to give the entire data
/// structure interior mutability, as we will need to mutate the layout data of
/// non-mutable DOM nodes.
#[derive(Default, MallocSizeOf)]
pub struct DOMLayoutData(AtomicRefCell<InnerDOMLayoutData>);
// The implementation of this trait allows the data to be stored in the DOM.
impl LayoutDataTrait for DOMLayoutData {}
impl GenericLayoutDataTrait for DOMLayoutData {
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct BoxSlot<'dom> {
pub(crate) slot: Option<ArcRefCell<Option<LayoutBox>>>,
pub(crate) marker: PhantomData<&'dom ()>,
}
/// A mutable reference to a `LayoutBox` stored in a DOM element.
impl BoxSlot<'_> {
pub(crate) fn new(slot: ArcRefCell<Option<LayoutBox>>) -> Self {
*slot.borrow_mut() = None;
let slot = Some(slot);
Self {
slot,
marker: PhantomData,
}
}
pub(crate) fn dummy() -> Self {
let slot = None;
Self {
slot,
marker: PhantomData,
}
}
pub(crate) fn set(mut self, box_: LayoutBox) {
if let Some(slot) = &mut self.slot {
*slot.borrow_mut() = Some(box_);
}
}
}
impl Drop for BoxSlot<'_> {
fn drop(&mut self) {
if !std::thread::panicking() {
if let Some(slot) = &mut self.slot {
assert!(slot.borrow().is_some(), "failed to set a layout box");
}
}
}
}
pub(crate) trait NodeExt<'dom> {
/// Returns the image if its loaded, and its size in image pixels
/// adjusted for `image_density`.
fn as_image(&self) -> Option<(Option<Image>, PhysicalSize<f64>)>;
fn as_canvas(&self) -> Option<(CanvasInfo, PhysicalSize<f64>)>;
fn as_iframe(&self) -> Option<(PipelineId, BrowsingContextId)>;
fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)>;
fn as_typeless_object_with_data_attribute(&self) -> Option<String>;
fn style(&self, context: &SharedStyleContext) -> ServoArc<ComputedValues>;
fn layout_data_mut(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData>;
fn layout_data(&self) -> Option<AtomicRef<'dom, InnerDOMLayoutData>>;
fn element_box_slot(&self) -> BoxSlot<'dom>;
fn pseudo_element_box_slot(&self, which: PseudoElement) -> BoxSlot<'dom>;
fn unset_pseudo_element_box(&self, which: PseudoElement);
/// Remove boxes for the element itself, and its `:before` and `:after` if any.
fn unset_all_boxes(&self);
fn fragments_for_pseudo(&self, pseudo_element: Option<PseudoElement>) -> Vec<Fragment>;
fn invalidate_cached_fragment(&self);
fn repair_style(&self, context: &SharedStyleContext);
}
impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> {
fn as_image(&self) -> Option<(Option<Image>, PhysicalSize<f64>)> {
let node = self.to_threadsafe();
let (resource, metadata) = node.image_data()?;
let (width, height) = resource
.as_ref()
.map(|image| {
let image_metadata = image.metadata();
(image_metadata.width, image_metadata.height)
})
.or_else(|| metadata.map(|metadata| (metadata.width, metadata.height)))
.unwrap_or((0, 0));
let (mut width, mut height) = (width as f64, height as f64);
if let Some(density) = node.image_density().filter(|density| *density != 1.) {
width /= density;
height /= density;
}
Some((resource, PhysicalSize::new(width, height)))
}
fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)> {
let node = self.to_threadsafe();
let data = node.media_data()?;
let natural_size = if let Some(frame) = data.current_frame {
Some(PhysicalSize::new(frame.width.into(), frame.height.into()))
} else {
data.metadata
.map(|meta| PhysicalSize::new(meta.width.into(), meta.height.into()))
};
Some((
data.current_frame.map(|frame| frame.image_key),
natural_size,
))
}
fn as_canvas(&self) -> Option<(CanvasInfo, PhysicalSize<f64>)> {
let node = self.to_threadsafe();
let canvas_data = node.canvas_data()?;
let source = canvas_data.source;
Some((
CanvasInfo { source },
PhysicalSize::new(canvas_data.width.into(), canvas_data.height.into()),
))
}
fn as_iframe(&self) -> Option<(PipelineId, BrowsingContextId)> {
let node = self.to_threadsafe();
match (node.iframe_pipeline_id(), node.iframe_browsing_context_id()) {
(Some(pipeline_id), Some(browsing_context_id)) => {
Some((pipeline_id, browsing_context_id))
},
_ => None,
}
}
fn as_typeless_object_with_data_attribute(&self) -> Option<String> {
if LayoutNode::type_id(self) !=
ScriptLayoutNodeType::Element(LayoutElementType::HTMLObjectElement)
{
return None;
}
// TODO: This is the what the legacy layout system did, but really if Servo
// supports any `<object>` that's an image, it should support those with URLs
// and `type` attributes with image mime types.
let element = self.to_threadsafe().as_element()?;
if element.get_attr(&ns!(), &local_name!("type")).is_some() {
return None;
}
element
.get_attr(&ns!(), &local_name!("data"))
.map(|string| string.to_owned())
}
fn style(&self, context: &SharedStyleContext) -> ServoArc<ComputedValues> {
self.to_threadsafe().style(context)
}
fn layout_data_mut(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData> {
if LayoutNode::layout_data(self).is_none() {
self.initialize_layout_data::<DOMLayoutData>();
}
LayoutNode::layout_data(self)
.unwrap()
.as_any()
.downcast_ref::<DOMLayoutData>()
.unwrap()
.0
.borrow_mut()
}
fn layout_data(&self) -> Option<AtomicRef<'dom, InnerDOMLayoutData>> {
LayoutNode::layout_data(self).map(|data| {
data.as_any()
.downcast_ref::<DOMLayoutData>()
.unwrap()
.0
.borrow()
})
}
fn element_box_slot(&self) -> BoxSlot<'dom> {
BoxSlot::new(self.layout_data_mut().self_box.clone())
}
fn pseudo_element_box_slot(&self, pseudo_element_type: PseudoElement) -> BoxSlot<'dom> {
let data = self.layout_data_mut();
let cell = match pseudo_element_type {
PseudoElement::Before => &data.pseudo_before_box,
PseudoElement::After => &data.pseudo_after_box,
PseudoElement::Marker => &data.pseudo_marker_box,
_ => unreachable!(
"Asked for box slot for unsupported pseudo-element: {:?}",
pseudo_element_type
),
};
BoxSlot::new(cell.clone())
}
fn unset_pseudo_element_box(&self, pseudo_element_type: PseudoElement) {
let data = self.layout_data_mut();
let cell = match pseudo_element_type {
PseudoElement::Before => &data.pseudo_before_box,
PseudoElement::After => &data.pseudo_after_box,
PseudoElement::Marker => &data.pseudo_marker_box,
_ => unreachable!(
"Asked for box slot for unsupported pseudo-element: {:?}",
pseudo_element_type
),
};
*cell.borrow_mut() = None;
}
fn unset_all_boxes(&self) {
let data = self.layout_data_mut();
*data.self_box.borrow_mut() = None;
*data.pseudo_before_box.borrow_mut() = None;
*data.pseudo_after_box.borrow_mut() = None;
*data.pseudo_marker_box.borrow_mut() = None;
// Stylo already takes care of removing all layout data
// for DOM descendants of elements with `display: none`.
}
fn invalidate_cached_fragment(&self) {
let data = self.layout_data_mut();
if let Some(data) = data.self_box.borrow_mut().as_mut() {
data.invalidate_cached_fragment();
}
}
fn fragments_for_pseudo(&self, pseudo_element: Option<PseudoElement>) -> Vec<Fragment> {
NodeExt::layout_data(self)
.and_then(|layout_data| {
layout_data
.for_pseudo(pseudo_element)
.as_ref()
.map(LayoutBox::fragments)
})
.unwrap_or_default()
}
fn repair_style(&self, context: &SharedStyleContext) {
let data = self.layout_data_mut();
if let Some(layout_object) = &*data.self_box.borrow() {
let style = self.to_threadsafe().style(context);
layout_object.repair_style(context, self, &style);
}
if let Some(layout_object) = &*data.pseudo_before_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::Before) {
layout_object.repair_style(context, self, &node.style(context));
}
}
if let Some(layout_object) = &*data.pseudo_after_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::After) {
layout_object.repair_style(context, self, &node.style(context));
}
}
if let Some(layout_object) = &*data.pseudo_marker_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::Marker) {
layout_object.repair_style(context, self, &node.style(context));
}
}
}
}

View file

@ -5,12 +5,17 @@
use std::borrow::Cow;
use std::iter::FusedIterator;
use fonts::ByteIndex;
use html5ever::{LocalName, local_name};
use script_layout_interface::wrapper_traits::{ThreadSafeLayoutElement, ThreadSafeLayoutNode};
use range::Range;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{
LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use script_layout_interface::{LayoutElementType, LayoutNodeType};
use selectors::Element as SelectorsElement;
use servo_arc::Arc as ServoArc;
use style::dom::{TElement, TShadowRoot};
use style::dom::{NodeInfo, TElement, TNode, TShadowRoot};
use style::properties::ComputedValues;
use style::selector_parser::PseudoElement;
use style::values::generics::counters::{Content, ContentItem};
@ -18,6 +23,7 @@ use style::values::specified::Quotes;
use crate::context::LayoutContext;
use crate::dom::{BoxSlot, LayoutBox, NodeExt};
use crate::flow::inline::SharedInlineStyles;
use crate::fragment_tree::{BaseFragmentInfo, FragmentFlags, Tag};
use crate::quotes::quotes_for_lang;
use crate::replaced::ReplacedContents;
@ -26,15 +32,15 @@ use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside, DisplayOuts
/// A data structure used to pass and store related layout information together to
/// avoid having to repeat the same arguments in argument lists.
#[derive(Clone)]
pub(crate) struct NodeAndStyleInfo<Node> {
pub node: Node,
pub(crate) struct NodeAndStyleInfo<'dom> {
pub node: ServoLayoutNode<'dom>,
pub pseudo_element_type: Option<PseudoElement>,
pub style: ServoArc<ComputedValues>,
}
impl<'dom, Node: NodeExt<'dom>> NodeAndStyleInfo<Node> {
impl<'dom> NodeAndStyleInfo<'dom> {
fn new_with_pseudo(
node: Node,
node: ServoLayoutNode<'dom>,
pseudo_element_type: PseudoElement,
style: ServoArc<ComputedValues>,
) -> Self {
@ -45,7 +51,7 @@ impl<'dom, Node: NodeExt<'dom>> NodeAndStyleInfo<Node> {
}
}
pub(crate) fn new(node: Node, style: ServoArc<ComputedValues>) -> Self {
pub(crate) fn new(node: ServoLayoutNode<'dom>, style: ServoArc<ComputedValues>) -> Self {
Self {
node,
pseudo_element_type: None,
@ -53,8 +59,14 @@ impl<'dom, Node: NodeExt<'dom>> NodeAndStyleInfo<Node> {
}
}
/// Whether this is a container for the editable text within a single-line text input.
/// This is used to solve the special case of line height for a text editor.
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
// FIXME(stevennovaryo): Now, this would also refer to HTMLInputElement, to handle input
// elements without shadow DOM.
pub(crate) fn is_single_line_text_input(&self) -> bool {
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) ||
self.node.is_text_control_inner_editor()
}
pub(crate) fn pseudo(
@ -74,13 +86,18 @@ impl<'dom, Node: NodeExt<'dom>> NodeAndStyleInfo<Node> {
style,
})
}
pub(crate) fn get_selected_style(&self) -> ServoArc<ComputedValues> {
self.node.to_threadsafe().selected_style()
}
pub(crate) fn get_selection_range(&self) -> Option<Range<ByteIndex>> {
self.node.to_threadsafe().selection()
}
}
impl<'dom, Node> From<&NodeAndStyleInfo<Node>> for BaseFragmentInfo
where
Node: NodeExt<'dom>,
{
fn from(info: &NodeAndStyleInfo<Node>) -> Self {
impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo {
fn from(info: &NodeAndStyleInfo<'dom>) -> Self {
let node = info.node;
let pseudo = info.pseudo_element_type;
let threadsafe_node = node.to_threadsafe();
@ -164,61 +181,54 @@ pub(super) enum PseudoElementContentItem {
Replaced(ReplacedContents),
}
pub(super) trait TraversalHandler<'dom, Node>
where
Node: 'dom,
{
fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>);
pub(super) trait TraversalHandler<'dom> {
fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>);
/// Or pseudo-element
fn handle_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
info: &NodeAndStyleInfo<'dom>,
display: DisplayGeneratingBox,
contents: Contents,
box_slot: BoxSlot<'dom>,
);
/// Notify the handler that we are about to recurse into a `display: contents` element.
fn enter_display_contents(&mut self, _: SharedInlineStyles) {}
/// Notify the handler that we have finished a `display: contents` element.
fn leave_display_contents(&mut self) {}
}
fn traverse_children_of<'dom, Node>(
parent_element: Node,
fn traverse_children_of<'dom>(
parent_element: ServoLayoutNode<'dom>,
context: &LayoutContext,
handler: &mut impl TraversalHandler<'dom, Node>,
) where
Node: NodeExt<'dom>,
{
handler: &mut impl TraversalHandler<'dom>,
) {
traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler);
let is_text_input_element = matches!(
parent_element.type_id(),
LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
);
let is_textarea_element = matches!(
parent_element.type_id(),
LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement)
);
if is_text_input_element || is_textarea_element {
let info = NodeAndStyleInfo::new(parent_element, parent_element.style(context));
if is_text_input_element {
if parent_element.is_text_input() {
let info = NodeAndStyleInfo::new(
parent_element,
parent_element.style(context.shared_context()),
);
let node_text_content = parent_element.to_threadsafe().node_text_content();
if node_text_content.is_empty() {
// The addition of zero-width space here forces the text input to have an inline formatting
// context that might otherwise be trimmed if there's no text. This is important to ensure
// that the input element is at least as tall as the line gap of the caret:
// <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
//
// This is also used to ensure that the caret will still be rendered when the input is empty.
// TODO: Is there a less hacky way to do this?
handler.handle_text(&info, "\u{200B}".into());
} else {
handler.handle_text(&info, node_text_content);
}
handler.handle_text(&info, parent_element.to_threadsafe().node_text_content());
}
if !is_text_input_element && !is_textarea_element {
} else {
for child in iter_child_nodes(parent_element) {
if child.is_text_node() {
let info = NodeAndStyleInfo::new(child, child.style(context));
let info = NodeAndStyleInfo::new(child, child.style(context.shared_context()));
handler.handle_text(&info, child.to_threadsafe().node_text_content());
} else if child.is_element() {
traverse_element(child, context, handler);
@ -229,15 +239,17 @@ fn traverse_children_of<'dom, Node>(
traverse_eager_pseudo_element(PseudoElement::After, parent_element, context, handler);
}
fn traverse_element<'dom, Node>(
element: Node,
fn traverse_element<'dom>(
element: ServoLayoutNode<'dom>,
context: &LayoutContext,
handler: &mut impl TraversalHandler<'dom, Node>,
) where
Node: NodeExt<'dom>,
{
handler: &mut impl TraversalHandler<'dom>,
) {
// Clear any existing pseudo-element box slot, because markers are not handled like
// `::before`` and `::after`. They are processed during box tree creation.
element.unset_pseudo_element_box(PseudoElement::Marker);
let replaced = ReplacedContents::for_element(element, context);
let style = element.style(context);
let style = element.style(context.shared_context());
match Display::from(style.get_box().display) {
Display::None => element.unset_all_boxes(),
Display::Contents => {
@ -246,8 +258,15 @@ fn traverse_element<'dom, Node>(
// <https://drafts.csswg.org/css-display-3/#valdef-display-contents>
element.unset_all_boxes()
} else {
element.element_box_slot().set(LayoutBox::DisplayContents);
traverse_children_of(element, context, handler)
let shared_inline_styles: SharedInlineStyles =
(&NodeAndStyleInfo::new(element, style)).into();
element
.element_box_slot()
.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
handler.enter_display_contents(shared_inline_styles);
traverse_children_of(element, context, handler);
handler.leave_display_contents();
}
},
Display::GeneratingBox(display) => {
@ -271,14 +290,12 @@ fn traverse_element<'dom, Node>(
}
}
fn traverse_eager_pseudo_element<'dom, Node>(
fn traverse_eager_pseudo_element<'dom>(
pseudo_element_type: PseudoElement,
node: Node,
node: ServoLayoutNode<'dom>,
context: &LayoutContext,
handler: &mut impl TraversalHandler<'dom, Node>,
) where
Node: NodeExt<'dom>,
{
handler: &mut impl TraversalHandler<'dom>,
) {
assert!(pseudo_element_type.is_eager());
// First clear any old contents from the node.
@ -302,8 +319,12 @@ fn traverse_eager_pseudo_element<'dom, Node>(
Display::Contents => {
let items = generate_pseudo_element_content(&info.style, node, context);
let box_slot = node.pseudo_element_box_slot(pseudo_element_type);
box_slot.set(LayoutBox::DisplayContents);
let shared_inline_styles: SharedInlineStyles = (&info).into();
box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
handler.enter_display_contents(shared_inline_styles);
traverse_pseudo_element_contents(&info, context, handler, items);
handler.leave_display_contents();
},
Display::GeneratingBox(display) => {
let items = generate_pseudo_element_content(&info.style, node, context);
@ -314,14 +335,12 @@ fn traverse_eager_pseudo_element<'dom, Node>(
}
}
fn traverse_pseudo_element_contents<'dom, Node>(
info: &NodeAndStyleInfo<Node>,
fn traverse_pseudo_element_contents<'dom>(
info: &NodeAndStyleInfo<'dom>,
context: &LayoutContext,
handler: &mut impl TraversalHandler<'dom, Node>,
handler: &mut impl TraversalHandler<'dom>,
items: Vec<PseudoElementContentItem>,
) where
Node: NodeExt<'dom>,
{
) {
let mut anonymous_info = None;
for item in items {
match item {
@ -381,14 +400,12 @@ impl std::convert::TryFrom<Contents> for NonReplacedContents {
}
impl NonReplacedContents {
pub(crate) fn traverse<'dom, Node>(
pub(crate) fn traverse<'dom>(
self,
context: &LayoutContext,
info: &NodeAndStyleInfo<Node>,
handler: &mut impl TraversalHandler<'dom, Node>,
) where
Node: NodeExt<'dom>,
{
info: &NodeAndStyleInfo<'dom>,
handler: &mut impl TraversalHandler<'dom>,
) {
match self {
NonReplacedContents::OfElement | NonReplacedContents::OfTextControl => {
traverse_children_of(info.node, context, handler)
@ -412,14 +429,11 @@ where
}
/// <https://www.w3.org/TR/CSS2/generate.html#propdef-content>
fn generate_pseudo_element_content<'dom, Node>(
fn generate_pseudo_element_content(
pseudo_element_style: &ComputedValues,
element: Node,
element: ServoLayoutNode<'_>,
context: &LayoutContext,
) -> Vec<PseudoElementContentItem>
where
Node: NodeExt<'dom>,
{
) -> Vec<PseudoElementContentItem> {
match &pseudo_element_style.get_counters().content {
Content::Items(items) => {
let mut vec = vec![];
@ -502,18 +516,14 @@ where
}
}
pub enum ChildNodeIterator<Node> {
pub enum ChildNodeIterator<'dom> {
/// Iterating over the children of a node
Node(Option<Node>),
Node(Option<ServoLayoutNode<'dom>>),
/// Iterating over the assigned nodes of a `HTMLSlotElement`
Slottables(<Vec<Node> as IntoIterator>::IntoIter),
Slottables(<Vec<ServoLayoutNode<'dom>> as IntoIterator>::IntoIter),
}
#[allow(clippy::unnecessary_to_owned)] // Clippy is wrong.
pub(crate) fn iter_child_nodes<'dom, Node>(parent: Node) -> ChildNodeIterator<Node>
where
Node: NodeExt<'dom>,
{
pub(crate) fn iter_child_nodes(parent: ServoLayoutNode<'_>) -> ChildNodeIterator<'_> {
if let Some(element) = parent.as_element() {
if let Some(shadow) = element.shadow_root() {
return iter_child_nodes(shadow.as_node());
@ -521,6 +531,7 @@ where
let slotted_nodes = element.slotted_nodes();
if !slotted_nodes.is_empty() {
#[allow(clippy::unnecessary_to_owned)] // Clippy is wrong.
return ChildNodeIterator::Slottables(slotted_nodes.to_owned().into_iter());
}
}
@ -529,11 +540,8 @@ where
ChildNodeIterator::Node(first)
}
impl<'dom, Node> Iterator for ChildNodeIterator<Node>
where
Node: NodeExt<'dom>,
{
type Item = Node;
impl<'dom> Iterator for ChildNodeIterator<'dom> {
type Item = ServoLayoutNode<'dom>;
fn next(&mut self) -> Option<Self::Item> {
match self {
@ -547,4 +555,4 @@ where
}
}
impl<'dom, Node> FusedIterator for ChildNodeIterator<Node> where Node: NodeExt<'dom> {}
impl FusedIterator for ChildNodeIterator<'_> {}

View file

@ -0,0 +1,272 @@
/* 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/. */
//! <https://drafts.csswg.org/css-flexbox/#box-model>
use malloc_size_of_derive::MallocSizeOf;
use style::properties::longhands::flex_direction::computed_value::T as FlexDirection;
use crate::geom::{LogicalRect, LogicalSides, LogicalVec2};
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct FlexRelativeVec2<T> {
pub main: T,
pub cross: T,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct FlexRelativeSides<T> {
pub cross_start: T,
pub main_start: T,
pub cross_end: T,
pub main_end: T,
}
pub(super) struct FlexRelativeRect<T> {
pub start_corner: FlexRelativeVec2<T>,
pub size: FlexRelativeVec2<T>,
}
impl<T> std::ops::Add for FlexRelativeVec2<T>
where
T: std::ops::Add,
{
type Output = FlexRelativeVec2<T::Output>;
fn add(self, rhs: Self) -> Self::Output {
FlexRelativeVec2 {
main: self.main + rhs.main,
cross: self.cross + rhs.cross,
}
}
}
impl<T> std::ops::Sub for FlexRelativeVec2<T>
where
T: std::ops::Sub,
{
type Output = FlexRelativeVec2<T::Output>;
fn sub(self, rhs: Self) -> Self::Output {
FlexRelativeVec2 {
main: self.main - rhs.main,
cross: self.cross - rhs.cross,
}
}
}
impl<T> FlexRelativeSides<T> {
pub fn sum_by_axis(self) -> FlexRelativeVec2<T::Output>
where
T: std::ops::Add,
{
FlexRelativeVec2 {
main: self.main_start + self.main_end,
cross: self.cross_start + self.cross_end,
}
}
}
/// One of the two bits set by the `flex-direction` property
/// (The other is "forward" v.s. reverse.)
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
pub(super) enum FlexAxis {
/// The main axis is the inline axis of the container (not necessarily of flex items!),
/// cross is block.
Row,
/// The main axis is the block axis, cross is inline.
Column,
}
/// Which flow-relative sides map to the main-start and cross-start sides, respectively.
/// See <https://drafts.csswg.org/css-flexbox/#box-model>
#[derive(Clone, Copy, Debug, MallocSizeOf)]
pub(super) enum MainStartCrossStart {
InlineStartBlockStart,
InlineStartBlockEnd,
BlockStartInlineStart,
BlockStartInlineEnd,
InlineEndBlockStart,
InlineEndBlockEnd,
BlockEndInlineStart,
BlockEndInlineEnd,
}
impl FlexAxis {
pub fn from(flex_direction: FlexDirection) -> Self {
match flex_direction {
FlexDirection::Row | FlexDirection::RowReverse => FlexAxis::Row,
FlexDirection::Column | FlexDirection::ColumnReverse => FlexAxis::Column,
}
}
pub fn vec2_to_flex_relative<T>(self, flow_relative: LogicalVec2<T>) -> FlexRelativeVec2<T> {
let LogicalVec2 { inline, block } = flow_relative;
match self {
FlexAxis::Row => FlexRelativeVec2 {
main: inline,
cross: block,
},
FlexAxis::Column => FlexRelativeVec2 {
main: block,
cross: inline,
},
}
}
pub fn vec2_to_flow_relative<T>(self, flex_relative: FlexRelativeVec2<T>) -> LogicalVec2<T> {
let FlexRelativeVec2 { main, cross } = flex_relative;
match self {
FlexAxis::Row => LogicalVec2 {
inline: main,
block: cross,
},
FlexAxis::Column => LogicalVec2 {
block: main,
inline: cross,
},
}
}
}
macro_rules! sides_mapping_methods {
(
$(
$variant: path => {
$( $flex_relative_side: ident <=> $flow_relative_side: ident, )+
},
)+
) => {
pub fn sides_to_flex_relative<T>(self, flow_relative: LogicalSides<T>) -> FlexRelativeSides<T> {
match self {
$(
$variant => FlexRelativeSides {
$( $flex_relative_side: flow_relative.$flow_relative_side, )+
},
)+
}
}
pub fn sides_to_flow_relative<T>(self, flex_relative: FlexRelativeSides<T>) -> LogicalSides<T> {
match self {
$(
$variant => LogicalSides {
$( $flow_relative_side: flex_relative.$flex_relative_side, )+
},
)+
}
}
}
}
impl MainStartCrossStart {
pub fn from(flex_direction: FlexDirection, flex_wrap_reverse: bool) -> Self {
match (flex_direction, flex_wrap_reverse) {
// See definition of each keyword in
// https://drafts.csswg.org/css-flexbox/#flex-direction-property and
// https://drafts.csswg.org/css-flexbox/#flex-wrap-property,
// or the tables (though they map to physical rather than flow-relative) at
// https://drafts.csswg.org/css-flexbox/#axis-mapping
(FlexDirection::Row, true) => MainStartCrossStart::InlineStartBlockEnd,
(FlexDirection::Row, false) => MainStartCrossStart::InlineStartBlockStart,
(FlexDirection::Column, true) => MainStartCrossStart::BlockStartInlineEnd,
(FlexDirection::Column, false) => MainStartCrossStart::BlockStartInlineStart,
(FlexDirection::RowReverse, true) => MainStartCrossStart::InlineEndBlockEnd,
(FlexDirection::RowReverse, false) => MainStartCrossStart::InlineEndBlockStart,
(FlexDirection::ColumnReverse, true) => MainStartCrossStart::BlockEndInlineEnd,
(FlexDirection::ColumnReverse, false) => MainStartCrossStart::BlockEndInlineStart,
}
}
sides_mapping_methods! {
MainStartCrossStart::InlineStartBlockStart => {
main_start <=> inline_start,
cross_start <=> block_start,
main_end <=> inline_end,
cross_end <=> block_end,
},
MainStartCrossStart::InlineStartBlockEnd => {
main_start <=> inline_start,
cross_start <=> block_end,
main_end <=> inline_end,
cross_end <=> block_start,
},
MainStartCrossStart::BlockStartInlineStart => {
main_start <=> block_start,
cross_start <=> inline_start,
main_end <=> block_end,
cross_end <=> inline_end,
},
MainStartCrossStart::BlockStartInlineEnd => {
main_start <=> block_start,
cross_start <=> inline_end,
main_end <=> block_end,
cross_end <=> inline_start,
},
MainStartCrossStart::InlineEndBlockStart => {
main_start <=> inline_end,
cross_start <=> block_start,
main_end <=> inline_start,
cross_end <=> block_end,
},
MainStartCrossStart::InlineEndBlockEnd => {
main_start <=> inline_end,
cross_start <=> block_end,
main_end <=> inline_start,
cross_end <=> block_start,
},
MainStartCrossStart::BlockEndInlineStart => {
main_start <=> block_end,
cross_start <=> inline_start,
main_end <=> block_start,
cross_end <=> inline_end,
},
MainStartCrossStart::BlockEndInlineEnd => {
main_start <=> block_end,
cross_start <=> inline_end,
main_end <=> block_start,
cross_end <=> inline_start,
},
}
}
/// The start corner coordinates in both the input rectangle and output rectangle
/// are relative to some “base rectangle” whose size is passed here.
pub(super) fn rect_to_flow_relative<T>(
flex_axis: FlexAxis,
main_start_cross_start_sides_are: MainStartCrossStart,
base_rect_size: FlexRelativeVec2<T>,
rect: FlexRelativeRect<T>,
) -> LogicalRect<T>
where
T: Copy + std::ops::Add<Output = T> + std::ops::Sub<Output = T>,
{
// First, convert from (start corner, size) to offsets from the edges of the base rectangle
let end_corner_position = rect.start_corner + rect.size;
let end_corner_offsets = base_rect_size - end_corner_position;
// No-ops, but hopefully clarifies to human readers:
let start_corner_position = rect.start_corner;
let start_corner_offsets = start_corner_position;
// Then, convert to flow-relative using methods above
let flow_relative_offsets =
main_start_cross_start_sides_are.sides_to_flow_relative(FlexRelativeSides {
main_start: start_corner_offsets.main,
cross_start: start_corner_offsets.cross,
main_end: end_corner_offsets.main,
cross_end: end_corner_offsets.cross,
});
let flow_relative_base_rect_size = flex_axis.vec2_to_flow_relative(base_rect_size);
// Finally, convert back to (start corner, size)
let start_corner = LogicalVec2 {
inline: flow_relative_offsets.inline_start,
block: flow_relative_offsets.block_start,
};
let end_corner_position = LogicalVec2 {
inline: flow_relative_base_rect_size.inline - flow_relative_offsets.inline_end,
block: flow_relative_base_rect_size.block - flow_relative_offsets.block_end,
};
let size = end_corner_position - start_corner;
LogicalRect { start_corner, size }
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,223 @@
/* 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 geom::{FlexAxis, MainStartCrossStart};
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc as ServoArc;
use style::context::SharedStyleContext;
use style::logical_geometry::WritingMode;
use style::properties::ComputedValues;
use style::properties::longhands::align_items::computed_value::T as AlignItems;
use style::properties::longhands::flex_direction::computed_value::T as FlexDirection;
use style::properties::longhands::flex_wrap::computed_value::T as FlexWrap;
use style::values::computed::{AlignContent, JustifyContent};
use style::values::specified::align::AlignFlags;
use crate::PropagatedBoxTreeData;
use crate::cell::ArcRefCell;
use crate::construct_modern::{ModernContainerBuilder, ModernItemKind};
use crate::context::LayoutContext;
use crate::dom::LayoutBox;
use crate::dom_traversal::{NodeAndStyleInfo, NonReplacedContents};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::{BaseFragmentInfo, Fragment};
use crate::positioned::AbsolutelyPositionedBox;
mod geom;
mod layout;
/// A structure to hold the configuration of a flex container for use during layout
/// and preferred width calculation.
#[derive(Clone, Debug, MallocSizeOf)]
pub(crate) struct FlexContainerConfig {
container_is_single_line: bool,
writing_mode: WritingMode,
flex_axis: FlexAxis,
flex_direction: FlexDirection,
flex_direction_is_reversed: bool,
flex_wrap: FlexWrap,
flex_wrap_is_reversed: bool,
main_start_cross_start_sides_are: MainStartCrossStart,
align_content: AlignContent,
align_items: AlignItems,
justify_content: JustifyContent,
}
impl FlexContainerConfig {
fn new(container_style: &ComputedValues) -> FlexContainerConfig {
let flex_direction = container_style.clone_flex_direction();
let flex_axis = FlexAxis::from(flex_direction);
let flex_wrap = container_style.get_position().flex_wrap;
let container_is_single_line = match flex_wrap {
FlexWrap::Nowrap => true,
FlexWrap::Wrap | FlexWrap::WrapReverse => false,
};
let flex_direction_is_reversed = match flex_direction {
FlexDirection::Row | FlexDirection::Column => false,
FlexDirection::RowReverse | FlexDirection::ColumnReverse => true,
};
let flex_wrap_reverse = match flex_wrap {
FlexWrap::Nowrap | FlexWrap::Wrap => false,
FlexWrap::WrapReverse => true,
};
let align_content = container_style.clone_align_content();
let align_items = AlignItems(match container_style.clone_align_items().0 {
AlignFlags::AUTO | AlignFlags::NORMAL => AlignFlags::STRETCH,
align => align,
});
let justify_content = container_style.clone_justify_content();
let main_start_cross_start_sides_are =
MainStartCrossStart::from(flex_direction, flex_wrap_reverse);
FlexContainerConfig {
container_is_single_line,
writing_mode: container_style.writing_mode,
flex_axis,
flex_direction,
flex_direction_is_reversed,
flex_wrap,
flex_wrap_is_reversed: flex_wrap_reverse,
main_start_cross_start_sides_are,
align_content,
align_items,
justify_content,
}
}
}
#[derive(Debug, MallocSizeOf)]
pub(crate) struct FlexContainer {
children: Vec<ArcRefCell<FlexLevelBox>>,
style: ServoArc<ComputedValues>,
/// The configuration of this [`FlexContainer`].
config: FlexContainerConfig,
}
impl FlexContainer {
pub fn construct(
context: &LayoutContext,
info: &NodeAndStyleInfo<'_>,
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
) -> Self {
let mut builder = ModernContainerBuilder::new(context, info, propagated_data);
contents.traverse(context, info, &mut builder);
let items = builder.finish();
let children = items
.into_iter()
.map(|item| {
let box_ = match item.kind {
ModernItemKind::InFlow => ArcRefCell::new(FlexLevelBox::FlexItem(
FlexItemBox::new(item.formatting_context),
)),
ModernItemKind::OutOfFlow => {
let abs_pos_box =
ArcRefCell::new(AbsolutelyPositionedBox::new(item.formatting_context));
ArcRefCell::new(FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(abs_pos_box))
},
};
if let Some(box_slot) = item.box_slot {
box_slot.set(LayoutBox::FlexLevel(box_.clone()));
}
box_
})
.collect();
Self {
children,
style: info.style.clone(),
config: FlexContainerConfig::new(&info.style),
}
}
pub(crate) fn repair_style(&mut self, new_style: &ServoArc<ComputedValues>) {
self.config = FlexContainerConfig::new(new_style);
self.style = new_style.clone();
}
}
#[derive(Debug, MallocSizeOf)]
pub(crate) enum FlexLevelBox {
FlexItem(FlexItemBox),
OutOfFlowAbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>),
}
impl FlexLevelBox {
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &ServoArc<ComputedValues>,
) {
match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box
.independent_formatting_context
.repair_style(context, node, new_style),
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut()
.context
.repair_style(context, node, new_style),
}
}
pub(crate) fn invalidate_cached_fragment(&self) {
match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box
.independent_formatting_context
.base
.invalidate_cached_fragment(),
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow()
.context
.base
.invalidate_cached_fragment(),
}
}
pub(crate) fn fragments(&self) -> Vec<Fragment> {
match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box
.independent_formatting_context
.base
.fragments(),
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
positioned_box.borrow().context.base.fragments()
},
}
}
}
#[derive(MallocSizeOf)]
pub(crate) struct FlexItemBox {
independent_formatting_context: IndependentFormattingContext,
}
impl std::fmt::Debug for FlexItemBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FlexItemBox")
}
}
impl FlexItemBox {
fn new(independent_formatting_context: IndependentFormattingContext) -> Self {
Self {
independent_formatting_context,
}
}
fn style(&self) -> &ServoArc<ComputedValues> {
self.independent_formatting_context.style()
}
fn base_fragment_info(&self) -> BaseFragmentInfo {
self.independent_formatting_context.base_fragment_info()
}
}

View file

@ -0,0 +1,772 @@
/* 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::borrow::Cow;
use std::convert::TryFrom;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use servo_arc::Arc;
use style::properties::ComputedValues;
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
use style::selector_parser::PseudoElement;
use style::str::char_is_whitespace;
use super::OutsideMarker;
use super::inline::construct::InlineFormattingContextBuilder;
use super::inline::inline_box::InlineBox;
use super::inline::{InlineFormattingContext, SharedInlineStyles};
use crate::PropagatedBoxTreeData;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::{BoxSlot, LayoutBox, NodeExt};
use crate::dom_traversal::{
Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler,
};
use crate::flow::float::FloatBox;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentFlags;
use crate::layout_box_base::LayoutBoxBase;
use crate::positioned::AbsolutelyPositionedBox;
use crate::style_ext::{ComputedValuesExt, DisplayGeneratingBox, DisplayInside, DisplayOutside};
use crate::table::{AnonymousTableContent, Table};
impl BlockFormattingContext {
pub(crate) fn construct(
context: &LayoutContext,
info: &NodeAndStyleInfo<'_>,
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
is_list_item: bool,
) -> Self {
Self::from_block_container(BlockContainer::construct(
context,
info,
contents,
propagated_data,
is_list_item,
))
}
pub(crate) fn from_block_container(contents: BlockContainer) -> Self {
let contains_floats = contents.contains_floats();
Self {
contents,
contains_floats,
}
}
}
struct BlockLevelJob<'dom> {
info: NodeAndStyleInfo<'dom>,
box_slot: BoxSlot<'dom>,
propagated_data: PropagatedBoxTreeData,
kind: BlockLevelCreator,
}
enum BlockLevelCreator {
SameFormattingContextBlock(IntermediateBlockContainer),
Independent {
display_inside: DisplayInside,
contents: Contents,
},
OutOfFlowAbsolutelyPositionedBox {
display_inside: DisplayInside,
contents: Contents,
},
OutOfFlowFloatBox {
display_inside: DisplayInside,
contents: Contents,
},
OutsideMarker {
list_item_style: Arc<ComputedValues>,
contents: Vec<PseudoElementContentItem>,
},
AnonymousTable {
table_block: ArcRefCell<BlockLevelBox>,
},
}
/// A block container that may still have to be constructed.
///
/// Represents either the inline formatting context of an anonymous block
/// box or the yet-to-be-computed block container generated from the children
/// of a given element.
///
/// Deferring allows using rayons `into_par_iter`.
enum IntermediateBlockContainer {
InlineFormattingContext(BlockContainer),
Deferred {
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
is_list_item: bool,
},
}
/// A builder for a block container.
///
/// This builder starts from the first child of a given DOM node
/// and does a preorder traversal of all of its inclusive siblings.
pub(crate) struct BlockContainerBuilder<'dom, 'style> {
context: &'style LayoutContext<'style>,
/// This NodeAndStyleInfo contains the root node, the corresponding pseudo
/// content designator, and the block container style.
info: &'style NodeAndStyleInfo<'dom>,
/// The list of block-level boxes to be built for the final block container.
///
/// Contains all the block-level jobs we found traversing the tree
/// so far, if this is empty at the end of the traversal and the ongoing
/// inline formatting context is not empty, the block container establishes
/// an inline formatting context (see end of `build`).
///
/// DOM nodes which represent block-level boxes are immediately pushed
/// to this list with their style without ever being traversed at this
/// point, instead we just move to their next sibling. If the DOM node
/// doesn't have a next sibling, we either reached the end of the container
/// root or there are ongoing inline-level boxes
/// (see `handle_block_level_element`).
block_level_boxes: Vec<BlockLevelJob<'dom>>,
/// Whether or not this builder has yet produced a block which would be
/// be considered the first line for the purposes of `text-indent`.
have_already_seen_first_line_for_text_indent: bool,
/// The propagated data to use for BoxTree construction.
propagated_data: PropagatedBoxTreeData,
/// The [`InlineFormattingContextBuilder`] if we have encountered any inline items,
/// otherwise None.
///
/// TODO: This can be `OnceCell` once `OnceCell::get_mut_or_init` is stabilized.
inline_formatting_context_builder: Option<InlineFormattingContextBuilder>,
/// The [`NodeAndStyleInfo`] to use for anonymous block boxes pushed to the list of
/// block-level boxes, lazily initialized.
anonymous_box_info: Option<NodeAndStyleInfo<'dom>>,
/// A collection of content that is being added to an anonymous table. This is
/// composed of any sequence of internal table elements or table captions that
/// are found outside of a table.
anonymous_table_content: Vec<AnonymousTableContent<'dom>>,
/// Any [`InlineFormattingContexts`] created need to know about the ongoing `display: contents`
/// ancestors that have been processed. This `Vec` allows passing those into new
/// [`InlineFormattingContext`]s that we create.
display_contents_shared_styles: Vec<SharedInlineStyles>,
}
impl BlockContainer {
pub fn construct(
context: &LayoutContext,
info: &NodeAndStyleInfo<'_>,
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
is_list_item: bool,
) -> BlockContainer {
let mut builder = BlockContainerBuilder::new(context, info, propagated_data);
if is_list_item {
if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) {
match marker_info.style.clone_list_style_position() {
ListStylePosition::Inside => {
builder.handle_list_item_marker_inside(&marker_info, info, marker_contents)
},
ListStylePosition::Outside => builder.handle_list_item_marker_outside(
&marker_info,
info,
marker_contents,
info.style.clone(),
),
}
}
}
contents.traverse(context, info, &mut builder);
builder.finish()
}
}
impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
pub(crate) fn new(
context: &'style LayoutContext,
info: &'style NodeAndStyleInfo<'dom>,
propagated_data: PropagatedBoxTreeData,
) -> Self {
BlockContainerBuilder {
context,
info,
block_level_boxes: Vec::new(),
propagated_data,
have_already_seen_first_line_for_text_indent: false,
anonymous_box_info: None,
anonymous_table_content: Vec::new(),
inline_formatting_context_builder: None,
display_contents_shared_styles: Vec::new(),
}
}
fn currently_processing_inline_box(&self) -> bool {
self.inline_formatting_context_builder
.as_ref()
.is_some_and(InlineFormattingContextBuilder::currently_processing_inline_box)
}
fn ensure_inline_formatting_context_builder(&mut self) -> &mut InlineFormattingContextBuilder {
self.inline_formatting_context_builder
.get_or_insert_with(|| {
let mut builder = InlineFormattingContextBuilder::new(self.info);
for shared_inline_styles in self.display_contents_shared_styles.iter() {
builder.enter_display_contents(shared_inline_styles.clone());
}
builder
})
}
fn finish_ongoing_inline_formatting_context(&mut self) -> Option<InlineFormattingContext> {
self.inline_formatting_context_builder.take()?.finish(
self.context,
!self.have_already_seen_first_line_for_text_indent,
self.info.is_single_line_text_input(),
self.info.style.writing_mode.to_bidi_level(),
)
}
pub(crate) fn finish(mut self) -> BlockContainer {
debug_assert!(!self.currently_processing_inline_box());
self.finish_anonymous_table_if_needed();
if let Some(inline_formatting_context) = self.finish_ongoing_inline_formatting_context() {
// There are two options here. This block was composed of both one or more inline formatting contexts
// and child blocks OR this block was a single inline formatting context. In the latter case, we
// just return the inline formatting context as the block itself.
if self.block_level_boxes.is_empty() {
return BlockContainer::InlineFormattingContext(inline_formatting_context);
}
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
let context = self.context;
let block_level_boxes = if self.context.use_rayon {
self.block_level_boxes
.into_par_iter()
.map(|block_level_job| block_level_job.finish(context))
.collect()
} else {
self.block_level_boxes
.into_iter()
.map(|block_level_job| block_level_job.finish(context))
.collect()
};
BlockContainer::BlockLevelBoxes(block_level_boxes)
}
fn finish_anonymous_table_if_needed(&mut self) {
if self.anonymous_table_content.is_empty() {
return;
}
// From https://drafts.csswg.org/css-tables/#fixup-algorithm:
// > If the boxs parent is an inline, run-in, or ruby box (or any box that would perform
// > inlinification of its children), then an inline-table box must be generated; otherwise
// > it must be a table box.
//
// Note that text content in the inline formatting context isn't enough to force the
// creation of an inline table. It requires the parent to be an inline box.
let inline_table = self.currently_processing_inline_box();
let contents: Vec<AnonymousTableContent<'dom>> =
self.anonymous_table_content.drain(..).collect();
let last_text = match contents.last() {
Some(AnonymousTableContent::Text(info, text)) => Some((info.clone(), text.clone())),
_ => None,
};
let (table_info, ifc) =
Table::construct_anonymous(self.context, self.info, contents, self.propagated_data);
if inline_table {
self.ensure_inline_formatting_context_builder()
.push_atomic(ifc);
} else {
let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc));
if let Some(inline_formatting_context) = self.finish_ongoing_inline_formatting_context()
{
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
self.block_level_boxes.push(BlockLevelJob {
info: table_info,
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::AnonymousTable { table_block },
propagated_data: self.propagated_data,
});
}
// If the last element in the anonymous table content is whitespace, that
// whitespace doesn't actually belong to the table. It should be processed outside
// ie become a space between the anonymous table and the rest of the block
// content. Anonymous tables are really only constructed around internal table
// elements and the whitespace between them, so this trailing whitespace should
// not be included.
//
// See https://drafts.csswg.org/css-tables/#fixup-algorithm sections "Remove
// irrelevant boxes" and "Generate missing parents."
if let Some((info, text)) = last_text {
self.handle_text(&info, text);
}
}
}
impl<'dom> TraversalHandler<'dom> for BlockContainerBuilder<'dom, '_> {
fn handle_element(
&mut self,
info: &NodeAndStyleInfo<'dom>,
display: DisplayGeneratingBox,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
match display {
DisplayGeneratingBox::OutsideInside { outside, inside } => {
self.finish_anonymous_table_if_needed();
match outside {
DisplayOutside::Inline => {
self.handle_inline_level_element(info, inside, contents, box_slot)
},
DisplayOutside::Block => {
let box_style = info.style.get_box();
// Floats and abspos cause blockification, so they only happen in this case.
// https://drafts.csswg.org/css2/visuren.html#dis-pos-flo
if box_style.position.is_absolutely_positioned() {
self.handle_absolutely_positioned_element(
info, inside, contents, box_slot,
)
} else if box_style.float.is_floating() {
self.handle_float_element(info, inside, contents, box_slot)
} else {
self.handle_block_level_element(info, inside, contents, box_slot)
}
},
};
},
DisplayGeneratingBox::LayoutInternal(_) => {
self.anonymous_table_content
.push(AnonymousTableContent::Element {
info: info.clone(),
display,
contents,
box_slot,
});
},
}
}
fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) {
if text.is_empty() {
return;
}
// If we are building an anonymous table ie this text directly followed internal
// table elements that did not have a `<table>` ancestor, then we forward all
// whitespace to the table builder.
if !self.anonymous_table_content.is_empty() && text.chars().all(char_is_whitespace) {
self.anonymous_table_content
.push(AnonymousTableContent::Text(info.clone(), text));
return;
} else {
self.finish_anonymous_table_if_needed();
}
self.ensure_inline_formatting_context_builder()
.push_text(text, info);
}
fn enter_display_contents(&mut self, styles: SharedInlineStyles) {
self.display_contents_shared_styles.push(styles.clone());
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
builder.enter_display_contents(styles);
}
}
fn leave_display_contents(&mut self) {
self.display_contents_shared_styles.pop();
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
builder.leave_display_contents();
}
}
}
impl<'dom> BlockContainerBuilder<'dom, '_> {
fn handle_list_item_marker_inside(
&mut self,
marker_info: &NodeAndStyleInfo<'dom>,
container_info: &NodeAndStyleInfo<'dom>,
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
) {
// TODO: We do not currently support saving box slots for ::marker pseudo-elements
// that are part nested in ::before and ::after pseudo elements. For now, just
// forget about them once they are built.
let box_slot = match container_info.pseudo_element_type {
Some(_) => BoxSlot::dummy(),
None => marker_info
.node
.pseudo_element_box_slot(PseudoElement::Marker),
};
self.handle_inline_level_element(
marker_info,
DisplayInside::Flow {
is_list_item: false,
},
NonReplacedContents::OfPseudoElement(contents).into(),
box_slot,
);
}
fn handle_list_item_marker_outside(
&mut self,
marker_info: &NodeAndStyleInfo<'dom>,
container_info: &NodeAndStyleInfo<'dom>,
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
list_item_style: Arc<ComputedValues>,
) {
// TODO: We do not currently support saving box slots for ::marker pseudo-elements
// that are part nested in ::before and ::after pseudo elements. For now, just
// forget about them once they are built.
let box_slot = match container_info.pseudo_element_type {
Some(_) => BoxSlot::dummy(),
None => marker_info
.node
.pseudo_element_box_slot(PseudoElement::Marker),
};
self.block_level_boxes.push(BlockLevelJob {
info: marker_info.clone(),
box_slot,
kind: BlockLevelCreator::OutsideMarker {
contents,
list_item_style,
},
propagated_data: self.propagated_data,
});
}
fn handle_inline_level_element(
&mut self,
info: &NodeAndStyleInfo<'dom>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
let (DisplayInside::Flow { is_list_item }, false) =
(display_inside, contents.is_replaced())
else {
// If this inline element is an atomic, handle it and return.
let context = self.context;
let propagated_data = self.propagated_data;
let atomic = self.ensure_inline_formatting_context_builder().push_atomic(
IndependentFormattingContext::construct(
context,
info,
display_inside,
contents,
propagated_data,
),
);
box_slot.set(LayoutBox::InlineLevel(vec![atomic]));
return;
};
// Otherwise, this is just a normal inline box. Whatever happened before, all we need to do
// before recurring is to remember this ongoing inline level box.
self.ensure_inline_formatting_context_builder()
.start_inline_box(InlineBox::new(info), None);
if is_list_item {
if let Some((marker_info, marker_contents)) =
crate::lists::make_marker(self.context, info)
{
// Ignore `list-style-position` here:
// “If the list item is an inline box: this value is equivalent to `inside`.”
// https://drafts.csswg.org/css-lists/#list-style-position-outside
self.handle_list_item_marker_inside(&marker_info, info, marker_contents)
}
}
// `unwrap` doesnt panic here because `is_replaced` returned `false`.
NonReplacedContents::try_from(contents)
.unwrap()
.traverse(self.context, info, self);
self.finish_anonymous_table_if_needed();
// As we are ending this inline box, during the course of the `traverse()` above, the ongoing
// inline formatting context may have been split around block-level elements. In that case,
// more than a single inline box tree item may have been produced for this inline-level box.
// `InlineFormattingContextBuilder::end_inline_box()` is returning all of those box tree
// items.
box_slot.set(LayoutBox::InlineLevel(
self.inline_formatting_context_builder
.as_mut()
.expect("Should be building an InlineFormattingContext")
.end_inline_box(),
));
}
fn handle_block_level_element(
&mut self,
info: &NodeAndStyleInfo<'dom>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
// We just found a block level element, all ongoing inline level boxes
// need to be split around it.
//
// After calling `split_around_block_and_finish`,
// `self.inline_formatting_context_builder` is set up with the state
// that we want to have after we push the block below.
if let Some(inline_formatting_context) = self
.inline_formatting_context_builder
.as_mut()
.and_then(|builder| {
builder.split_around_block_and_finish(
self.context,
!self.have_already_seen_first_line_for_text_indent,
self.info.style.writing_mode.to_bidi_level(),
)
})
{
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
let propagated_data = self.propagated_data;
let kind = match contents {
Contents::NonReplaced(contents) => match display_inside {
DisplayInside::Flow { is_list_item }
// Fragment flags are just used to indicate that the element is not replaced, so empty
// flags are okay here.
if !info.style.establishes_block_formatting_context(
FragmentFlags::empty()
) =>
{
BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::Deferred {
contents,
propagated_data,
is_list_item,
},
)
},
_ => BlockLevelCreator::Independent {
display_inside,
contents: contents.into(),
},
},
Contents::Replaced(contents) => {
let contents = Contents::Replaced(contents);
BlockLevelCreator::Independent {
display_inside,
contents,
}
},
};
self.block_level_boxes.push(BlockLevelJob {
info: info.clone(),
box_slot,
kind,
propagated_data,
});
// Any block also counts as the first line for the purposes of text indent. Even if
// they don't actually indent.
self.have_already_seen_first_line_for_text_indent = true;
}
fn handle_absolutely_positioned_element(
&mut self,
info: &NodeAndStyleInfo<'dom>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
if !builder.is_empty() {
let inline_level_box =
builder.push_absolutely_positioned_box(AbsolutelyPositionedBox::construct(
self.context,
info,
display_inside,
contents,
));
box_slot.set(LayoutBox::InlineLevel(vec![inline_level_box]));
return;
}
}
let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox {
contents,
display_inside,
};
self.block_level_boxes.push(BlockLevelJob {
info: info.clone(),
box_slot,
kind,
propagated_data: self.propagated_data,
});
}
fn handle_float_element(
&mut self,
info: &NodeAndStyleInfo<'dom>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
if !builder.is_empty() {
let inline_level_box = builder.push_float_box(FloatBox::construct(
self.context,
info,
display_inside,
contents,
self.propagated_data,
));
box_slot.set(LayoutBox::InlineLevel(vec![inline_level_box]));
return;
}
}
let kind = BlockLevelCreator::OutOfFlowFloatBox {
contents,
display_inside,
};
self.block_level_boxes.push(BlockLevelJob {
info: info.clone(),
box_slot,
kind,
propagated_data: self.propagated_data,
});
}
fn push_block_level_job_for_inline_formatting_context(
&mut self,
inline_formatting_context: InlineFormattingContext,
) {
let layout_context = self.context;
let info = self
.anonymous_box_info
.get_or_insert_with(|| {
self.info
.pseudo(layout_context, PseudoElement::ServoAnonymousBox)
.expect("Should never fail to create anonymous box")
})
.clone();
self.block_level_boxes.push(BlockLevelJob {
info,
// FIXME(nox): We should be storing this somewhere.
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::InlineFormattingContext(
BlockContainer::InlineFormattingContext(inline_formatting_context),
),
),
propagated_data: self.propagated_data,
});
self.have_already_seen_first_line_for_text_indent = true;
}
}
impl BlockLevelJob<'_> {
fn finish(self, context: &LayoutContext) -> ArcRefCell<BlockLevelBox> {
let info = &self.info;
let block_level_box = match self.kind {
BlockLevelCreator::SameFormattingContextBlock(intermediate_block_container) => {
let contents = intermediate_block_container.finish(context, info);
let contains_floats = contents.contains_floats();
ArcRefCell::new(BlockLevelBox::SameFormattingContextBlock {
base: LayoutBoxBase::new(info.into(), info.style.clone()),
contents,
contains_floats,
})
},
BlockLevelCreator::Independent {
display_inside,
contents,
} => {
let context = IndependentFormattingContext::construct(
context,
info,
display_inside,
contents,
self.propagated_data,
);
ArcRefCell::new(BlockLevelBox::Independent(context))
},
BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox {
display_inside,
contents,
} => ArcRefCell::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
ArcRefCell::new(AbsolutelyPositionedBox::construct(
context,
info,
display_inside,
contents,
)),
)),
BlockLevelCreator::OutOfFlowFloatBox {
display_inside,
contents,
} => ArcRefCell::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
context,
info,
display_inside,
contents,
self.propagated_data,
))),
BlockLevelCreator::OutsideMarker {
contents,
list_item_style,
} => {
let contents = NonReplacedContents::OfPseudoElement(contents);
let block_container = BlockContainer::construct(
context,
info,
contents,
self.propagated_data,
false, /* is_list_item */
);
ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker {
base: LayoutBoxBase::new(info.into(), info.style.clone()),
block_container,
list_item_style,
}))
},
BlockLevelCreator::AnonymousTable { table_block } => table_block,
};
self.box_slot
.set(LayoutBox::BlockLevel(block_level_box.clone()));
block_level_box
}
}
impl IntermediateBlockContainer {
fn finish(self, context: &LayoutContext, info: &NodeAndStyleInfo<'_>) -> BlockContainer {
match self {
IntermediateBlockContainer::Deferred {
contents,
propagated_data,
is_list_item,
} => BlockContainer::construct(context, info, contents, propagated_data, is_list_item),
IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container,
}
}
}

View file

@ -13,6 +13,7 @@ use std::ops::Range;
use app_units::{Au, MAX_AU, MIN_AU};
use euclid::num::Zero;
use malloc_size_of_derive::MallocSizeOf;
use servo_arc::Arc;
use style::computed_values::float::T as FloatProperty;
use style::computed_values::position::T as Position;
@ -21,7 +22,6 @@ use style::properties::ComputedValues;
use style::values::computed::Clear as StyleClear;
use crate::context::LayoutContext;
use crate::dom::NodeExt;
use crate::dom_traversal::{Contents, NodeAndStyleInfo};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::{BoxFragment, CollapsedMargin};
@ -31,7 +31,7 @@ use crate::style_ext::{DisplayInside, PaddingBorderMargin};
use crate::{ContainingBlock, PropagatedBoxTreeData};
/// A floating box.
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) struct FloatBox {
/// The formatting context that makes up the content of this box.
pub contents: IndependentFormattingContext,
@ -884,9 +884,9 @@ impl FloatBandLink {
impl FloatBox {
/// Creates a new float box.
pub fn construct<'dom>(
pub fn construct(
context: &LayoutContext,
info: &NodeAndStyleInfo<impl NodeExt<'dom>>,
info: &NodeAndStyleInfo<'_>,
display_inside: DisplayInside,
contents: Contents,
propagated_data: PropagatedBoxTreeData,
@ -897,8 +897,7 @@ impl FloatBox {
info,
display_inside,
contents,
// Text decorations are not propagated to any out-of-flow descendants
propagated_data.without_text_decorations(),
propagated_data,
),
}
}
@ -912,11 +911,10 @@ impl FloatBox {
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
) -> BoxFragment {
let style = self.contents.style().clone();
positioning_context.layout_maybe_position_relative_fragment(
layout_context,
containing_block,
&style,
&self.contents.base,
|positioning_context| {
self.contents
.layout_float_or_atomic_inline(

View file

@ -0,0 +1,701 @@
/* 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::borrow::Cow;
use std::char::{ToLowercase, ToUppercase};
use icu_segmenter::WordSegmenter;
use itertools::izip;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::values::specified::text::TextTransformCase;
use unicode_bidi::Level;
use super::text_run::TextRun;
use super::{
InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
SharedInlineStyles,
};
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom_traversal::NodeAndStyleInfo;
use crate::flow::float::FloatBox;
use crate::formatting_contexts::IndependentFormattingContext;
use crate::positioned::AbsolutelyPositionedBox;
use crate::style_ext::ComputedValuesExt;
#[derive(Default)]
pub(crate) struct InlineFormattingContextBuilder {
/// A stack of [`SharedInlineStyles`] including one for the root, one for each inline box on the
/// inline box stack, and importantly, one for every `display: contents` element that we are
/// currently processing. Normally `display: contents` elements don't affect the structure of
/// the [`InlineFormattingContext`], but the styles they provide do style their children.
pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
/// The collection of text strings that make up this [`InlineFormattingContext`] under
/// construction.
pub text_segments: Vec<String>,
/// The current offset in the final text string of this [`InlineFormattingContext`],
/// used to properly set the text range of new [`InlineItem::TextRun`]s.
current_text_offset: usize,
/// Whether the last processed node ended with whitespace. This is used to
/// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
///
/// > Any collapsible space immediately following another collapsible space—even one
/// > outside the boundary of the inline containing that space, provided both spaces are
/// > within the same inline formatting context—is collapsed to have zero advance width.
/// > (It is invisible, but retains its soft wrap opportunity, if any.)
last_inline_box_ended_with_collapsible_white_space: bool,
/// Whether or not the current state of the inline formatting context is on a word boundary
/// for the purposes of `text-transform: capitalize`.
on_word_boundary: bool,
/// Whether or not this inline formatting context will contain floats.
pub contains_floats: bool,
/// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under
/// construction. This is stored in a flat list to make it easy to access the last
/// item.
pub inline_items: Vec<ArcRefCell<InlineItem>>,
/// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction.
pub inline_boxes: InlineBoxes,
/// The ongoing stack of inline boxes stack of the builder.
///
/// Contains all the currently ongoing inline boxes we entered so far.
/// The traversal is at all times as deep in the tree as this stack is,
/// which is why the code doesn't need to keep track of the actual
/// container root (see `handle_inline_level_element`).
//_
/// When an inline box ends, it's removed from this stack.
inline_box_stack: Vec<InlineBoxIdentifier>,
/// Normally, an inline box produces a single box tree [`InlineItem`]. When a block
/// element causes an inline box [to be split], it can produce multiple
/// [`InlineItem`]s, all inserted into different [`InlineFormattingContext`]s.
/// [`Self::block_in_inline_splits`] is responsible for tracking all of these split
/// inline box results, so that they can be inserted into the [`crate::dom::BoxSlot`]
/// for the DOM element once it has been processed for BoxTree construction.
///
/// [to be split]: https://www.w3.org/TR/CSS2/visuren.html#anonymous-block-level
block_in_inline_splits: Vec<Vec<ArcRefCell<InlineItem>>>,
/// Whether or not the inline formatting context under construction has any
/// uncollapsible text content.
pub has_uncollapsible_text_content: bool,
}
impl InlineFormattingContextBuilder {
pub(crate) fn new(info: &NodeAndStyleInfo) -> Self {
Self::new_for_shared_styles(vec![info.into()])
}
pub(crate) fn new_for_shared_styles(
shared_inline_styles_stack: Vec<SharedInlineStyles>,
) -> Self {
Self {
// For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary.
on_word_boundary: true,
shared_inline_styles_stack,
..Default::default()
}
}
pub(crate) fn currently_processing_inline_box(&self) -> bool {
!self.inline_box_stack.is_empty()
}
fn push_control_character_string(&mut self, string_to_push: &str) {
self.text_segments.push(string_to_push.to_owned());
self.current_text_offset += string_to_push.len();
}
fn shared_inline_styles(&self) -> SharedInlineStyles {
self.shared_inline_styles_stack
.last()
.expect("Should always have at least one SharedInlineStyles")
.clone()
}
/// Return true if this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring
/// during box tree construction. An IFC is empty if it only contains TextRuns with
/// completely collapsible whitespace. When that happens it can be ignored completely.
pub(crate) fn is_empty(&self) -> bool {
if self.has_uncollapsible_text_content {
return false;
}
if !self.inline_box_stack.is_empty() {
return false;
}
fn inline_level_box_is_empty(inline_level_box: &InlineItem) -> bool {
match inline_level_box {
InlineItem::StartInlineBox(_) => false,
InlineItem::EndInlineBox => false,
// Text content is handled by `self.has_uncollapsible_text` content above in order
// to avoid having to iterate through the character once again.
InlineItem::TextRun(_) => true,
InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => false,
InlineItem::OutOfFlowFloatBox(_) => false,
InlineItem::Atomic(..) => false,
}
}
self.inline_items
.iter()
.all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow()))
}
pub(crate) fn push_atomic(
&mut self,
independent_formatting_context: IndependentFormattingContext,
) -> ArcRefCell<InlineItem> {
let inline_level_box = ArcRefCell::new(InlineItem::Atomic(
ArcRefCell::new(independent_formatting_context),
self.current_text_offset,
Level::ltr(), /* This will be assigned later if necessary. */
));
self.inline_items.push(inline_level_box.clone());
// Push an object replacement character for this atomic, which will ensure that the line breaker
// inserts a line breaking opportunity here.
self.push_control_character_string("\u{fffc}");
self.last_inline_box_ended_with_collapsible_white_space = false;
self.on_word_boundary = true;
inline_level_box
}
pub(crate) fn push_absolutely_positioned_box(
&mut self,
absolutely_positioned_box: AbsolutelyPositionedBox,
) -> ArcRefCell<InlineItem> {
let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box);
let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowAbsolutelyPositionedBox(
absolutely_positioned_box,
self.current_text_offset,
));
self.inline_items.push(inline_level_box.clone());
inline_level_box
}
pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineItem> {
let inline_level_box =
ArcRefCell::new(InlineItem::OutOfFlowFloatBox(ArcRefCell::new(float_box)));
self.inline_items.push(inline_level_box.clone());
self.contains_floats = true;
inline_level_box
}
pub(crate) fn start_inline_box(
&mut self,
inline_box: InlineBox,
block_in_inline_splits: Option<Vec<ArcRefCell<InlineItem>>>,
) {
self.push_control_character_string(inline_box.base.style.bidi_control_chars().0);
// Don't push a `SharedInlineStyles` if we are pushing this box when splitting
// an IFC for a block-in-inline split. Shared styles are pushed as part of setting
// up the second split of the IFC.
if inline_box.is_first_split {
self.shared_inline_styles_stack
.push(inline_box.shared_inline_styles.clone());
}
let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box);
let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box));
self.inline_items.push(inline_level_box.clone());
self.inline_box_stack.push(identifier);
let mut block_in_inline_splits = block_in_inline_splits.unwrap_or_default();
block_in_inline_splits.push(inline_level_box);
self.block_in_inline_splits.push(block_in_inline_splits);
}
/// End the ongoing inline box in this [`InlineFormattingContextBuilder`], returning
/// shared references to all of the box tree items that were created for it. More than
/// a single box tree items may be produced for a single inline box when that inline
/// box is split around a block-level element.
pub(crate) fn end_inline_box(&mut self) -> Vec<ArcRefCell<InlineItem>> {
self.shared_inline_styles_stack.pop();
let (identifier, block_in_inline_splits) = self.end_inline_box_internal();
let inline_level_box = self.inline_boxes.get(&identifier);
{
let mut inline_level_box = inline_level_box.borrow_mut();
inline_level_box.is_last_split = true;
self.push_control_character_string(inline_level_box.base.style.bidi_control_chars().1);
}
block_in_inline_splits.unwrap_or_default()
}
fn end_inline_box_internal(
&mut self,
) -> (InlineBoxIdentifier, Option<Vec<ArcRefCell<InlineItem>>>) {
let identifier = self
.inline_box_stack
.pop()
.expect("Ended non-existent inline box");
self.inline_items
.push(ArcRefCell::new(InlineItem::EndInlineBox));
self.inline_boxes.end_inline_box(identifier);
// This might be `None` if this builder has already drained its block-in-inline-splits
// into the new builder on the other side of a new block-in-inline split.
let block_in_inline_splits = self.block_in_inline_splits.pop();
(identifier, block_in_inline_splits)
}
pub(crate) fn push_text<'dom>(&mut self, text: Cow<'dom, str>, info: &NodeAndStyleInfo<'dom>) {
let white_space_collapse = info.style.clone_white_space_collapse();
let collapsed = WhitespaceCollapse::new(
text.chars(),
white_space_collapse,
self.last_inline_box_ended_with_collapsible_white_space,
);
// TODO: Not all text transforms are about case, this logic should stop ignoring
// TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA.
let text_transform = info.style.clone_text_transform().case();
let capitalized_text: String;
let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
TextTransformCase::None => Box::new(collapsed),
TextTransformCase::Capitalize => {
// `TextTransformation` doesn't support capitalization, so we must capitalize the whole
// string at once and make a copy. Here `on_word_boundary` indicates whether or not the
// inline formatting context as a whole is on a word boundary. This is different from
// `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are
// between atomic inlines and at the start of the IFC, and because preserved spaces
// are a word boundary.
let collapsed_string: String = collapsed.collect();
capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
Box::new(capitalized_text.chars())
},
_ => {
// If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in
// a `TextTransformation` iterator.
Box::new(TextTransformation::new(collapsed, text_transform))
},
};
let white_space_collapse = info.style.clone_white_space_collapse();
let new_text: String = char_iterator
.inspect(|&character| {
self.has_uncollapsible_text_content |= matches!(
white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) || !character.is_ascii_whitespace() ||
(character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse);
})
.collect();
if new_text.is_empty() {
return;
}
let selection_range = info.get_selection_range();
if let Some(last_character) = new_text.chars().next_back() {
self.on_word_boundary = last_character.is_whitespace();
self.last_inline_box_ended_with_collapsible_white_space =
self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
}
let new_range = self.current_text_offset..self.current_text_offset + new_text.len();
self.current_text_offset = new_range.end;
self.text_segments.push(new_text);
if let Some(inline_item) = self.inline_items.last() {
if let InlineItem::TextRun(text_run) = &mut *inline_item.borrow_mut() {
text_run.borrow_mut().text_range.end = new_range.end;
return;
}
}
self.inline_items
.push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new(
TextRun::new(
info.into(),
self.shared_inline_styles(),
new_range,
selection_range,
),
))));
}
pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
self.shared_inline_styles_stack.push(shared_inline_styles);
}
pub(crate) fn leave_display_contents(&mut self) {
self.shared_inline_styles_stack.pop();
}
pub(crate) fn split_around_block_and_finish(
&mut self,
layout_context: &LayoutContext,
has_first_formatted_line: bool,
default_bidi_level: Level,
) -> Option<InlineFormattingContext> {
if self.is_empty() {
return None;
}
// Create a new inline builder which will be active after the block splits this inline formatting
// context. It has the same inline box structure as this builder, except the boxes are
// marked as not being the first fragment. No inline content is carried over to this new
// builder.
let mut new_builder = Self::new_for_shared_styles(self.shared_inline_styles_stack.clone());
let block_in_inline_splits = std::mem::take(&mut self.block_in_inline_splits);
for (identifier, historical_inline_boxes) in
izip!(self.inline_box_stack.iter(), block_in_inline_splits)
{
// Start a new inline box for every ongoing inline box in this
// InlineFormattingContext once we are done processing this block element,
// being sure to give the block-in-inline-split to the new
// InlineFormattingContext. These will finally be inserted into the DOM's
// BoxSlot once the inline box has been fully processed.
new_builder.start_inline_box(
self.inline_boxes
.get(identifier)
.borrow()
.split_around_block(),
Some(historical_inline_boxes),
);
}
let mut inline_builder_from_before_split = std::mem::replace(self, new_builder);
// End all ongoing inline boxes in the first builder, but ensure that they are not
// marked as the final fragments, so that they do not get inline end margin, borders,
// and padding.
while !inline_builder_from_before_split.inline_box_stack.is_empty() {
inline_builder_from_before_split.end_inline_box_internal();
}
inline_builder_from_before_split.finish(
layout_context,
has_first_formatted_line,
/* is_single_line_text_input = */ false,
default_bidi_level,
)
}
/// Finish the current inline formatting context, returning [`None`] if the context was empty.
pub(crate) fn finish(
self,
layout_context: &LayoutContext,
has_first_formatted_line: bool,
is_single_line_text_input: bool,
default_bidi_level: Level,
) -> Option<InlineFormattingContext> {
if self.is_empty() {
return None;
}
assert!(self.inline_box_stack.is_empty());
Some(InlineFormattingContext::new_with_builder(
self,
layout_context,
has_first_formatted_line,
is_single_line_text_input,
default_bidi_level,
))
}
}
fn preserve_segment_break() -> bool {
true
}
pub struct WhitespaceCollapse<InputIterator> {
char_iterator: InputIterator,
white_space_collapse: WhiteSpaceCollapse,
/// Whether or not we should collapse white space completely at the start of the string.
/// This is true when the last character handled in our owning [`super::InlineFormattingContext`]
/// was collapsible white space.
remove_collapsible_white_space_at_start: bool,
/// Whether or not the last character produced was newline. There is special behavior
/// we do after each newline.
following_newline: bool,
/// Whether or not we have seen any non-white space characters, indicating that we are not
/// in a collapsible white space section at the beginning of the string.
have_seen_non_white_space_characters: bool,
/// Whether the last character that we processed was a non-newline white space character. When
/// collapsing white space we need to wait until the next non-white space character or the end
/// of the string to push a single white space.
inside_white_space: bool,
/// When we enter a collapsible white space region, we may need to wait to produce a single
/// white space character as soon as we encounter a non-white space character. When that
/// happens we queue up the non-white space character for the next iterator call.
character_pending_to_return: Option<char>,
}
impl<InputIterator> WhitespaceCollapse<InputIterator> {
pub fn new(
char_iterator: InputIterator,
white_space_collapse: WhiteSpaceCollapse,
trim_beginning_white_space: bool,
) -> Self {
Self {
char_iterator,
white_space_collapse,
remove_collapsible_white_space_at_start: trim_beginning_white_space,
inside_white_space: false,
following_newline: false,
have_seen_non_white_space_characters: false,
character_pending_to_return: None,
}
}
fn is_leading_trimmed_white_space(&self) -> bool {
!self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
}
/// Whether or not we need to produce a space character if the next character is not a newline
/// and not white space. This happens when we are exiting a section of white space and we
/// waited to produce a single space character for the entire section of white space (but
/// not following or preceding a newline).
fn need_to_produce_space_character_after_white_space(&self) -> bool {
self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
}
}
impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
where
InputIterator: Iterator<Item = char>,
{
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
// Point 4.1.1 first bullet:
// > If white-space is set to normal, nowrap, or pre-line, whitespace
// > characters are considered collapsible
// If whitespace is not considered collapsible, it is preserved entirely, which
// means that we can simply return the input string exactly.
if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
{
// From <https://drafts.csswg.org/css-text-3/#white-space-processing>:
// > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects.
//
// In the non-preserved case these are converted to space below.
return match self.char_iterator.next() {
Some('\r') => Some(' '),
next => next,
};
}
if let Some(character) = self.character_pending_to_return.take() {
self.inside_white_space = false;
self.have_seen_non_white_space_characters = true;
self.following_newline = false;
return Some(character);
}
while let Some(character) = self.char_iterator.next() {
// Don't push non-newline whitespace immediately. Instead wait to push it until we
// know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
// above.
if character.is_ascii_whitespace() && character != '\n' {
self.inside_white_space = true;
continue;
}
// Point 4.1.1:
// > 2. Collapsible segment breaks are transformed for rendering according to the
// > segment break transformation rules.
if character == '\n' {
// From <https://drafts.csswg.org/css-text-3/#line-break-transform>
// (4.1.3 -- the segment break transformation rules):
//
// > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
// > collapsible and are instead transformed into a preserved line feed"
if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
self.inside_white_space = false;
self.following_newline = true;
return Some(character);
// Point 4.1.3:
// > 1. First, any collapsible segment break immediately following another
// > collapsible segment break is removed.
// > 2. Then any remaining segment break is either transformed into a space (U+0020)
// > or removed depending on the context before and after the break.
} else if !self.following_newline &&
preserve_segment_break() &&
!self.is_leading_trimmed_white_space()
{
self.inside_white_space = false;
self.following_newline = true;
return Some(' ');
} else {
self.following_newline = true;
continue;
}
}
// Point 4.1.1:
// > 2. Any sequence of collapsible spaces and tabs immediately preceding or
// > following a segment break is removed.
// > 3. Every collapsible tab is converted to a collapsible space (U+0020).
// > 4. Any collapsible space immediately following another collapsible space—even
// > one outside the boundary of the inline containing that space, provided both
// > spaces are within the same inline formatting context—is collapsed to have zero
// > advance width.
if self.need_to_produce_space_character_after_white_space() {
self.inside_white_space = false;
self.character_pending_to_return = Some(character);
return Some(' ');
}
self.inside_white_space = false;
self.have_seen_non_white_space_characters = true;
self.following_newline = false;
return Some(character);
}
if self.need_to_produce_space_character_after_white_space() {
self.inside_white_space = false;
return Some(' ');
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.char_iterator.size_hint()
}
fn count(self) -> usize
where
Self: Sized,
{
self.char_iterator.count()
}
}
enum PendingCaseConversionResult {
Uppercase(ToUppercase),
Lowercase(ToLowercase),
}
impl PendingCaseConversionResult {
fn next(&mut self) -> Option<char> {
match self {
PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
}
}
}
/// This is an interator that consumes a char iterator and produces character transformed
/// by the given CSS `text-transform` value. It currently does not support
/// `text-transform: capitalize` because Unicode segmentation libraries do not support
/// streaming input one character at a time.
pub struct TextTransformation<InputIterator> {
/// The input character iterator.
char_iterator: InputIterator,
/// The `text-transform` value to use.
text_transform: TextTransformCase,
/// If an uppercasing or lowercasing produces more than one character, this
/// caches them so that they can be returned in subsequent iterator calls.
pending_case_conversion_result: Option<PendingCaseConversionResult>,
}
impl<InputIterator> TextTransformation<InputIterator> {
pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
Self {
char_iterator,
text_transform,
pending_case_conversion_result: None,
}
}
}
impl<InputIterator> Iterator for TextTransformation<InputIterator>
where
InputIterator: Iterator<Item = char>,
{
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
if let Some(character) = self
.pending_case_conversion_result
.as_mut()
.and_then(|result| result.next())
{
return Some(character);
}
self.pending_case_conversion_result = None;
for character in self.char_iterator.by_ref() {
match self.text_transform {
TextTransformCase::None => return Some(character),
TextTransformCase::Uppercase => {
let mut pending_result =
PendingCaseConversionResult::Uppercase(character.to_uppercase());
if let Some(character) = pending_result.next() {
self.pending_case_conversion_result = Some(pending_result);
return Some(character);
}
},
TextTransformCase::Lowercase => {
let mut pending_result =
PendingCaseConversionResult::Lowercase(character.to_lowercase());
if let Some(character) = pending_result.next() {
self.pending_case_conversion_result = Some(pending_result);
return Some(character);
}
},
// `text-transform: capitalize` currently cannot work on a per-character basis,
// so must be handled outside of this iterator.
TextTransformCase::Capitalize => return Some(character),
}
}
None
}
}
/// Given a string and whether the start of the string represents a word boundary, create a copy of
/// the string with letters after word boundaries capitalized.
fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
let mut output_string = String::new();
output_string.reserve(string.len());
let word_segmenter = WordSegmenter::new_auto();
let mut bounds = word_segmenter.segment_str(string).peekable();
let mut byte_index = 0;
for character in string.chars() {
let current_byte_index = byte_index;
byte_index += character.len_utf8();
if let Some(next_index) = bounds.peek() {
if *next_index == current_byte_index {
bounds.next();
if current_byte_index != 0 || allow_word_at_start {
output_string.extend(character.to_uppercase());
continue;
}
}
}
output_string.push(character);
}
output_string
}

View file

@ -6,60 +6,83 @@ use std::vec::IntoIter;
use app_units::Au;
use fonts::FontMetrics;
use servo_arc::Arc;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode};
use servo_arc::Arc as ServoArc;
use style::properties::ComputedValues;
use super::{InlineContainerState, InlineContainerStateFlags, inline_container_needs_strut};
use super::{
InlineContainerState, InlineContainerStateFlags, SharedInlineStyles,
inline_container_needs_strut,
};
use crate::ContainingBlock;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::NodeExt;
use crate::dom_traversal::NodeAndStyleInfo;
use crate::fragment_tree::BaseFragmentInfo;
use crate::layout_box_base::LayoutBoxBase;
use crate::style_ext::{LayoutStyle, PaddingBorderMargin};
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) struct InlineBox {
pub base_fragment_info: BaseFragmentInfo,
pub style: Arc<ComputedValues>,
pub base: LayoutBoxBase,
/// The [`SharedInlineStyles`] for this [`InlineBox`] that are used to share styles
/// with all [`super::TextRun`] children.
pub(super) shared_inline_styles: SharedInlineStyles,
/// The identifier of this inline box in the containing [`super::InlineFormattingContext`].
pub(super) identifier: InlineBoxIdentifier,
pub is_first_fragment: bool,
pub is_last_fragment: bool,
/// Whether or not this is the first instance of an [`InlineBox`] before a possible
/// block-in-inline split. When no split occurs, this is always true.
pub is_first_split: bool,
/// Whether or not this is the last instance of an [`InlineBox`] before a possible
/// block-in-inline split. When no split occurs, this is always true.
pub is_last_split: bool,
/// The index of the default font in the [`super::InlineFormattingContext`]'s font metrics store.
/// This is initialized during IFC shaping.
pub default_font_index: Option<usize>,
}
impl InlineBox {
pub(crate) fn new<'dom, Node: NodeExt<'dom>>(info: &NodeAndStyleInfo<Node>) -> Self {
pub(crate) fn new(info: &NodeAndStyleInfo) -> Self {
Self {
base_fragment_info: info.into(),
style: info.style.clone(),
base: LayoutBoxBase::new(info.into(), info.style.clone()),
shared_inline_styles: info.into(),
// This will be assigned later, when the box is actually added to the IFC.
identifier: InlineBoxIdentifier::default(),
is_first_fragment: true,
is_last_fragment: false,
is_first_split: true,
is_last_split: false,
default_font_index: None,
}
}
pub(crate) fn split_around_block(&self) -> Self {
Self {
style: self.style.clone(),
is_first_fragment: false,
is_last_fragment: false,
base: LayoutBoxBase::new(self.base.base_fragment_info, self.base.style.clone()),
shared_inline_styles: self.shared_inline_styles.clone(),
is_first_split: false,
is_last_split: false,
..*self
}
}
#[inline]
pub(crate) fn layout_style(&self) -> LayoutStyle {
LayoutStyle::Default(&self.style)
LayoutStyle::Default(&self.base.style)
}
pub(crate) fn repair_style(
&mut self,
node: &ServoLayoutNode,
new_style: &ServoArc<ComputedValues>,
) {
self.base.repair_style(new_style);
*self.shared_inline_styles.style.borrow_mut() = new_style.clone();
*self.shared_inline_styles.selected.borrow_mut() = node.to_threadsafe().selected_style();
}
}
#[derive(Debug, Default)]
#[derive(Debug, Default, MallocSizeOf)]
pub(crate) struct InlineBoxes {
/// A collection of all inline boxes in a particular [`super::InlineFormattingContext`].
inline_boxes: Vec<ArcRefCell<InlineBox>>,
@ -76,6 +99,10 @@ impl InlineBoxes {
self.inline_boxes.len()
}
pub(super) fn iter(&self) -> impl Iterator<Item = &ArcRefCell<InlineBox>> {
self.inline_boxes.iter()
}
pub(super) fn get(&self, identifier: &InlineBoxIdentifier) -> ArcRefCell<InlineBox> {
self.inline_boxes[identifier.index_in_inline_boxes as usize].clone()
}
@ -161,7 +188,7 @@ impl InlineBoxes {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
pub(super) enum InlineBoxTreePathToken {
Start(InlineBoxIdentifier),
End(InlineBoxIdentifier),
@ -182,7 +209,7 @@ impl InlineBoxTreePathToken {
/// [`u32`] is used for the index, in order to save space. The value refers to the token
/// in the start tree data structure which can be fetched to find the actual index of
/// of the [`InlineBox`] in [`InlineBoxes::inline_boxes`].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)]
pub(crate) struct InlineBoxIdentifier {
pub index_of_start_in_tree: u32,
pub index_in_inline_boxes: u32,
@ -218,7 +245,7 @@ impl InlineBoxContainerState {
is_last_fragment: bool,
font_metrics: Option<&FontMetrics>,
) -> Self {
let style = inline_box.style.clone();
let style = inline_box.base.style.clone();
let pbm = inline_box
.layout_style()
.padding_border_margin(containing_block);
@ -229,15 +256,9 @@ impl InlineBoxContainerState {
}
Self {
base: InlineContainerState::new(
style,
flags,
Some(parent_container),
parent_container.text_decoration_line,
font_metrics,
),
base: InlineContainerState::new(style, flags, Some(parent_container), font_metrics),
identifier: inline_box.identifier,
base_fragment_info: inline_box.base_fragment_info,
base_fragment_info: inline_box.base.base_fragment_info,
pbm,
is_last_fragment,
}

View file

@ -4,9 +4,9 @@
use app_units::Au;
use bitflags::bitflags;
use fonts::{FontMetrics, GlyphStore};
use fonts::{ByteIndex, FontMetrics, GlyphStore};
use itertools::Either;
use servo_arc::Arc;
use range::Range;
use style::Zero;
use style::computed_values::position::T as Position;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
@ -15,16 +15,13 @@ use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword};
use style::values::generics::font::LineHeight;
use style::values::specified::align::AlignFlags;
use style::values::specified::box_::DisplayOutside;
use style::values::specified::text::TextDecorationLine;
use unicode_bidi::{BidiInfo, Level};
use webrender_api::FontInstanceKey;
use super::inline_box::{InlineBoxContainerState, InlineBoxIdentifier, InlineBoxTreePathToken};
use super::{InlineFormattingContextLayout, LineBlockSizes};
use super::{InlineFormattingContextLayout, LineBlockSizes, SharedInlineStyles};
use crate::cell::ArcRefCell;
use crate::fragment_tree::{
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, Fragment, TextFragment,
};
use crate::fragment_tree::{BaseFragmentInfo, BoxFragment, Fragment, TextFragment};
use crate::geom::{LogicalRect, LogicalVec2, PhysicalRect, ToLogical};
use crate::positioned::{
AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement,
@ -327,13 +324,12 @@ impl LineItemLayout<'_, '_> {
let inline_box = self.layout.ifc.inline_boxes.get(identifier);
let inline_box = &*(inline_box.borrow());
let style = &inline_box.style;
let space_above_baseline = inline_box_state.calculate_space_above_baseline();
let block_start_offset =
self.calculate_inline_box_block_start(inline_box_state, space_above_baseline);
let positioning_context_or_start_offset_in_parent =
match PositioningContext::new_for_style(style) {
match PositioningContext::new_for_layout_box_base(&inline_box.base) {
Some(positioning_context) => Either::Left(positioning_context),
None => Either::Right(self.current_positioning_context_mut().len()),
};
@ -379,7 +375,7 @@ impl LineItemLayout<'_, '_> {
let containing_block_writing_mode = self.layout.containing_block.style.writing_mode;
if containing_block_writing_mode.is_bidi_ltr() !=
inline_box.style.writing_mode.is_bidi_ltr()
inline_box.base.style.writing_mode.is_bidi_ltr()
{
std::mem::swap(&mut had_start, &mut had_end)
}
@ -419,7 +415,7 @@ impl LineItemLayout<'_, '_> {
// Relative adjustment should not affect the rest of line layout, so we can
// do it right before creating the Fragment.
let style = &inline_box.style;
let style = &inline_box.base.style;
if style.get_box().position == Position::Relative {
content_rect.start_corner += relative_adjustement(style, self.layout.containing_block);
}
@ -456,7 +452,7 @@ impl LineItemLayout<'_, '_> {
// but they need to be made relative to this fragment.
let physical_content_rect = content_rect.as_physical(Some(self.layout.containing_block));
let mut fragment = BoxFragment::new(
inline_box.base_fragment_info,
inline_box.base.base_fragment_info,
style.clone(),
fragments,
physical_content_rect,
@ -464,7 +460,6 @@ impl LineItemLayout<'_, '_> {
border.to_physical(ifc_writing_mode),
margin.to_physical(ifc_writing_mode),
None, /* clearance */
CollapsedBlockMargins::zero(),
);
let offset_from_parent_ifc = LogicalVec2 {
@ -494,9 +489,11 @@ impl LineItemLayout<'_, '_> {
}
self.current_state.inline_advance += inner_state.inline_advance + pbm_sums.inline_sum();
self.current_state
.fragments
.push((Fragment::Box(ArcRefCell::new(fragment)), content_rect));
let fragment = Fragment::Box(ArcRefCell::new(fragment));
inline_box.base.add_fragment(fragment.clone());
self.current_state.fragments.push((fragment, content_rect));
}
fn calculate_inline_box_block_start(
@ -569,13 +566,13 @@ impl LineItemLayout<'_, '_> {
self.current_state.fragments.push((
Fragment::Text(ArcRefCell::new(TextFragment {
base: text_item.base_fragment_info.into(),
parent_style: text_item.parent_style,
inline_styles: text_item.inline_styles.clone(),
rect: PhysicalRect::zero(),
font_metrics: text_item.font_metrics,
font_key: text_item.font_key,
glyphs: text_item.text,
text_decoration_line: text_item.text_decoration_line,
justification_adjustment: self.justification_adjustment,
selection_range: text_item.selection_range,
})),
content_rect,
));
@ -587,32 +584,34 @@ impl LineItemLayout<'_, '_> {
// This needs to be added to the calculated block and inline positions.
// Make the final result relative to the parent box.
let ifc_writing_mode = self.layout.containing_block.style.writing_mode;
let padding_border_margin_sides = atomic
.fragment
.padding_border_margin()
.to_logical(ifc_writing_mode);
let content_rect = {
let block_start = atomic.calculate_block_start(&self.line_metrics);
let atomic_fragment = atomic.fragment.borrow_mut();
let padding_border_margin_sides = atomic_fragment
.padding_border_margin()
.to_logical(ifc_writing_mode);
let mut atomic_offset = LogicalVec2 {
inline: self.current_state.inline_advance + padding_border_margin_sides.inline_start,
block: atomic.calculate_block_start(&self.line_metrics) -
self.current_state.parent_offset.block +
padding_border_margin_sides.block_start,
};
let mut atomic_offset = LogicalVec2 {
inline: self.current_state.inline_advance +
padding_border_margin_sides.inline_start,
block: block_start - self.current_state.parent_offset.block +
padding_border_margin_sides.block_start,
};
if atomic.fragment.style.get_box().position == Position::Relative {
atomic_offset +=
relative_adjustement(&atomic.fragment.style, self.layout.containing_block);
}
if atomic_fragment.style.get_box().position == Position::Relative {
atomic_offset +=
relative_adjustement(&atomic_fragment.style, self.layout.containing_block);
}
// Reconstruct a logical rectangle relative to the inline box container that will be used
// after the inline box is procesed to find a final physical rectangle.
let content_rect = LogicalRect {
start_corner: atomic_offset,
size: atomic
.fragment
.content_rect
.size
.to_logical(ifc_writing_mode),
// Reconstruct a logical rectangle relative to the inline box container that will be used
// after the inline box is procesed to find a final physical rectangle.
LogicalRect {
start_corner: atomic_offset,
size: atomic_fragment
.content_rect
.size
.to_logical(ifc_writing_mode),
}
};
if let Some(mut positioning_context) = atomic.positioning_context {
@ -628,10 +627,10 @@ impl LineItemLayout<'_, '_> {
}
self.current_state.inline_advance += atomic.size.inline;
self.current_state.fragments.push((
Fragment::Box(ArcRefCell::new(atomic.fragment)),
content_rect,
));
self.current_state
.fragments
.push((Fragment::Box(atomic.fragment), content_rect));
}
fn layout_absolute(&mut self, absolute: AbsolutelyPositionedLineItem) {
@ -691,7 +690,7 @@ impl LineItemLayout<'_, '_> {
));
}
fn layout_float(&mut self, mut float: FloatLineItem) {
fn layout_float(&mut self, float: FloatLineItem) {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_ANY_FLOATS);
@ -705,13 +704,12 @@ impl LineItemLayout<'_, '_> {
inline: self.current_state.parent_offset.inline,
block: self.line_metrics.block_offset + self.current_state.parent_offset.block,
};
float.fragment.content_rect.origin -= distance_from_parent_to_ifc
float.fragment.borrow_mut().content_rect.origin -= distance_from_parent_to_ifc
.to_physical_size(self.layout.containing_block.style.writing_mode);
self.current_state.fragments.push((
Fragment::Float(ArcRefCell::new(float.fragment)),
LogicalRect::zero(),
));
self.current_state
.fragments
.push((Fragment::Float(float.fragment), LogicalRect::zero()));
}
}
@ -761,19 +759,23 @@ impl LineItem {
pub(super) struct TextRunLineItem {
pub base_fragment_info: BaseFragmentInfo,
pub parent_style: Arc<ComputedValues>,
pub inline_styles: SharedInlineStyles,
pub text: Vec<std::sync::Arc<GlyphStore>>,
pub font_metrics: FontMetrics,
pub font_key: FontInstanceKey,
pub text_decoration_line: TextDecorationLine,
/// The BiDi level of this [`TextRunLineItem`] to enable reordering.
pub bidi_level: Level,
pub selection_range: Option<Range<ByteIndex>>,
}
impl TextRunLineItem {
fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
self.inline_styles
.style
.borrow()
.get_inherited_text()
.white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
@ -799,7 +801,11 @@ impl TextRunLineItem {
fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
self.inline_styles
.style
.borrow()
.get_inherited_text()
.white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
@ -827,7 +833,7 @@ impl TextRunLineItem {
}
pub(super) struct AtomicLineItem {
pub fragment: BoxFragment,
pub fragment: ArcRefCell<BoxFragment>,
pub size: LogicalVec2<Au>,
pub positioning_context: Option<PositioningContext>,
@ -847,7 +853,7 @@ impl AtomicLineItem {
/// Given the metrics for a line, our vertical alignment, and our block size, find a block start
/// position relative to the top of the line.
fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Au {
match self.fragment.style.clone_vertical_align() {
match self.fragment.borrow().style.clone_vertical_align() {
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Au::zero(),
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => {
line_metrics.block_size - self.size.block
@ -867,7 +873,7 @@ pub(super) struct AbsolutelyPositionedLineItem {
}
pub(super) struct FloatLineItem {
pub fragment: BoxFragment,
pub fragment: ArcRefCell<BoxFragment>,
/// Whether or not this float Fragment has been placed yet. Fragments that
/// do not fit on a line need to be placed after the hypothetical block start
/// of the next line.

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ use fonts::{
};
use fonts_traits::ByteIndex;
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use range::Range as ServoRange;
use servo_arc::Arc;
use style::computed_values::text_rendering::T as TextRendering;
@ -25,7 +26,7 @@ use unicode_script::Script;
use xi_unicode::linebreak_property;
use super::line_breaker::LineBreaker;
use super::{FontKeyAndMetrics, InlineFormattingContextLayout};
use super::{FontKeyAndMetrics, InlineFormattingContextLayout, SharedInlineStyles};
use crate::fragment_tree::BaseFragmentInfo;
// These constants are the xi-unicode line breaking classes that are defined in
@ -36,18 +37,6 @@ pub(crate) const XI_LINE_BREAKING_CLASS_ZW: u8 = 28;
pub(crate) const XI_LINE_BREAKING_CLASS_WJ: u8 = 30;
pub(crate) const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 42;
/// <https://www.w3.org/TR/css-display-3/#css-text-run>
#[derive(Debug)]
pub(crate) struct TextRun {
pub base_fragment_info: BaseFragmentInfo,
pub parent_style: Arc<ComputedValues>,
pub text_range: Range<usize>,
/// The text of this [`TextRun`] with a font selected, broken into unbreakable
/// segments, and shaped.
pub shaped_text: Vec<TextRunSegment>,
}
// There are two reasons why we might want to break at the start:
//
// 1. The line breaker told us that a break was necessary between two separate
@ -62,7 +51,7 @@ enum SegmentStartSoftWrapPolicy {
FollowLinebreaker,
}
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) struct TextRunSegment {
/// The index of this font in the parent [`super::InlineFormattingContext`]'s collection of font
/// information.
@ -140,6 +129,7 @@ impl TextRunSegment {
soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
}
let mut byte_processed = ByteIndex(0);
for (run_index, run) in self.runs.iter().enumerate() {
ifc.possibly_flush_deferred_forced_line_break();
@ -147,6 +137,7 @@ impl TextRunSegment {
// see any content. We don't line break immediately, because we'd like to finish processing
// any ongoing inline boxes before ending the line.
if run.is_single_preserved_newline() {
byte_processed = byte_processed + run.range.length();
ifc.defer_forced_line_break();
continue;
}
@ -160,7 +151,12 @@ impl TextRunSegment {
text_run,
self.font_index,
self.bidi_level,
ServoRange::<ByteIndex>::new(
byte_processed + ByteIndex(self.range.start as isize),
ByteIndex(self.range.len() as isize) - byte_processed,
),
);
byte_processed = byte_processed + run.range.length();
}
}
@ -322,17 +318,49 @@ impl TextRunSegment {
}
}
/// A single [`TextRun`] for the box tree. These are all descendants of
/// [`super::InlineBox`] or the root of the [`super::InlineFormattingContext`]. During
/// box tree construction, text is split into [`TextRun`]s based on their font, script,
/// etc. When these are created text is already shaped.
///
/// <https://www.w3.org/TR/css-display-3/#css-text-run>
#[derive(Debug, MallocSizeOf)]
pub(crate) struct TextRun {
/// The [`BaseFragmentInfo`] for this [`TextRun`]. Usually this comes from the
/// original text node in the DOM for the text.
pub base_fragment_info: BaseFragmentInfo,
/// The [`crate::SharedStyle`] from this [`TextRun`]s parent element. This is
/// shared so that incremental layout can simply update the parent element and
/// this [`TextRun`] will be updated automatically.
pub inline_styles: SharedInlineStyles,
/// The range of text in [`super::InlineFormattingContext::text_content`] of the
/// [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are UTF-8 offsets.
pub text_range: Range<usize>,
/// The text of this [`TextRun`] with a font selected, broken into unbreakable
/// segments, and shaped.
pub shaped_text: Vec<TextRunSegment>,
/// The selection range for the DOM text node that originated this [`TextRun`]. This
/// comes directly from the DOM.
pub selection_range: Option<ServoRange<ByteIndex>>,
}
impl TextRun {
pub(crate) fn new(
base_fragment_info: BaseFragmentInfo,
parent_style: Arc<ComputedValues>,
inline_styles: SharedInlineStyles,
text_range: Range<usize>,
selection_range: Option<ServoRange<ByteIndex>>,
) -> Self {
Self {
base_fragment_info,
parent_style,
inline_styles,
text_range,
shaped_text: Vec::new(),
selection_range,
}
}
@ -344,11 +372,12 @@ impl TextRun {
font_cache: &mut Vec<FontKeyAndMetrics>,
bidi_info: &BidiInfo,
) {
let inherited_text_style = self.parent_style.get_inherited_text().clone();
let parent_style = self.inline_styles.style.borrow().clone();
let inherited_text_style = parent_style.get_inherited_text().clone();
let letter_spacing = inherited_text_style
.letter_spacing
.0
.resolve(self.parent_style.clone_font().font_size.computed_size());
.resolve(parent_style.clone_font().font_size.computed_size());
let letter_spacing = if letter_spacing.px() != 0. {
Some(app_units::Au::from(letter_spacing))
} else {
@ -368,7 +397,13 @@ impl TextRun {
let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into());
let segments = self
.segment_text_by_font(formatting_context_text, font_context, font_cache, bidi_info)
.segment_text_by_font(
formatting_context_text,
font_context,
font_cache,
bidi_info,
&parent_style,
)
.into_iter()
.map(|(mut segment, font)| {
let word_spacing = style_word_spacing.unwrap_or_else(|| {
@ -391,7 +426,7 @@ impl TextRun {
};
segment.shape_text(
&self.parent_style,
&parent_style,
formatting_context_text,
linebreaker,
&shaping_options,
@ -414,8 +449,9 @@ impl TextRun {
font_context: &FontContext,
font_cache: &mut Vec<FontKeyAndMetrics>,
bidi_info: &BidiInfo,
parent_style: &Arc<ComputedValues>,
) -> Vec<(TextRunSegment, FontRef)> {
let font_group = font_context.font_group(self.parent_style.clone_font());
let font_group = font_context.font_group(parent_style.clone_font());
let mut current: Option<(TextRunSegment, FontRef)> = None;
let mut results = Vec::new();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,402 @@
/* 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 app_units::Au;
use atomic_refcell::AtomicRef;
use compositing_traits::display_list::AxesScrollSensitivity;
use euclid::Rect;
use euclid::default::Size2D as UntypedSize2D;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{
LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use script_layout_interface::{LayoutElementType, LayoutNodeType};
use servo_arc::Arc;
use style::dom::{NodeInfo, TNode};
use style::properties::ComputedValues;
use style::values::computed::Overflow;
use style_traits::CSSPixel;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::{LayoutBox, NodeExt};
use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, iter_child_nodes};
use crate::flexbox::FlexLevelBox;
use crate::flow::float::FloatBox;
use crate::flow::inline::InlineItem;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentTree;
use crate::geom::{LogicalVec2, PhysicalSize};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContents;
use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
use crate::taffy::{TaffyItemBox, TaffyItemBoxInner};
use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
#[derive(MallocSizeOf)]
pub struct BoxTree {
/// Contains typically exactly one block-level box, which was generated by the root element.
/// There may be zero if that element has `display: none`.
root: BlockFormattingContext,
/// Whether or not the viewport should be sensitive to scrolling input events in two axes
viewport_scroll_sensitivity: AxesScrollSensitivity,
}
impl BoxTree {
pub fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
let boxes = construct_for_root_element(context, root_element);
// Zero box for `:root { display: none }`, one for the root element otherwise.
assert!(boxes.len() <= 1);
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
// > UAs must apply the overflow-* values set on the root element to the viewport when the
// > root elements display value is not none. However, when the root element is an [HTML]
// > html element (including XML syntax for HTML) whose overflow value is visible (in both
// > axes), and that element has as a child a body element whose display value is also not
// > none, user agents must instead apply the overflow-* values of the first such child
// > element to the viewport. The element from which the value is propagated must then have a
// > used overflow value of visible.
let root_style = root_element.style(context.shared_context());
let mut viewport_overflow_x = root_style.clone_overflow_x();
let mut viewport_overflow_y = root_style.clone_overflow_y();
if viewport_overflow_x == Overflow::Visible &&
viewport_overflow_y == Overflow::Visible &&
!root_style.get_box().display.is_none()
{
for child in iter_child_nodes(root_element) {
if !child
.to_threadsafe()
.as_element()
.is_some_and(|element| element.is_body_element_of_html_element_root())
{
continue;
}
let style = child.style(context.shared_context());
if !style.get_box().display.is_none() {
viewport_overflow_x = style.clone_overflow_x();
viewport_overflow_y = style.clone_overflow_y();
break;
}
}
}
let contents = BlockContainer::BlockLevelBoxes(boxes);
let contains_floats = contents.contains_floats();
Self {
root: BlockFormattingContext {
contents,
contains_floats,
},
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
// > If visible is applied to the viewport, it must be interpreted as auto.
// > If clip is applied to the viewport, it must be interpreted as hidden.
viewport_scroll_sensitivity: AxesScrollSensitivity {
x: viewport_overflow_x.to_scrollable().into(),
y: viewport_overflow_y.to_scrollable().into(),
},
}
}
/// This method attempts to incrementally update the box tree from an
/// arbitrary node that is not necessarily the document's root element.
///
/// If the node is not a valid candidate for incremental update, the method
/// loops over its parent. The only valid candidates for now are absolutely
/// positioned boxes which don't change their outside display mode (i.e. it
/// will not attempt to update from an absolutely positioned inline element
/// which became an absolutely positioned block element). The value `true`
/// is returned if an incremental update could be done, and `false`
/// otherwise.
///
/// There are various pain points that need to be taken care of to extend
/// the set of valid candidates:
/// * it is not obvious how to incrementally check whether a block
/// formatting context still contains floats or not;
/// * the propagation of text decorations towards node descendants is
/// hard to do incrementally with our current representation of boxes
/// * how intrinsic content sizes are computed eagerly makes it hard
/// to update those sizes for ancestors of the node from which we
/// made an incremental update.
pub fn update(context: &LayoutContext, mut dirty_node: ServoLayoutNode<'_>) -> bool {
#[allow(clippy::enum_variant_names)]
enum UpdatePoint {
AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>),
AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize),
AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>),
AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>),
}
fn update_point(
node: ServoLayoutNode<'_>,
) -> Option<(Arc<ComputedValues>, DisplayInside, UpdatePoint)> {
if !node.is_element() {
return None;
}
if node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) {
// This can require changes to the canvas background.
return None;
}
// Don't update unstyled nodes or nodes that have pseudo-elements.
let element_data = node.style_data()?.element_data.borrow();
if !element_data.styles.pseudos.is_empty() {
return None;
}
let layout_data = NodeExt::layout_data(&node)?;
if layout_data.pseudo_before_box.borrow().is_some() {
return None;
}
if layout_data.pseudo_after_box.borrow().is_some() {
return None;
}
let primary_style = element_data.styles.primary();
let box_style = primary_style.get_box();
if !box_style.position.is_absolutely_positioned() {
return None;
}
let display_inside = match Display::from(box_style.display) {
Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => {
inside
},
_ => return None,
};
let update_point =
match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? {
LayoutBox::DisplayContents(..) => return None,
LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box.clone())
},
_ => return None,
},
LayoutBox::InlineLevel(inline_level_items) => {
let inline_level_box = inline_level_items.first()?;
let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) =
&*inline_level_box.borrow()
else {
return None;
};
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box.clone(),
*text_offset_index,
)
},
LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box.clone())
},
_ => return None,
},
LayoutBox::TableLevelBox(..) => return None,
LayoutBox::TaffyItemBox(taffy_level_box) => match &taffy_level_box
.borrow()
.taffy_level_box
{
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box.clone())
},
_ => return None,
},
};
Some((primary_style.clone(), display_inside, update_point))
}
loop {
let Some((primary_style, display_inside, update_point)) = update_point(dirty_node)
else {
dirty_node = match dirty_node.parent_node() {
Some(parent) => parent,
None => return false,
};
continue;
};
let contents = ReplacedContents::for_element(dirty_node, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let info = NodeAndStyleInfo::new(dirty_node, Arc::clone(&primary_style));
let out_of_flow_absolutely_positioned_box = ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
);
match update_point {
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box) => {
*block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box,
text_offset_index,
) => {
*inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
text_offset_index,
);
},
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box) => {
*flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => {
taffy_level_box.borrow_mut().taffy_level_box =
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
}
break;
}
// We are going to rebuild the box tree from the update point downward, but this update
// point is an absolute, which means that it needs to be laid out again in the containing
// block for absolutes, which is established by one of its ancestors. In addition,
// absolutes, when laid out, can produce more absolutes (either fixed or absolutely
// positioned) elements, so there may be yet more layout that has to happen in this
// ancestor.
//
// We do not know which ancestor is the one that established the containing block for this
// update point, so just invalidate the fragment cache of all ancestors, meaning that even
// though the box tree is preserved, the fragment tree from the root to the update point and
// all of its descendants will need to be rebuilt. This isn't as bad as it seems, because
// siblings and siblings of ancestors of this path through the tree will still have cached
// fragments.
//
// TODO: Do better. This is still a very crude way to do incremental layout.
while let Some(parent_node) = dirty_node.parent_node() {
parent_node.invalidate_cached_fragment();
dirty_node = parent_node;
}
true
}
}
fn construct_for_root_element(
context: &LayoutContext,
root_element: ServoLayoutNode<'_>,
) -> Vec<ArcRefCell<BlockLevelBox>> {
let info = NodeAndStyleInfo::new(root_element, root_element.style(context.shared_context()));
let box_style = info.style.get_box();
let display_inside = match Display::from(box_style.display) {
Display::None => {
root_element.unset_all_boxes();
return Vec::new();
},
Display::Contents => {
// Unreachable because the style crate adjusts the computed values:
// https://drafts.csswg.org/css-display-3/#transformations
// “'display' of 'contents' computes to 'block' on the root element”
unreachable!()
},
// The root element is blockified, ignore DisplayOutside
Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(),
};
let contents = ReplacedContents::for_element(root_element, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let propagated_data = PropagatedBoxTreeData::default();
let root_box = if box_style.position.is_absolutely_positioned() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
))
} else if box_style.float.is_floating() {
BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
context,
&info,
display_inside,
contents,
propagated_data,
))
} else {
BlockLevelBox::Independent(IndependentFormattingContext::construct(
context,
&info,
display_inside,
contents,
propagated_data,
))
};
let root_box = ArcRefCell::new(root_box);
root_element
.element_box_slot()
.set(LayoutBox::BlockLevel(root_box.clone()));
vec![root_box]
}
impl BoxTree {
pub fn layout(
&self,
layout_context: &LayoutContext,
viewport: UntypedSize2D<Au>,
) -> FragmentTree {
let style = layout_context
.style_context
.stylist
.device()
.default_computed_values();
// FIXME: use the documents mode:
// https://drafts.csswg.org/css-writing-modes/#principal-flow
let physical_containing_block: Rect<Au, CSSPixel> =
PhysicalSize::from_untyped(viewport).into();
let initial_containing_block = DefiniteContainingBlock {
size: LogicalVec2 {
inline: physical_containing_block.size.width,
block: physical_containing_block.size.height,
},
style,
};
let mut positioning_context = PositioningContext::default();
let independent_layout = self.root.layout(
layout_context,
&mut positioning_context,
&(&initial_containing_block).into(),
false, /* depends_on_block_constraints */
);
let mut root_fragments = independent_layout.fragments.into_iter().collect::<Vec<_>>();
// Zero box for `:root { display: none }`, one for the root element otherwise.
assert!(root_fragments.len() <= 1);
// There may be more fragments at the top-level
// (for positioned boxes whose containing is the initial containing block)
// but only if there was one fragment for the root element.
positioning_context.layout_initial_containing_block_children(
layout_context,
&initial_containing_block,
&mut root_fragments,
);
FragmentTree::new(
layout_context,
root_fragments,
physical_containing_block,
self.viewport_scroll_sensitivity,
)
}
}

View file

@ -3,16 +3,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use app_units::Au;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_arc::Arc;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
use style::selector_parser::PseudoElement;
use crate::context::LayoutContext;
use crate::dom::NodeExt;
use crate::dom_traversal::{Contents, NodeAndStyleInfo};
use crate::flexbox::FlexContainer;
use crate::flow::BlockFormattingContext;
use crate::fragment_tree::{BaseFragmentInfo, FragmentFlags};
use crate::geom::LazySize;
use crate::layout_box_base::{
CacheableLayoutResult, CacheableLayoutResultAndInputs, LayoutBoxBase,
};
@ -27,13 +30,13 @@ use crate::{
};
/// <https://drafts.csswg.org/css-display/#independent-formatting-context>
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) struct IndependentFormattingContext {
pub base: LayoutBoxBase,
pub contents: IndependentFormattingContextContents,
}
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) enum IndependentFormattingContextContents {
NonReplaced(IndependentNonReplacedContents),
Replaced(ReplacedContents),
@ -41,7 +44,7 @@ pub(crate) enum IndependentFormattingContextContents {
// Private so that code outside of this module cannot match variants.
// It should got through methods instead.
#[derive(Debug)]
#[derive(Debug, MallocSizeOf)]
pub(crate) enum IndependentNonReplacedContents {
Flow(BlockFormattingContext),
Flex(FlexContainer),
@ -52,7 +55,7 @@ pub(crate) enum IndependentNonReplacedContents {
/// The baselines of a layout or a [`crate::fragment_tree::BoxFragment`]. Some layout
/// uses the first and some layout uses the last.
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, MallocSizeOf)]
pub(crate) struct Baselines {
pub first: Option<Au>,
pub last: Option<Au>,
@ -68,9 +71,9 @@ impl Baselines {
}
impl IndependentFormattingContext {
pub fn construct<'dom, Node: NodeExt<'dom>>(
pub fn construct(
context: &LayoutContext,
node_and_style_info: &NodeAndStyleInfo<Node>,
node_and_style_info: &NodeAndStyleInfo,
display_inside: DisplayInside,
contents: Contents,
propagated_data: PropagatedBoxTreeData,
@ -110,11 +113,11 @@ impl IndependentFormattingContext {
let table_grid_style = context
.shared_context()
.stylist
.style_for_anonymous::<Node::ConcreteElement>(
&context.shared_context().guards,
&PseudoElement::ServoTableGrid,
&node_and_style_info.style,
);
.style_for_anonymous::<ServoLayoutElement>(
&context.shared_context().guards,
&PseudoElement::ServoTableGrid,
&node_and_style_info.style,
);
base_fragment_info.flags.insert(FragmentFlags::DO_NOT_PAINT);
IndependentNonReplacedContents::Table(Table::construct(
context,
@ -216,16 +219,32 @@ impl IndependentFormattingContext {
},
}
}
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
self.base.repair_style(new_style);
match &mut self.contents {
IndependentFormattingContextContents::NonReplaced(content) => {
content.repair_style(context, node, new_style);
},
IndependentFormattingContextContents::Replaced(..) => {},
}
}
}
impl IndependentNonReplacedContents {
pub fn layout(
pub(crate) fn layout_without_caching(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block_for_children: &ContainingBlock,
containing_block: &ContainingBlock,
depends_on_block_constraints: bool,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
match self {
IndependentNonReplacedContents::Flow(bfc) => bfc.layout(
@ -239,6 +258,7 @@ impl IndependentNonReplacedContents {
positioning_context,
containing_block_for_children,
depends_on_block_constraints,
lazy_block_size,
),
IndependentNonReplacedContents::Grid(fc) => fc.layout(
layout_context,
@ -265,7 +285,8 @@ impl IndependentNonReplacedContents {
level = "trace",
)
)]
pub fn layout_with_caching(
#[allow(clippy::too_many_arguments)]
pub fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
@ -273,8 +294,10 @@ impl IndependentNonReplacedContents {
containing_block: &ContainingBlock,
base: &LayoutBoxBase,
depends_on_block_constraints: bool,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
if let Some(cache) = base.cached_layout_result.borrow().as_ref() {
let cache = &**cache;
if cache.containing_block_for_children_size.inline ==
containing_block_for_children.size.inline &&
(cache.containing_block_for_children_size.block ==
@ -293,23 +316,21 @@ impl IndependentNonReplacedContents {
);
}
let mut child_positioning_context = PositioningContext::new_for_subtree(
positioning_context.collects_for_nearest_positioned_ancestor(),
);
let result = self.layout(
let mut child_positioning_context = PositioningContext::default();
let result = self.layout_without_caching(
layout_context,
&mut child_positioning_context,
containing_block_for_children,
containing_block,
depends_on_block_constraints,
lazy_block_size,
);
*base.cached_layout_result.borrow_mut() = Some(CacheableLayoutResultAndInputs {
*base.cached_layout_result.borrow_mut() = Some(Box::new(CacheableLayoutResultAndInputs {
result: result.clone(),
positioning_context: child_positioning_context.clone(),
containing_block_for_children_size: containing_block_for_children.size.clone(),
});
}));
positioning_context.append(child_positioning_context);
result
@ -335,6 +356,26 @@ impl IndependentNonReplacedContents {
pub(crate) fn is_table(&self) -> bool {
matches!(self, Self::Table(_))
}
fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
match self {
IndependentNonReplacedContents::Flow(block_formatting_context) => {
block_formatting_context.repair_style(node, new_style);
},
IndependentNonReplacedContents::Flex(flex_container) => {
flex_container.repair_style(new_style)
},
IndependentNonReplacedContents::Grid(taffy_container) => {
taffy_container.repair_style(new_style)
},
IndependentNonReplacedContents::Table(table) => table.repair_style(context, new_style),
}
}
}
impl ComputeInlineContentSizes for IndependentNonReplacedContents {

View file

@ -3,6 +3,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use bitflags::bitflags;
use malloc_size_of::malloc_size_of_is_0;
use malloc_size_of_derive::MallocSizeOf;
use script_layout_interface::combine_id_with_fragment_type;
use style::dom::OpaqueNode;
use style::selector_parser::PseudoElement;
@ -10,7 +12,7 @@ use style::selector_parser::PseudoElement;
/// This data structure stores fields that are common to all non-base
/// Fragment types and should generally be the first member of all
/// concrete fragments.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, MallocSizeOf)]
pub(crate) struct BaseFragment {
/// A tag which identifies the DOM node and pseudo element of this
/// Fragment's content. If this fragment is for an anonymous box,
@ -30,15 +32,13 @@ impl BaseFragment {
}
}
/// Returns true if this fragment is non-anonymous and it is for the given
/// OpaqueNode, regardless of the pseudo element.
pub(crate) fn is_for_node(&self, node: OpaqueNode) -> bool {
self.tag.map(|tag| tag.node == node).unwrap_or(false)
pub(crate) fn is_anonymous(&self) -> bool {
self.tag.is_none()
}
}
/// Information necessary to construct a new BaseFragment.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, MallocSizeOf)]
pub(crate) struct BaseFragmentInfo {
/// The tag to use for the new BaseFragment, if it is not an anonymous Fragment.
pub tag: Option<Tag>,
@ -107,9 +107,11 @@ bitflags! {
}
}
malloc_size_of_is_0!(FragmentFlags);
/// A data structure used to hold DOM and pseudo-element information about
/// a particular layout object.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)]
pub(crate) struct Tag {
pub(crate) node: OpaqueNode,
pub(crate) pseudo: Option<PseudoElement>,
@ -128,11 +130,6 @@ impl Tag {
Tag { node, pseudo }
}
/// Returns true if this tag is for a pseudo element.
pub(crate) fn is_pseudo(&self) -> bool {
self.pseudo.is_some()
}
pub(crate) fn to_display_list_fragment_id(self) -> u64 {
combine_id_with_fragment_type(self.node.id(), self.pseudo.into())
}

Some files were not shown because too many files have changed in this diff Show more