Merge branch 'main' into issue_36590

This commit is contained in:
Usman Yahaya Baba 2025-05-31 19:44:56 +01:00 committed by GitHub
commit 0e2a826a0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2293 changed files with 53299 additions and 391712 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

View file

@ -169,11 +169,17 @@ jobs:
- name: Build for aarch64 HarmonyOS - name: Build for aarch64 HarmonyOS
run: | run: |
./mach build --locked --target aarch64-unknown-linux-ohos --profile=${{ inputs.profile }} --flavor=harmonyos --no-default-features --features tracing,tracing-hitrace ./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 - uses: actions/upload-artifact@v4
with: with:
# Upload the **unsigned** artifact - We don't have the signing materials in pull request workflows # 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 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: test-harmonyos-aarch64:
@ -239,10 +245,19 @@ jobs:
[[ $servo_pid =~ ^[0-9]+$ ]] || { echo "It looks like servo crashed!" ; exit 1; } [[ $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 # 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 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" - name: "Run benchmark"
run: hitrace-bench --bencher -b "org.servo.servo" -p "https://www.servo.org" -n 5 run: hitrace-bench -r runs.json
- name: Getting bencher - name: Getting bencher
uses: bencherdev/bencher@main 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 - name: Uploading to bencher.dev
run: | run: |
bencher run --adapter json --file bench.json --project '${{ env.BENCHER_PROJECT }}' --token '${{ secrets.BENCHER_API_TOKEN }}' --github-actions '${{ secrets.GITHUB_TOKEN }}' 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

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

627
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" rust-version = "1.85.0"
[workspace.dependencies] [workspace.dependencies]
accountable-refcell = "0.2.0" accountable-refcell = "0.2.2"
aes = "0.8.4" aes = "0.8.4"
aes-gcm = "0.10.3" aes-gcm = "0.10.3"
aes-kw = { version = "0.2.1", features = ["alloc"] } aes-kw = { version = "0.2.1", features = ["alloc"] }
@ -70,7 +70,7 @@ gstreamer-sys = "0.23"
gstreamer-video = "0.23" gstreamer-video = "0.23"
harfbuzz-sys = "0.6.1" harfbuzz-sys = "0.6.1"
headers = "0.4" headers = "0.4"
hitrace = "0.1.4" hitrace = "0.1.5"
html5ever = "0.31" html5ever = "0.31"
http = "1.3" http = "1.3"
http-body-util = "0.1" http-body-util = "0.1"
@ -113,6 +113,7 @@ rand_isaac = "0.3"
raw-window-handle = "0.6" raw-window-handle = "0.6"
rayon = "1" rayon = "1"
regex = "1.11" regex = "1.11"
resvg = "0.45.0"
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] } rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }
rustls-pemfile = "2.0" rustls-pemfile = "2.0"
rustls-pki-types = "1.12" rustls-pki-types = "1.12"

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 [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. 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 ## Getting started

View file

@ -1,4 +1,4 @@
# Security Policy # 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

@ -24,7 +24,6 @@ ipc-channel = { workspace = true }
log = { workspace = true } log = { workspace = true }
lyon_geom = "1.0.4" lyon_geom = "1.0.4"
net_traits = { workspace = true } net_traits = { workspace = true }
num-traits = { workspace = true }
pixels = { path = "../pixels" } pixels = { path = "../pixels" }
range = { path = "../range" } range = { path = "../range" }
raqote = "0.8.5" raqote = "0.8.5"

View file

@ -97,7 +97,10 @@ impl<'a> CanvasPaintThread<'a> {
let canvas_data = canvas_paint_thread.create_canvas(size); let canvas_data = canvas_paint_thread.create_canvas(size);
creator.send(canvas_data).unwrap(); creator.send(canvas_data).unwrap();
}, },
Ok(ConstellationCanvasMsg::Exit) => break, Ok(ConstellationCanvasMsg::Exit(exit_sender)) => {
let _ = exit_sender.send(());
break;
},
Err(e) => { Err(e) => {
warn!("Error on CanvasPaintThread receive ({})", e); warn!("Error on CanvasPaintThread receive ({})", e);
break; break;

View file

@ -34,11 +34,11 @@ log = { workspace = true }
net = { path = "../net" } net = { path = "../net" }
pixels = { path = "../pixels" } pixels = { path = "../pixels" }
profile_traits = { workspace = true } profile_traits = { workspace = true }
script_traits = { workspace = true }
servo_allocator = { path = "../allocator" } servo_allocator = { path = "../allocator" }
servo_config = { path = "../config" } servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" } servo_geometry = { path = "../geometry" }
stylo_traits = { workspace = true } stylo_traits = { workspace = true }
timers = { path = "../timers" }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
webrender = { workspace = true } webrender = { workspace = true }
webrender_api = { workspace = true } webrender_api = { workspace = true }

View file

@ -7,10 +7,9 @@ use std::collections::HashMap;
use std::env; use std::env;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::iter::once; use std::iter::once;
use std::mem::take;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
@ -33,7 +32,7 @@ use fnv::FnvHashMap;
use ipc_channel::ipc::{self, IpcSharedMemory}; use ipc_channel::ipc::{self, IpcSharedMemory};
use libc::c_void; use libc::c_void;
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use pixels::{CorsStatus, Image, ImageFrame, PixelFormat}; use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind}; use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind};
use profile_traits::time::{self as profile_time, ProfilerCategory}; use profile_traits::time::{self as profile_time, ProfilerCategory};
use profile_traits::{path, time_profile}; use profile_traits::{path, time_profile};
@ -54,8 +53,9 @@ use webrender_api::{
}; };
use crate::InitialCompositorState; use crate::InitialCompositorState;
use crate::refresh_driver::RefreshDriver;
use crate::webview_manager::WebViewManager; use crate::webview_manager::WebViewManager;
use crate::webview_renderer::{UnknownWebView, WebViewRenderer}; use crate::webview_renderer::{PinchZoomResult, UnknownWebView, WebViewRenderer};
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum UnableToComposite { enum UnableToComposite {
@ -87,6 +87,9 @@ pub enum WebRenderDebugOption {
} }
/// Data that is shared by all WebView renderers. /// Data that is shared by all WebView renderers.
pub struct ServoRenderer { pub struct ServoRenderer {
/// The [`RefreshDriver`] which manages the rythym of painting.
refresh_driver: RefreshDriver,
/// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once /// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once
/// all renderer operations become per-`WebView` this map can be removed, but we still sometimes /// all renderer operations become per-`WebView` this map can be removed, but we still sometimes
/// need to work backwards to figure out what `WebView` is associated with a `Pipeline`. /// need to work backwards to figure out what `WebView` is associated with a `Pipeline`.
@ -152,18 +155,14 @@ pub struct IOCompositor {
/// The number of frames pending to receive from WebRender. /// The number of frames pending to receive from WebRender.
pending_frames: usize, pending_frames: usize,
/// The [`Instant`] of the last animation tick, used to avoid flooding the Constellation and
/// ScriptThread with a deluge of animation ticks.
last_animation_tick: Instant,
/// A handle to the memory profiler which will automatically unregister /// A handle to the memory profiler which will automatically unregister
/// when it's dropped. /// when it's dropped.
_mem_profiler_registration: ProfilerRegistration, _mem_profiler_registration: ProfilerRegistration,
} }
/// Why we need to be repainted. This is used for debugging. /// Why we need to be repainted. This is used for debugging.
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default, PartialEq)]
struct RepaintReason(u8); pub(crate) struct RepaintReason(u8);
bitflags! { bitflags! {
impl RepaintReason: u8 { impl RepaintReason: u8 {
@ -387,6 +386,10 @@ impl IOCompositor {
); );
let compositor = IOCompositor { let compositor = IOCompositor {
global: Rc::new(RefCell::new(ServoRenderer { global: Rc::new(RefCell::new(ServoRenderer {
refresh_driver: RefreshDriver::new(
state.constellation_chan.clone(),
state.event_loop_waker,
),
shutdown_state: state.shutdown_state, shutdown_state: state.shutdown_state,
pipeline_to_webview_map: Default::default(), pipeline_to_webview_map: Default::default(),
compositor_receiver: state.receiver, compositor_receiver: state.receiver,
@ -407,7 +410,6 @@ impl IOCompositor {
webrender: Some(state.webrender), webrender: Some(state.webrender),
rendering_context: state.rendering_context, rendering_context: state.rendering_context,
pending_frames: 0, pending_frames: 0,
last_animation_tick: Instant::now(),
_mem_profiler_registration: registration, _mem_profiler_registration: registration,
}; };
@ -451,7 +453,16 @@ impl IOCompositor {
} }
pub fn needs_repaint(&self) -> bool { pub fn needs_repaint(&self) -> bool {
!self.needs_repaint.get().is_empty() let repaint_reason = self.needs_repaint.get();
if repaint_reason.is_empty() {
return false;
}
!self
.global
.borrow()
.refresh_driver
.wait_to_paint(repaint_reason)
} }
pub fn finish_shutting_down(&mut self) { pub fn finish_shutting_down(&mut self) {
@ -520,15 +531,17 @@ impl IOCompositor {
pipeline_id, pipeline_id,
animation_state, animation_state,
) => { ) => {
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
return;
};
if webview_renderer if webview_renderer
.change_pipeline_running_animations_state(pipeline_id, animation_state) && .change_pipeline_running_animations_state(pipeline_id, animation_state)
webview_renderer.animating()
{ {
// These operations should eventually happen per-WebView, but they are self.global
// global now as rendering is still global to all WebViews. .borrow()
self.process_animations(true); .refresh_driver
} .notify_animation_state_changed(webview_renderer);
} }
}, },
@ -573,14 +586,15 @@ impl IOCompositor {
}, },
CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => { CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => {
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
if webview_renderer.set_throttled(pipeline_id, throttled) && return;
webview_renderer.animating() };
{
// These operations should eventually happen per-WebView, but they are if webview_renderer.set_throttled(pipeline_id, throttled) {
// global now as rendering is still global to all WebViews. self.global
self.process_animations(true); .borrow()
} .refresh_driver
.notify_animation_state_changed(webview_renderer);
} }
}, },
@ -621,29 +635,37 @@ impl IOCompositor {
} }
}, },
CompositorMsg::WebDriverMouseButtonEvent(webview_id, action, button, x, y) => { CompositorMsg::WebDriverMouseButtonEvent(
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { webview_id,
warn!("Handling input event for unknown webview: {webview_id}");
return;
};
let dppx = webview_renderer.device_pixels_per_page_pixel();
let point = dppx.transform_point(Point2D::new(x, y));
webview_renderer.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
point,
action, action,
button, button,
})); x,
}, y,
message_id,
CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y) => { ) => {
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
warn!("Handling input event for unknown webview: {webview_id}"); warn!("Handling input event for unknown webview: {webview_id}");
return; return;
}; };
let dppx = webview_renderer.device_pixels_per_page_pixel(); let dppx = webview_renderer.device_pixels_per_page_pixel();
let point = dppx.transform_point(Point2D::new(x, y)); let point = dppx.transform_point(Point2D::new(x, y));
webview_renderer webview_renderer.dispatch_input_event(
.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); InputEvent::MouseButton(MouseButtonEvent::new(action, button, point))
.with_webdriver_message_id(Some(message_id)),
);
},
CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y, message_id) => {
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
warn!("Handling input event for unknown webview: {webview_id}");
return;
};
let dppx = webview_renderer.device_pixels_per_page_pixel();
let point = dppx.transform_point(Point2D::new(x, y));
webview_renderer.dispatch_input_event(
InputEvent::MouseMove(MouseMoveEvent::new(point))
.with_webdriver_message_id(Some(message_id)),
);
}, },
CompositorMsg::WebDriverWheelScrollEvent(webview_id, x, y, delta_x, delta_y) => { CompositorMsg::WebDriverWheelScrollEvent(webview_id, x, y, delta_x, delta_y) => {
@ -1264,39 +1286,6 @@ impl IOCompositor {
self.set_needs_repaint(RepaintReason::Resize); self.set_needs_repaint(RepaintReason::Resize);
} }
/// If there are any animations running, dispatches appropriate messages to the constellation.
fn process_animations(&mut self, force: bool) {
// When running animations in order to dump a screenshot (not after a full composite), don't send
// animation ticks faster than about 60Hz.
//
// TODO: This should be based on the refresh rate of the screen and also apply to all
// animation ticks, not just ones sent while waiting to dump screenshots. This requires
// something like a refresh driver concept though.
if !force && (Instant::now() - self.last_animation_tick) < Duration::from_millis(16) {
return;
}
self.last_animation_tick = Instant::now();
let animating_webviews: Vec<_> = self
.webview_renderers
.iter()
.filter_map(|webview_renderer| {
if webview_renderer.animating() {
Some(webview_renderer.id)
} else {
None
}
})
.collect();
if !animating_webviews.is_empty() {
if let Err(error) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::TickAnimation(animating_webviews),
) {
warn!("Sending tick to constellation failed ({error:?}).");
}
}
}
pub fn on_zoom_reset_window_event(&mut self, webview_id: WebViewId) { pub fn on_zoom_reset_window_event(&mut self, webview_id: WebViewId) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return; return;
@ -1403,6 +1392,11 @@ impl IOCompositor {
/// Render the WebRender scene to the active `RenderingContext`. If successful, trigger /// Render the WebRender scene to the active `RenderingContext`. If successful, trigger
/// the next round of animations. /// the next round of animations.
pub fn render(&mut self) -> bool { pub fn render(&mut self) -> bool {
self.global
.borrow()
.refresh_driver
.notify_will_paint(self.webview_renderers.iter());
if let Err(error) = self.render_inner() { if let Err(error) = self.render_inner() {
warn!("Unable to render: {error:?}"); warn!("Unable to render: {error:?}");
return false; return false;
@ -1412,9 +1406,6 @@ impl IOCompositor {
// the scene no longer needs to be repainted. // the scene no longer needs to be repainted.
self.needs_repaint.set(RepaintReason::empty()); self.needs_repaint.set(RepaintReason::empty());
// Queue up any subsequent paints for animations.
self.process_animations(true);
true true
} }
@ -1424,7 +1415,7 @@ impl IOCompositor {
&mut self, &mut self,
webview_id: WebViewId, webview_id: WebViewId,
page_rect: Option<Rect<f32, CSSPixel>>, page_rect: Option<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, UnableToComposite> { ) -> Result<Option<RasterImage>, UnableToComposite> {
self.render_inner()?; self.render_inner()?;
let size = self.rendering_context.size2d().to_i32(); let size = self.rendering_context.size2d().to_i32();
@ -1451,16 +1442,19 @@ impl IOCompositor {
Ok(self Ok(self
.rendering_context .rendering_context
.read_to_image(rect) .read_to_image(rect)
.map(|image| Image { .map(|image| RasterImage {
metadata: ImageMetadata {
width: image.width(), width: image.width(),
height: image.height(), height: image.height(),
},
format: PixelFormat::RGBA8, format: PixelFormat::RGBA8,
frames: vec![ImageFrame { frames: vec![ImageFrame {
delay: None, delay: None,
bytes: ipc::IpcSharedMemory::from_bytes(&image), byte_range: 0..image.len(),
width: image.width(), width: image.width(),
height: image.height(), height: image.height(),
}], }],
bytes: ipc::IpcSharedMemory::from_bytes(&image),
id: None, id: None,
cors_status: CorsStatus::Safe, cors_status: CorsStatus::Safe,
})) }))
@ -1482,10 +1476,8 @@ impl IOCompositor {
if opts::get().wait_for_stable_image { if opts::get().wait_for_stable_image {
// The current image may be ready to output. However, if there are animations active, // The current image may be ready to output. However, if there are animations active,
// tick those instead and continue waiting for the image output to be stable AND // continue waiting for the image output to be stable AND all active animations to complete.
// all active animations to complete.
if self.animations_or_animation_callbacks_running() { if self.animations_or_animation_callbacks_running() {
self.process_animations(false);
return Err(UnableToComposite::NotReadyToPaintImage( return Err(UnableToComposite::NotReadyToPaintImage(
NotReadyToPaint::AnimationsActive, NotReadyToPaint::AnimationsActive,
)); ));
@ -1668,11 +1660,39 @@ impl IOCompositor {
if let Err(err) = self.rendering_context.make_current() { if let Err(err) = self.rendering_context.make_current() {
warn!("Failed to make the rendering context current: {:?}", err); warn!("Failed to make the rendering context current: {:?}", err);
} }
let mut webview_renderers = take(&mut self.webview_renderers);
for webview_renderer in webview_renderers.iter_mut() { let mut need_zoom = false;
webview_renderer.process_pending_scroll_events(self); let scroll_offset_updates: Vec<_> = self
.webview_renderers
.iter_mut()
.filter_map(|webview_renderer| {
let (zoom, scroll_result) =
webview_renderer.process_pending_scroll_and_pinch_zoom_events();
need_zoom = need_zoom || (zoom == PinchZoomResult::DidPinchZoom);
scroll_result
})
.collect();
if need_zoom || !scroll_offset_updates.is_empty() {
let mut transaction = Transaction::new();
if need_zoom {
self.send_root_pipeline_display_list_in_transaction(&mut transaction);
} }
self.webview_renderers = webview_renderers; for update in scroll_offset_updates {
let offset = LayoutVector2D::new(-update.offset.x, -update.offset.y);
transaction.set_scroll_offsets(
update.external_scroll_id,
vec![SampledScrollOffset {
offset,
generation: 0,
}],
);
}
self.generate_frame(&mut transaction, RenderReasons::APZ);
self.global.borrow_mut().send_transaction(transaction);
}
self.global.borrow().shutdown_state() != ShutdownState::FinishedShuttingDown self.global.borrow().shutdown_state() != ShutdownState::FinishedShuttingDown
} }

View file

@ -11,7 +11,7 @@ use compositing_traits::rendering_context::RenderingContext;
use compositing_traits::{CompositorMsg, CompositorProxy}; use compositing_traits::{CompositorMsg, CompositorProxy};
use constellation_traits::EmbedderToConstellationMessage; use constellation_traits::EmbedderToConstellationMessage;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use embedder_traits::ShutdownState; use embedder_traits::{EventLoopWaker, ShutdownState};
use profile_traits::{mem, time}; use profile_traits::{mem, time};
use webrender::RenderApi; use webrender::RenderApi;
use webrender_api::DocumentId; use webrender_api::DocumentId;
@ -22,9 +22,10 @@ pub use crate::compositor::{IOCompositor, WebRenderDebugOption};
mod tracing; mod tracing;
mod compositor; mod compositor;
mod refresh_driver;
mod touch; mod touch;
pub mod webview_manager; mod webview_manager;
pub mod webview_renderer; mod webview_renderer;
/// Data used to construct a compositor. /// Data used to construct a compositor.
pub struct InitialCompositorState { pub struct InitialCompositorState {
@ -49,4 +50,7 @@ pub struct InitialCompositorState {
pub webrender_gl: Rc<dyn gleam::gl::Gl>, pub webrender_gl: Rc<dyn gleam::gl::Gl>,
#[cfg(feature = "webxr")] #[cfg(feature = "webxr")]
pub webxr_main_thread: webxr::MainThreadRegistry, 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

@ -20,15 +20,11 @@ use fnv::FnvHashSet;
use log::{debug, warn}; use log::{debug, warn};
use servo_geometry::DeviceIndependentPixel; use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor}; use style_traits::{CSSPixel, PinchZoomFactor};
use webrender::Transaction;
use webrender_api::units::{ use webrender_api::units::{
DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D, DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D,
}; };
use webrender_api::{ use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation};
ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation,
};
use crate::IOCompositor;
use crate::compositor::{PipelineDetails, ServoRenderer}; use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
@ -55,6 +51,19 @@ enum ScrollZoomEvent {
Scroll(ScrollEvent), 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 /// 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 /// 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. /// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself.
@ -678,17 +687,17 @@ impl WebViewRenderer {
/// <http://w3c.github.io/touch-events/#mouse-events> /// <http://w3c.github.io/touch-events/#mouse-events>
fn simulate_mouse_click(&mut self, point: DevicePoint) { fn simulate_mouse_click(&mut self, point: DevicePoint) {
let button = MouseButton::Left; let button = MouseButton::Left;
self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point)));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Down,
button, button,
action: MouseButtonAction::Down,
point, point,
})); )));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Up,
button, button,
action: MouseButtonAction::Up,
point, point,
})); )));
} }
pub(crate) fn notify_scroll_event( pub(crate) fn notify_scroll_event(
@ -737,9 +746,17 @@ impl WebViewRenderer {
self.on_scroll_window_event(scroll_location, cursor) self.on_scroll_window_event(scroll_location, cursor)
} }
pub(crate) fn process_pending_scroll_events(&mut self, compositor: &mut IOCompositor) { /// 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() { if self.pending_scroll_zoom_events.is_empty() {
return; return (PinchZoomResult::DidNotPinchZoom, None);
} }
// Batch up all scroll events into one, or else we'll do way too much painting. // Batch up all scroll events into one, or else we'll do way too much painting.
@ -790,37 +807,24 @@ impl WebViewRenderer {
} }
} }
let zoom_changed =
self.set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification);
let scroll_result = combined_scroll_event.and_then(|combined_event| { let scroll_result = combined_scroll_event.and_then(|combined_event| {
self.scroll_node_at_device_point( self.scroll_node_at_device_point(
combined_event.cursor.to_f32(), combined_event.cursor.to_f32(),
combined_event.scroll_location, combined_event.scroll_location,
) )
}); });
if !zoom_changed && scroll_result.is_none() { if let Some(scroll_result) = scroll_result {
return; self.send_scroll_positions_to_layout_for_pipeline(scroll_result.pipeline_id);
} }
let mut transaction = Transaction::new(); let pinch_zoom_result = match self
if zoom_changed { .set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification)
compositor.send_root_pipeline_display_list_in_transaction(&mut transaction); {
} true => PinchZoomResult::DidPinchZoom,
false => PinchZoomResult::DidNotPinchZoom,
};
if let Some((pipeline_id, external_id, offset)) = scroll_result { (pinch_zoom_result, 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`] /// Perform a hit test at the given [`DevicePoint`] and apply the [`ScrollLocation`]
@ -831,7 +835,7 @@ impl WebViewRenderer {
&mut self, &mut self,
cursor: DevicePoint, cursor: DevicePoint,
scroll_location: ScrollLocation, scroll_location: ScrollLocation,
) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> { ) -> Option<ScrollResult> {
let scroll_location = match scroll_location { let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => { ScrollLocation::Delta(delta) => {
let device_pixels_per_page = self.device_pixels_per_page_pixel(); let device_pixels_per_page = self.device_pixels_per_page_pixel();
@ -871,8 +875,12 @@ impl WebViewRenderer {
let scroll_result = pipeline_details let scroll_result = pipeline_details
.scroll_tree .scroll_tree
.scroll_node_or_ancestor(scroll_tree_node, scroll_location); .scroll_node_or_ancestor(scroll_tree_node, scroll_location);
if let Some((external_id, offset)) = scroll_result { if let Some((external_scroll_id, offset)) = scroll_result {
return Some((*pipeline_id, external_id, offset)); return Some(ScrollResult {
pipeline_id: *pipeline_id,
external_scroll_id,
offset,
});
} }
} }
} }

View file

@ -99,7 +99,6 @@ pub struct Preferences {
pub dom_serviceworker_timeout_seconds: i64, pub dom_serviceworker_timeout_seconds: i64,
pub dom_servo_helpers_enabled: bool, pub dom_servo_helpers_enabled: bool,
pub dom_servoparser_async_html_tokenizer_enabled: bool, pub dom_servoparser_async_html_tokenizer_enabled: bool,
pub dom_shadowdom_enabled: bool,
pub dom_svg_enabled: bool, pub dom_svg_enabled: bool,
pub dom_testable_crash_enabled: bool, pub dom_testable_crash_enabled: bool,
pub dom_testbinding_enabled: bool, pub dom_testbinding_enabled: bool,
@ -117,10 +116,6 @@ pub struct Preferences {
// https://testutils.spec.whatwg.org#availability // https://testutils.spec.whatwg.org#availability
pub dom_testutils_enabled: bool, pub dom_testutils_enabled: bool,
pub dom_trusted_types_enabled: bool, pub dom_trusted_types_enabled: bool,
/// Enable the [URLPattern] API.
///
/// [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
pub dom_urlpattern_enabled: bool,
pub dom_xpath_enabled: bool, pub dom_xpath_enabled: bool,
/// Enable WebGL2 APIs. /// Enable WebGL2 APIs.
pub dom_webgl2_enabled: bool, pub dom_webgl2_enabled: bool,
@ -277,7 +272,6 @@ impl Preferences {
dom_serviceworker_timeout_seconds: 60, dom_serviceworker_timeout_seconds: 60,
dom_servo_helpers_enabled: false, dom_servo_helpers_enabled: false,
dom_servoparser_async_html_tokenizer_enabled: false, dom_servoparser_async_html_tokenizer_enabled: false,
dom_shadowdom_enabled: true,
dom_svg_enabled: false, dom_svg_enabled: false,
dom_testable_crash_enabled: false, dom_testable_crash_enabled: false,
dom_testbinding_enabled: false, dom_testbinding_enabled: false,
@ -294,7 +288,6 @@ impl Preferences {
dom_testperf_enabled: false, dom_testperf_enabled: false,
dom_testutils_enabled: false, dom_testutils_enabled: false,
dom_trusted_types_enabled: false, dom_trusted_types_enabled: false,
dom_urlpattern_enabled: false,
dom_webgl2_enabled: false, dom_webgl2_enabled: false,
dom_webgpu_enabled: false, dom_webgpu_enabled: false,
dom_webgpu_wgpu_backend: String::new(), dom_webgpu_wgpu_backend: String::new(),

View file

@ -132,7 +132,7 @@ use embedder_traits::{
FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError, FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError,
JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState,
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
WebDriverLoadStatus, WebDriverCommandResponse, WebDriverLoadStatus,
}; };
use euclid::Size2D; use euclid::Size2D;
use euclid::default::Size2D as UntypedSize2D; use euclid::default::Size2D as UntypedSize2D;
@ -148,6 +148,7 @@ use net_traits::pub_domains::reg_host;
use net_traits::request::Referrer; use net_traits::request::Referrer;
use net_traits::storage_thread::{StorageThreadMsg, StorageType}; use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads}; use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads};
use profile_traits::mem::ProfilerMsg;
use profile_traits::{mem, time}; use profile_traits::{mem, time};
use script_layout_interface::{LayoutFactory, ScriptThreadFactory}; use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::{ use script_traits::{
@ -172,6 +173,7 @@ use crate::browsingcontext::{
AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator, AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator,
NewBrowsingContextInfo, NewBrowsingContextInfo,
}; };
use crate::constellation_webview::ConstellationWebView;
use crate::event_loop::EventLoop; use crate::event_loop::EventLoop;
use crate::pipeline::{InitialPipelineState, Pipeline}; use crate::pipeline::{InitialPipelineState, Pipeline};
use crate::process_manager::ProcessManager; use crate::process_manager::ProcessManager;
@ -229,18 +231,6 @@ struct WebrenderWGPU {
wgpu_image_map: WGPUImageMap, wgpu_image_map: WGPUImageMap,
} }
/// Servo supports multiple top-level browsing contexts or “webviews”, so `Constellation` needs to
/// store webview-specific data for bookkeeping.
struct WebView {
/// The currently focused browsing context in this webview for key events.
/// The focused pipeline is the current entry of the focused browsing
/// context.
focused_browsing_context_id: BrowsingContextId,
/// The joint session history for this webview.
session_history: JointSessionHistory,
}
/// A browsing context group. /// A browsing context group.
/// ///
/// <https://html.spec.whatwg.org/multipage/#browsing-context-group> /// <https://html.spec.whatwg.org/multipage/#browsing-context-group>
@ -324,7 +314,7 @@ pub struct Constellation<STF, SWF> {
compositor_proxy: CompositorProxy, compositor_proxy: CompositorProxy,
/// Bookkeeping data for all webviews in the constellation. /// Bookkeeping data for all webviews in the constellation.
webviews: WebViewManager<WebView>, webviews: WebViewManager<ConstellationWebView>,
/// Channels for the constellation to send messages to the public /// Channels for the constellation to send messages to the public
/// resource-related threads. There are two groups of resource threads: one /// resource-related threads. There are two groups of resource threads: one
@ -532,6 +522,8 @@ pub struct InitialConstellationState {
struct WebDriverData { struct WebDriverData {
load_channel: Option<(PipelineId, IpcSender<WebDriverLoadStatus>)>, load_channel: Option<(PipelineId, IpcSender<WebDriverLoadStatus>)>,
resize_channel: Option<IpcSender<Size2D<f32, CSSPixel>>>, resize_channel: Option<IpcSender<Size2D<f32, CSSPixel>>>,
// Forward responses from the script thread to the webdriver server.
input_command_response_sender: Option<IpcSender<WebDriverCommandResponse>>,
} }
impl WebDriverData { impl WebDriverData {
@ -539,6 +531,7 @@ impl WebDriverData {
WebDriverData { WebDriverData {
load_channel: None, load_channel: None,
resize_channel: None, resize_channel: None,
input_command_response_sender: None,
} }
} }
} }
@ -892,6 +885,16 @@ where
if self.shutting_down { if self.shutting_down {
return; return;
} }
let Some(theme) = self
.webviews
.get(webview_id)
.map(ConstellationWebView::theme)
else {
warn!("Tried to create Pipeline for uknown WebViewId: {webview_id:?}");
return;
};
debug!( debug!(
"{}: Creating new pipeline in {}", "{}: Creating new pipeline in {}",
pipeline_id, browsing_context_id pipeline_id, browsing_context_id
@ -970,6 +973,7 @@ where
time_profiler_chan: self.time_profiler_chan.clone(), time_profiler_chan: self.time_profiler_chan.clone(),
mem_profiler_chan: self.mem_profiler_chan.clone(), mem_profiler_chan: self.mem_profiler_chan.clone(),
viewport_details: initial_viewport_details, viewport_details: initial_viewport_details,
theme,
event_loop, event_loop,
load_data, load_data,
prev_throttled: throttled, prev_throttled: throttled,
@ -1433,8 +1437,8 @@ where
size_type, size_type,
); );
}, },
EmbedderToConstellationMessage::ThemeChange(theme) => { EmbedderToConstellationMessage::ThemeChange(webview_id, theme) => {
self.handle_theme_change(theme); self.handle_theme_change(webview_id, theme);
}, },
EmbedderToConstellationMessage::TickAnimation(webview_ids) => { EmbedderToConstellationMessage::TickAnimation(webview_ids) => {
self.handle_tick_animation(webview_ids) self.handle_tick_animation(webview_ids)
@ -1485,6 +1489,9 @@ where
) => { ) => {
self.handle_evaluate_javascript(webview_id, evaluation_id, script); self.handle_evaluate_javascript(webview_id, evaluation_id, script);
}, },
EmbedderToConstellationMessage::CreateMemoryReport(sender) => {
self.mem_profiler_chan.send(ProfilerMsg::Report(sender));
},
} }
} }
@ -1867,6 +1874,18 @@ where
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => { ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => {
self.handle_finish_javascript_evaluation(evaluation_id, result) self.handle_finish_javascript_evaluation(evaluation_id, result)
}, },
ScriptToConstellationMessage::WebDriverInputComplete(msg_id) => {
if let Some(ref reply_sender) = self.webdriver.input_command_response_sender {
reply_sender
.send(WebDriverCommandResponse { id: msg_id })
.unwrap_or_else(|_| {
warn!("Failed to send WebDriverInputComplete {:?}", msg_id);
self.webdriver.input_command_response_sender = None;
});
} else {
warn!("No WebDriver input_command_response_sender");
}
},
} }
} }
@ -2743,7 +2762,11 @@ where
} }
debug!("Exiting Canvas Paint thread."); debug!("Exiting Canvas Paint thread.");
if let Err(e) = self.canvas_sender.send(ConstellationCanvasMsg::Exit) { let (canvas_exit_sender, canvas_exit_receiver) = unbounded();
if let Err(e) = self
.canvas_sender
.send(ConstellationCanvasMsg::Exit(canvas_exit_sender))
{
warn!("Exit Canvas Paint thread failed ({})", e); warn!("Exit Canvas Paint thread failed ({})", e);
} }
@ -2785,6 +2808,10 @@ where
debug!("Exiting GLPlayer thread."); debug!("Exiting GLPlayer thread.");
WindowGLContext::get().exit(); WindowGLContext::get().exit();
// Wait for the canvas thread to exit before shutting down the font service, as
// canvas might still be using the system font service before shutting down.
let _ = canvas_exit_receiver.recv();
debug!("Exiting the system font service thread."); debug!("Exiting the system font service thread.");
self.system_font_service.exit(); self.system_font_service.exit();
@ -3127,13 +3154,8 @@ where
// Register this new top-level browsing context id as a webview and set // Register this new top-level browsing context id as a webview and set
// its focused browsing context to be itself. // its focused browsing context to be itself.
self.webviews.add( self.webviews
webview_id, .add(webview_id, ConstellationWebView::new(browsing_context_id));
WebView {
focused_browsing_context_id: browsing_context_id,
session_history: JointSessionHistory::new(),
},
);
// https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group // https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group
let mut new_bc_group: BrowsingContextGroup = Default::default(); let mut new_bc_group: BrowsingContextGroup = Default::default();
@ -3539,10 +3561,7 @@ where
self.pipelines.insert(new_pipeline_id, pipeline); self.pipelines.insert(new_pipeline_id, pipeline);
self.webviews.add( self.webviews.add(
new_webview_id, new_webview_id,
WebView { ConstellationWebView::new(new_browsing_context_id),
focused_browsing_context_id: new_browsing_context_id,
session_history: JointSessionHistory::new(),
},
); );
// https://html.spec.whatwg.org/multipage/#bcg-append // https://html.spec.whatwg.org/multipage/#bcg-append
@ -4836,7 +4855,11 @@ where
mouse_button, mouse_button,
x, x,
y, y,
msg_id,
response_sender,
) => { ) => {
self.webdriver.input_command_response_sender = Some(response_sender);
self.compositor_proxy self.compositor_proxy
.send(CompositorMsg::WebDriverMouseButtonEvent( .send(CompositorMsg::WebDriverMouseButtonEvent(
webview_id, webview_id,
@ -4844,11 +4867,16 @@ where
mouse_button, mouse_button,
x, x,
y, y,
msg_id,
)); ));
}, },
WebDriverCommandMsg::MouseMoveAction(webview_id, x, y) => { WebDriverCommandMsg::MouseMoveAction(webview_id, x, y, msg_id, response_sender) => {
self.webdriver.input_command_response_sender = Some(response_sender);
self.compositor_proxy self.compositor_proxy
.send(CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y)); .send(CompositorMsg::WebDriverMouseMoveEvent(
webview_id, x, y, msg_id,
));
}, },
WebDriverCommandMsg::WheelScrollAction(webview, x, y, delta_x, delta_y) => { WebDriverCommandMsg::WheelScrollAction(webview, x, y, delta_x, delta_y) => {
self.compositor_proxy self.compositor_proxy
@ -5599,18 +5627,31 @@ where
} }
} }
/// Handle theme change events from the embedder and forward them to the script thread /// Handle theme change events from the embedder and forward them to all appropriate `ScriptThread`s.
#[cfg_attr( #[cfg_attr(
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)] )]
fn handle_theme_change(&mut self, theme: Theme) { fn handle_theme_change(&mut self, webview_id: WebViewId, theme: Theme) {
let Some(webview) = self.webviews.get_mut(webview_id) else {
warn!("Received theme change request for uknown WebViewId: {webview_id:?}");
return;
};
if !webview.set_theme(theme) {
return;
}
for pipeline in self.pipelines.values() { for pipeline in self.pipelines.values() {
let msg = ScriptThreadMessage::ThemeChange(pipeline.id, theme); if pipeline.webview_id != webview_id {
if let Err(err) = pipeline.event_loop.send(msg) { continue;
}
if let Err(error) = pipeline
.event_loop
.send(ScriptThreadMessage::ThemeChange(pipeline.id, theme))
{
warn!( warn!(
"{}: Failed to send theme change event to pipeline ({:?}).", "{}: Failed to send theme change event to pipeline ({error:?}).",
pipeline.id, err pipeline.id,
); );
} }
} }

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

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

View file

@ -25,7 +25,7 @@ use constellation_traits::{LoadData, SWManagerMsg, ScriptToConstellationChan};
use crossbeam_channel::{Sender, unbounded}; use crossbeam_channel::{Sender, unbounded};
use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg}; use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::user_content_manager::UserContentManager;
use embedder_traits::{AnimationState, FocusSequenceNumber, ViewportDetails}; use embedder_traits::{AnimationState, FocusSequenceNumber, Theme, ViewportDetails};
use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender}; use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender};
use ipc_channel::Error; use ipc_channel::Error;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@ -61,7 +61,7 @@ pub struct Pipeline {
/// The ID of the browsing context that contains this Pipeline. /// The ID of the browsing context that contains this Pipeline.
pub browsing_context_id: BrowsingContextId, 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 webview_id: WebViewId,
pub opener: Option<BrowsingContextId>, pub opener: Option<BrowsingContextId>,
@ -170,6 +170,9 @@ pub struct InitialPipelineState {
/// The initial [`ViewportDetails`] to use when starting this new [`Pipeline`]. /// The initial [`ViewportDetails`] to use when starting this new [`Pipeline`].
pub viewport_details: ViewportDetails, 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. /// The ID of the pipeline namespace for this script thread.
pub pipeline_namespace_id: PipelineNamespaceId, pub pipeline_namespace_id: PipelineNamespaceId,
@ -224,6 +227,7 @@ impl Pipeline {
opener: state.opener, opener: state.opener,
load_data: state.load_data.clone(), load_data: state.load_data.clone(),
viewport_details: state.viewport_details, viewport_details: state.viewport_details,
theme: state.theme,
}; };
if let Err(e) = script_chan.send(ScriptThreadMessage::AttachLayout(new_layout_info)) if let Err(e) = script_chan.send(ScriptThreadMessage::AttachLayout(new_layout_info))
@ -280,6 +284,7 @@ impl Pipeline {
time_profiler_chan: state.time_profiler_chan, time_profiler_chan: state.time_profiler_chan,
mem_profiler_chan: state.mem_profiler_chan, mem_profiler_chan: state.mem_profiler_chan,
viewport_details: state.viewport_details, viewport_details: state.viewport_details,
theme: state.theme,
script_chan: script_chan.clone(), script_chan: script_chan.clone(),
load_data: state.load_data.clone(), load_data: state.load_data.clone(),
script_port, script_port,
@ -494,6 +499,7 @@ pub struct UnprivilegedPipelineContent {
time_profiler_chan: time::ProfilerChan, time_profiler_chan: time::ProfilerChan,
mem_profiler_chan: profile_mem::ProfilerChan, mem_profiler_chan: profile_mem::ProfilerChan,
viewport_details: ViewportDetails, viewport_details: ViewportDetails,
theme: Theme,
script_chan: IpcSender<ScriptThreadMessage>, script_chan: IpcSender<ScriptThreadMessage>,
load_data: LoadData, load_data: LoadData,
script_port: IpcReceiver<ScriptThreadMessage>, script_port: IpcReceiver<ScriptThreadMessage>,
@ -544,6 +550,7 @@ impl UnprivilegedPipelineContent {
memory_profiler_sender: self.mem_profiler_chan.clone(), memory_profiler_sender: self.mem_profiler_chan.clone(),
devtools_server_sender: self.devtools_ipc_sender, devtools_server_sender: self.devtools_ipc_sender,
viewport_details: self.viewport_details, viewport_details: self.viewport_details,
theme: self.theme,
pipeline_namespace_id: self.pipeline_namespace_id, pipeline_namespace_id: self.pipeline_namespace_id,
content_process_shutdown_sender: content_process_shutdown_chan, content_process_shutdown_sender: content_process_shutdown_chan,
webgl_chan: self.webgl_chan, webgl_chan: self.webgl_chan,

View file

@ -78,6 +78,7 @@ mod from_compositor {
Self::SetScrollStates(..) => target!("SetScrollStates"), Self::SetScrollStates(..) => target!("SetScrollStates"),
Self::PaintMetric(..) => target!("PaintMetric"), Self::PaintMetric(..) => target!("PaintMetric"),
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"), Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),
Self::CreateMemoryReport(..) => target!("CreateMemoryReport"),
} }
} }
} }
@ -177,6 +178,7 @@ mod from_script {
Self::TitleChanged(..) => target!("TitleChanged"), Self::TitleChanged(..) => target!("TitleChanged"),
Self::IFrameSizes(..) => target!("IFrameSizes"), Self::IFrameSizes(..) => target!("IFrameSizes"),
Self::ReportMemory(..) => target!("ReportMemory"), Self::ReportMemory(..) => target!("ReportMemory"),
Self::WebDriverInputComplete(..) => target!("WebDriverInputComplete"),
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"), Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
} }
} }
@ -239,7 +241,7 @@ mod from_script {
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"), Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
Self::ShutdownComplete => target_variant!("ShutdownComplete"), Self::ShutdownComplete => target_variant!("ShutdownComplete"),
Self::ShowNotification(..) => target_variant!("ShowNotification"), Self::ShowNotification(..) => target_variant!("ShowNotification"),
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"), Self::ShowFormControl(..) => target_variant!("ShowFormControl"),
Self::FinishJavaScriptEvaluation(..) => { Self::FinishJavaScriptEvaluation(..) => {
target_variant!("FinishJavaScriptEvaluation") target_variant!("FinishJavaScriptEvaluation")
}, },

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

@ -19,6 +19,7 @@ use serde::Serialize;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use self::network_parent::{NetworkParentActor, NetworkParentActorMsg}; use self::network_parent::{NetworkParentActor, NetworkParentActorMsg};
use super::breakpoint::BreakpointListActor;
use super::thread::ThreadActor; use super::thread::ThreadActor;
use super::worker::WorkerMsg; use super::worker::WorkerMsg;
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
@ -362,10 +363,14 @@ impl Actor for WatcherActor {
ActorMessageStatus::Processed ActorMessageStatus::Processed
}, },
"getBreakpointListActor" => { "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 { let _ = stream.write_json_packet(&GetBreakpointListActorReply {
from: self.name(), from: self.name(),
breakpoint_list: GetBreakpointListActorReplyInner { breakpoint_list: GetBreakpointListActorReplyInner {
actor: registry.new_name("breakpoint-list"), actor: breakpoint_list_name,
}, },
}); });
ActorMessageStatus::Processed ActorMessageStatus::Processed

View file

@ -53,6 +53,7 @@ use crate::protocol::JsonPacketStream;
mod actor; mod actor;
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors> /// <https://searchfox.org/mozilla-central/source/devtools/server/actors>
mod actors { mod actors {
pub mod breakpoint;
pub mod browsing_context; pub mod browsing_context;
pub mod console; pub mod console;
pub mod device; pub mod device;

View file

@ -287,7 +287,7 @@ impl PlatformFontMethods for PlatformFont {
.unwrap_or(average_advance); .unwrap_or(average_advance);
let metrics = FontMetrics { 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 // TODO(Issue #201): underline metrics are not reliable. Have to pull out of font table
// directly. // directly.
// //

View file

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

View file

@ -25,7 +25,9 @@ where
{ {
let system_fc = FontCollection::system(); let system_fc = FontCollection::system();
for family in system_fc.families_iter() { 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 { impl LocalFontIdentifier {
pub fn index(&self) -> u32 { pub fn index(&self) -> u32 {
FontCollection::system() 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()) .map_or(0, |font| font.create_font_face().get_index())
} }
pub(crate) fn native_font_handle(&self) -> NativeFontHandle { pub(crate) fn native_font_handle(&self) -> NativeFontHandle {
let face = FontCollection::system() 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") .expect("Could not create Font from FontDescriptor")
.create_font_face(); .create_font_face();
let path = face let path = face
@ -62,7 +68,9 @@ impl LocalFontIdentifier {
} }
pub(crate) fn read_data_from_file(&self) -> Option<Vec<u8>> { 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 face = font.create_font_face();
let files = face.get_files(); let files = face.get_files();
assert!(!files.is_empty()); assert!(!files.is_empty());
@ -86,10 +94,12 @@ where
F: FnMut(FontTemplate), F: FnMut(FontTemplate),
{ {
let system_fc = FontCollection::system(); 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(); let count = family.get_font_count();
for i in 0..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 template_descriptor = (&font).into();
let local_font_identifier = LocalFontIdentifier { let local_font_identifier = LocalFontIdentifier {
font_descriptor: Arc::new(font.to_descriptor()), font_descriptor: Arc::new(font.to_descriptor()),

View file

@ -21,7 +21,6 @@ app_units = { workspace = true }
atomic_refcell = { workspace = true } atomic_refcell = { workspace = true }
base = { workspace = true } base = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
canvas_traits = { workspace = true }
compositing_traits = { workspace = true } compositing_traits = { workspace = true }
constellation_traits = { workspace = true } constellation_traits = { workspace = true }
data-url = { workspace = true } data-url = { workspace = true }

View file

@ -150,7 +150,6 @@ impl<'a, 'dom> ModernContainerBuilder<'a, 'dom> {
let inline_formatting_context = inline_formatting_context_builder.finish( let inline_formatting_context = inline_formatting_context_builder.finish(
self.context, self.context,
self.propagated_data,
true, /* has_first_formatted_line */ true, /* has_first_formatted_line */
false, /* is_single_line_text_box */ false, /* is_single_line_text_box */
self.info.style.writing_mode.to_bidi_level(), self.info.style.writing_mode.to_bidi_level(),

View file

@ -10,17 +10,21 @@ use fnv::FnvHashMap;
use fonts::FontContext; use fonts::FontContext;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use net_traits::image_cache::{ use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, UsePlaceholder, Image as CachedImage, ImageCache, ImageCacheResult, ImageOrMetadataAvailable, PendingImageId,
UsePlaceholder,
}; };
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use pixels::Image as PixelImage; use pixels::RasterImage;
use script_layout_interface::{IFrameSizes, ImageAnimationState, PendingImage, PendingImageState}; use script_layout_interface::{
IFrameSizes, ImageAnimationState, PendingImage, PendingImageState, PendingRasterizationImage,
};
use servo_url::{ImmutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, ServoUrl};
use style::context::SharedStyleContext; use style::context::SharedStyleContext;
use style::dom::OpaqueNode; use style::dom::OpaqueNode;
use style::values::computed::image::{Gradient, Image}; use style::values::computed::image::{Gradient, Image};
use webrender_api::units::{DeviceIntSize, DeviceSize};
use crate::display_list::WebRenderImageInfo; pub(crate) type CachedImageOrError = Result<CachedImage, ResolveImageError>;
pub struct LayoutContext<'a> { pub struct LayoutContext<'a> {
pub id: PipelineId, pub id: PipelineId,
@ -39,11 +43,17 @@ pub struct LayoutContext<'a> {
/// A list of in-progress image loads to be shared with the script thread. /// A list of in-progress image loads to be shared with the script thread.
pub pending_images: Mutex<Vec<PendingImage>>, 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. /// A collection of `<iframe>` sizes to send back to script.
pub iframe_sizes: Mutex<IFrameSizes>, pub iframe_sizes: Mutex<IFrameSizes>,
pub webrender_image_cache: // A cache that maps image resources used in CSS (e.g as the `url()` value
Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), WebRenderImageInfo>>>, // 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>>>, pub node_image_animation_map: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>,
@ -53,18 +63,24 @@ pub struct LayoutContext<'a> {
pub enum ResolvedImage<'a> { pub enum ResolvedImage<'a> {
Gradient(&'a Gradient), Gradient(&'a Gradient),
Image(WebRenderImageInfo), // 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<'_> { impl Drop for LayoutContext<'_> {
fn drop(&mut self) { fn drop(&mut self) {
if !std::thread::panicking() { if !std::thread::panicking() {
assert!(self.pending_images.lock().is_empty()); assert!(self.pending_images.lock().is_empty());
assert!(self.pending_rasterization_images.lock().is_empty());
} }
} }
} }
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub enum ResolveImageError { pub enum ResolveImageError {
LoadError, LoadError,
ImagePending, ImagePending,
@ -78,18 +94,24 @@ pub enum ResolveImageError {
None, None,
} }
pub(crate) enum LayoutImageCacheResult {
Pending,
DataAvailable(ImageOrMetadataAvailable),
LoadError,
}
impl LayoutContext<'_> { impl LayoutContext<'_> {
#[inline(always)] #[inline(always)]
pub fn shared_context(&self) -> &SharedStyleContext { pub fn shared_context(&self) -> &SharedStyleContext {
&self.style_context &self.style_context
} }
pub fn get_or_request_image_or_meta( pub(crate) fn get_or_request_image_or_meta(
&self, &self,
node: OpaqueNode, node: OpaqueNode,
url: ServoUrl, url: ServoUrl,
use_placeholder: UsePlaceholder, use_placeholder: UsePlaceholder,
) -> Result<ImageOrMetadataAvailable, ResolveImageError> { ) -> LayoutImageCacheResult {
// Check for available image or start tracking. // Check for available image or start tracking.
let cache_result = self.image_cache.get_cached_image_status( let cache_result = self.image_cache.get_cached_image_status(
url.clone(), url.clone(),
@ -99,7 +121,9 @@ impl LayoutContext<'_> {
); );
match cache_result { match cache_result {
ImageCacheResult::Available(img_or_meta) => Ok(img_or_meta), ImageCacheResult::Available(img_or_meta) => {
LayoutImageCacheResult::DataAvailable(img_or_meta)
},
// Image has been requested, is still pending. Return no image for this paint loop. // 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. // When the image loads it will trigger a reflow and/or repaint.
ImageCacheResult::Pending(id) => { ImageCacheResult::Pending(id) => {
@ -110,7 +134,7 @@ impl LayoutContext<'_> {
origin: self.origin.clone(), origin: self.origin.clone(),
}; };
self.pending_images.lock().push(image); self.pending_images.lock().push(image);
Result::Err(ResolveImageError::ImagePending) LayoutImageCacheResult::Pending
}, },
// Not yet requested - request image or metadata from the cache // Not yet requested - request image or metadata from the cache
ImageCacheResult::ReadyForRequest(id) => { ImageCacheResult::ReadyForRequest(id) => {
@ -121,14 +145,14 @@ impl LayoutContext<'_> {
origin: self.origin.clone(), origin: self.origin.clone(),
}; };
self.pending_images.lock().push(image); self.pending_images.lock().push(image);
Result::Err(ResolveImageError::ImageRequested) LayoutImageCacheResult::Pending
}, },
// Image failed to load, so just return nothing // Image failed to load, so just return the same error.
ImageCacheResult::LoadError => Result::Err(ResolveImageError::LoadError), ImageCacheResult::LoadError => LayoutImageCacheResult::LoadError,
} }
} }
pub fn handle_animated_image(&self, node: OpaqueNode, image: Arc<PixelImage>) { pub fn handle_animated_image(&self, node: OpaqueNode, image: Arc<RasterImage>) {
let mut store = self.node_image_animation_map.write(); let mut store = self.node_image_animation_map.write();
// 1. first check whether node previously being track for animated image. // 1. first check whether node previously being track for animated image.
@ -137,53 +161,86 @@ impl LayoutContext<'_> {
if image_state.image_key() != image.id { if image_state.image_key() != image.id {
if image.should_animate() { if image.should_animate() {
// i. Register/Replace tracking item in image_animation_manager. // i. Register/Replace tracking item in image_animation_manager.
store.insert(node, ImageAnimationState::new(image)); store.insert(
node,
ImageAnimationState::new(
image,
self.shared_context().current_time_for_animations,
),
);
} else { } else {
// ii. Cancel Action if the node's image is no longer animated. // ii. Cancel Action if the node's image is no longer animated.
store.remove(&node); store.remove(&node);
} }
} }
} else if image.should_animate() { } else if image.should_animate() {
store.insert(node, ImageAnimationState::new(image)); store.insert(
node,
ImageAnimationState::new(image, self.shared_context().current_time_for_animations),
);
} }
} }
fn get_webrender_image_for_url( fn get_cached_image_for_url(
&self, &self,
node: OpaqueNode, node: OpaqueNode,
url: ServoUrl, url: ServoUrl,
use_placeholder: UsePlaceholder, use_placeholder: UsePlaceholder,
) -> Result<WebRenderImageInfo, ResolveImageError> { ) -> Result<CachedImage, ResolveImageError> {
if let Some(existing_webrender_image) = self if let Some(cached_image) = self
.webrender_image_cache .resolved_images_cache
.read() .read()
.get(&(url.clone(), use_placeholder)) .get(&(url.clone(), use_placeholder))
{ {
return Ok(*existing_webrender_image); return cached_image.clone();
} }
let image_or_meta =
self.get_or_request_image_or_meta(node, url.clone(), use_placeholder)?; let result = self.get_or_request_image_or_meta(node, url.clone(), use_placeholder);
match image_or_meta { match result {
LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
ImageOrMetadataAvailable::ImageAvailable { image, .. } => { ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
if let Some(image) = image.as_raster_image() {
self.handle_animated_image(node, image.clone()); self.handle_animated_image(node, image.clone());
let image_info = WebRenderImageInfo {
size: Size2D::new(image.width, image.height),
key: image.id,
};
if image_info.key.is_none() {
Ok(image_info)
} else {
let mut webrender_image_cache = self.webrender_image_cache.write();
webrender_image_cache.insert((url, use_placeholder), image_info);
Ok(image_info)
} }
let mut resolved_images_cache = self.resolved_images_cache.write();
resolved_images_cache.insert((url, use_placeholder), Ok(image.clone()));
Ok(image)
}, },
ImageOrMetadataAvailable::MetadataAvailable(..) => { ImageOrMetadataAvailable::MetadataAvailable(..) => {
Result::Err(ResolveImageError::OnlyMetadata) 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>( pub fn resolve_image<'a>(
&self, &self,
node: Option<OpaqueNode>, node: Option<OpaqueNode>,
@ -206,12 +263,14 @@ impl LayoutContext<'_> {
// element and not just the node. // element and not just the node.
let image_url = image_url.url().ok_or(ResolveImageError::InvalidUrl)?; let image_url = image_url.url().ok_or(ResolveImageError::InvalidUrl)?;
let node = node.ok_or(ResolveImageError::MissingNode)?; let node = node.ok_or(ResolveImageError::MissingNode)?;
let webrender_info = self.get_webrender_image_for_url( let image = self.get_cached_image_for_url(
node, node,
image_url.clone().into(), image_url.clone().into(),
UsePlaceholder::No, UsePlaceholder::No,
)?; )?;
Ok(ResolvedImage::Image(webrender_info)) let metadata = image.metadata();
let size = Size2D::new(metadata.width, metadata.height).to_f32();
Ok(ResolvedImage::Image { image, size })
}, },
Image::ImageSet(image_set) => { Image::ImageSet(image_set) => {
image_set image_set
@ -221,17 +280,32 @@ impl LayoutContext<'_> {
.and_then(|image| { .and_then(|image| {
self.resolve_image(node, &image.image) self.resolve_image(node, &image.image)
.map(|info| match info { .map(|info| match info {
ResolvedImage::Image(mut image_info) => { ResolvedImage::Image {
image: cached_image,
..
} => {
// From <https://drafts.csswg.org/css-images-4/#image-set-notation>: // From <https://drafts.csswg.org/css-images-4/#image-set-notation>:
// > A <resolution> (optional). This is used to help the UA decide // > A <resolution> (optional). This is used to help the UA decide
// > which <image-set-option> to choose. If the image reference is // > which <image-set-option> to choose. If the image reference is
// > for a raster image, it also specifies the images natural // > for a raster image, it also specifies the images natural
// > resolution, overriding any other source of data that might // > resolution, overriding any other source of data that might
// > supply a natural resolution. // > supply a natural resolution.
image_info.size = (image_info.size.to_f32() / let image_metadata = cached_image.metadata();
image.resolution.dppx()) let size = if cached_image.as_raster_image().is_some() {
.to_u32(); let scale_factor = image.resolution.dppx();
ResolvedImage::Image(image_info) 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, _ => info,
}) })

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use app_units::Au; 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_attachment::SingleComputedValue as BackgroundAttachment;
use style::computed_values::background_clip::single_value::T as Clip; use style::computed_values::background_clip::single_value::T as Clip;
use style::computed_values::background_origin::single_value::T as Origin; 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 webrender_api::{self as wr, units};
use wr::ClipChainId; use wr::ClipChainId;
use wr::units::LayoutSize;
use crate::replaced::NaturalSizes; use crate::replaced::NaturalSizes;
@ -66,8 +65,7 @@ impl<'a> BackgroundPainter<'a> {
if &BackgroundAttachment::Fixed == if &BackgroundAttachment::Fixed ==
get_cyclic(&background.background_attachment.0, layer_index) get_cyclic(&background.background_attachment.0, layer_index)
{ {
let viewport_size = builder.display_list.compositor_info.viewport_size; return builder.compositor_info.viewport_size.into();
return units::LayoutRect::from_origin_and_size(Point2D::origin(), viewport_size);
} }
match get_cyclic(&background.background_clip.0, layer_index) { match get_cyclic(&background.background_clip.0, layer_index) {
@ -121,7 +119,7 @@ impl<'a> BackgroundPainter<'a> {
if &BackgroundAttachment::Fixed == if &BackgroundAttachment::Fixed ==
get_cyclic(&style.get_background().background_attachment.0, layer_index) 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 common
} }
@ -132,6 +130,7 @@ impl<'a> BackgroundPainter<'a> {
pub(super) fn positioning_area( pub(super) fn positioning_area(
&self, &self,
fragment_builder: &'a super::BuilderForBoxFragment, fragment_builder: &'a super::BuilderForBoxFragment,
builder: &mut super::DisplayListBuilder,
layer_index: usize, layer_index: usize,
) -> units::LayoutRect { ) -> units::LayoutRect {
if let Some(positioning_area_override) = self.positioning_area_override { 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::PaddingBox => *fragment_builder.padding_rect(),
Origin::BorderBox => fragment_builder.border_rect, Origin::BorderBox => fragment_builder.border_rect,
}, },
BackgroundAttachment::Fixed => { BackgroundAttachment::Fixed => builder.compositor_info.viewport_size.into(),
// 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),
)
},
} }
} }
} }
@ -170,7 +162,7 @@ pub(super) fn layout_layer(
natural_sizes: NaturalSizes, natural_sizes: NaturalSizes,
) -> Option<BackgroundLayer> { ) -> Option<BackgroundLayer> {
let painting_area = painter.painting_area(fragment_builder, builder, layer_index); 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); let common = painter.common_properties(fragment_builder, builder, layer_index, painting_area);
// https://drafts.csswg.org/css-backgrounds/#background-size // 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

@ -1,259 +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 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::ClipChainId;
use webrender_api::units::{LayoutRect, LayoutSideOffsets, LayoutSize};
use super::{BuilderForBoxFragment, DisplayList, compute_margin_box_radius, normalize_radii};
pub(super) fn build_clip_path_clip_chain_if_necessary(
clip_path: ClipPath,
display_list: &mut DisplayList,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
fragment_builder: BuilderForBoxFragment,
) -> Option<ClipChainId> {
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(_) => {
build_simple_shape(
*shape,
layout_rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
)
},
BasicShape::Polygon(_) | BasicShape::PathOrShape(_) => None,
}
} else {
Some(create_rect_clip_chain(
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,
display_list,
))
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "display_list::build_simple_shape",
skip_all,
fields(servo_profiling = true),
level = "trace",
)
)]
fn build_simple_shape(
shape: BasicShape,
layout_box: LayoutRect,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
display_list: &mut DisplayList,
) -> Option<ClipChainId> {
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(create_rect_clip_chain(
radii,
shape_rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
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(create_rect_clip_chain(
radii,
rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
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(create_rect_clip_chain(
radii,
rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
_ => 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(),
}
}
fn create_rect_clip_chain(
radii: webrender_api::BorderRadius,
rect: LayoutRect,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
display_list: &mut DisplayList,
) -> ClipChainId {
let new_clip_id = if radii.is_zero() {
display_list
.wr
.define_clip_rect(parent_scroll_node_id.spatial_id, rect)
} else {
display_list.wr.define_clip_rounded_rect(
parent_scroll_node_id.spatial_id,
webrender_api::ComplexClipRegion {
rect,
radii,
mode: webrender_api::ClipMode::Clip,
},
)
};
display_list.define_clip_chain(*parent_clip_chain_id, [new_clip_id])
}

View file

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

View file

@ -8,18 +8,23 @@ use std::sync::Arc;
use app_units::{AU_PER_PX, Au}; use app_units::{AU_PER_PX, Au};
use base::WebRenderEpochToU16; use base::WebRenderEpochToU16;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use compositing_traits::display_list::{AxesScrollSensitivity, CompositorDisplayListInfo}; use clip::{Clip, ClipId};
use compositing_traits::display_list::{CompositorDisplayListInfo, SpatialTreeNodeInfo};
use embedder_traits::Cursor; use embedder_traits::Cursor;
use euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit}; use euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit, Vector2D};
use fonts::GlyphStore; use fonts::GlyphStore;
use gradient::WebRenderGradient; use gradient::WebRenderGradient;
use net_traits::image_cache::Image as CachedImage;
use range::Range as ServoRange; use range::Range as ServoRange;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
use servo_config::opts::DebugOptions;
use servo_geometry::MaxRect; use servo_geometry::MaxRect;
use style::Zero; use style::Zero;
use style::color::{AbsoluteColor, ColorSpace}; use style::color::{AbsoluteColor, ColorSpace};
use style::computed_values::border_image_outset::T as BorderImageOutset; use style::computed_values::border_image_outset::T as BorderImageOutset;
use style::computed_values::text_decoration_style::T as ComputedTextDecorationStyle; use style::computed_values::text_decoration_style::{
T as ComputedTextDecorationStyle, T as TextDecorationStyle,
};
use style::dom::OpaqueNode; use style::dom::OpaqueNode;
use style::properties::ComputedValues; use style::properties::ComputedValues;
use style::properties::longhands::visibility::computed_value::T as Visibility; use style::properties::longhands::visibility::computed_value::T as Visibility;
@ -33,10 +38,11 @@ use style::values::generics::rect::Rect;
use style::values::specified::text::TextDecorationLine; use style::values::specified::text::TextDecorationLine;
use style::values::specified::ui::CursorKind; use style::values::specified::ui::CursorKind;
use style_traits::CSSPixel; use style_traits::CSSPixel;
use webrender_api::units::{DevicePixel, LayoutPixel, LayoutRect, LayoutSize}; use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize};
use webrender_api::{ use webrender_api::{
self as wr, BorderDetails, BoxShadowClipMode, ClipChainId, CommonItemProperties, self as wr, BorderDetails, BoxShadowClipMode, BuiltDisplayList, ClipChainId, ClipMode,
ImageRendering, NinePatchBorder, NinePatchBorderSource, SpatialId, units, CommonItemProperties, ComplexClipRegion, ImageRendering, NinePatchBorder,
NinePatchBorderSource, PropertyBinding, SpatialId, SpatialTreeItemKey, units,
}; };
use wr::units::LayoutVector2D; use wr::units::LayoutVector2D;
@ -55,7 +61,7 @@ use crate::replaced::NaturalSizes;
use crate::style_ext::{BorderStyleColor, ComputedValuesExt}; use crate::style_ext::{BorderStyleColor, ComputedValuesExt};
mod background; mod background;
mod clip_path; mod clip;
mod conversions; mod conversions;
mod gradient; mod gradient;
mod stacking_context; mod stacking_context;
@ -63,79 +69,11 @@ mod stacking_context;
use background::BackgroundPainter; use background::BackgroundPainter;
pub use stacking_context::*; pub use stacking_context::*;
#[derive(Clone, Copy)]
pub struct WebRenderImageInfo {
pub size: Size2D<u32, UnknownUnit>,
pub key: Option<wr::ImageKey>,
}
// webrender's `ItemTag` is private. // webrender's `ItemTag` is private.
type ItemTag = (u64, u16); type ItemTag = (u64, u16);
type HitInfo = Option<ItemTag>; type HitInfo = Option<ItemTag>;
const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX); const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX);
/// Where the information that's used to build display lists is stored. This
/// includes both a [wr::DisplayListBuilder] for building up WebRender-specific
/// display list information and a [CompositorDisplayListInfo] used to store
/// information used by the compositor, such as a compositor-side scroll tree.
pub struct DisplayList {
/// The [wr::DisplayListBuilder] used to collect display list items.
pub wr: wr::DisplayListBuilder,
/// The information about the WebRender display list that the compositor
/// consumes. This curerntly contains the out-of-band hit testing information
/// data structure that the compositor uses to map hit tests to information
/// about the item hit.
pub compositor_info: CompositorDisplayListInfo,
/// A count of the number of SpatialTree nodes pushed to the WebRender display
/// list. This is merely to ensure that the currently-unused SpatialTreeItemKey
/// produced for every SpatialTree node is unique.
pub spatial_tree_count: u64,
}
impl DisplayList {
/// Create a new [DisplayList] given the dimensions of the layout and the WebRender
/// pipeline id.
pub fn new(
viewport_size: units::LayoutSize,
content_size: units::LayoutSize,
pipeline_id: wr::PipelineId,
epoch: wr::Epoch,
viewport_scroll_sensitivity: AxesScrollSensitivity,
first_reflow: bool,
) -> Self {
Self {
wr: wr::DisplayListBuilder::new(pipeline_id),
compositor_info: CompositorDisplayListInfo::new(
viewport_size,
content_size,
pipeline_id,
epoch,
viewport_scroll_sensitivity,
first_reflow,
),
spatial_tree_count: 0,
}
}
pub fn define_clip_chain<I>(&mut self, parent: ClipChainId, clips: I) -> ClipChainId
where
I: IntoIterator<Item = wr::ClipId>,
I::IntoIter: ExactSizeIterator + Clone,
{
// WebRender has two different ways of expressing "no clip." ClipChainId::INVALID should be
// used for primitives, but `None` is used for stacking contexts and clip chains. We convert
// to the `Option<ClipChainId>` representation here. Just passing Some(ClipChainId::INVALID)
// leads to a crash.
let parent = match parent {
ClipChainId::INVALID => None,
parent => Some(parent),
};
self.wr.define_clip_chain(parent, clips)
}
}
pub(crate) struct DisplayListBuilder<'a> { pub(crate) struct DisplayListBuilder<'a> {
/// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This /// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This
/// allows only passing the builder instead passing the containing /// allows only passing the builder instead passing the containing
@ -148,18 +86,21 @@ pub(crate) struct DisplayListBuilder<'a> {
/// `background-attachment: fixed` need to not scroll while the rest of the fragment does. /// `background-attachment: fixed` need to not scroll while the rest of the fragment does.
current_reference_frame_scroll_node_id: ScrollTreeNodeId, current_reference_frame_scroll_node_id: ScrollTreeNodeId,
/// The current [wr::ClipId] for this [DisplayListBuilder]. This allows /// The current [`ClipId`] for this [DisplayListBuilder]. This allows
/// only passing the builder instead passing the containing /// only passing the builder instead passing the containing
/// [stacking_context::StackingContextContent::Fragment] as an argument to display /// [stacking_context::StackingContextContent::Fragment] as an argument to display
/// list building functions. /// list building functions.
current_clip_chain_id: ClipChainId, current_clip_id: ClipId,
/// A [LayoutContext] used to get information about the device pixel ratio /// A [LayoutContext] used to get information about the device pixel ratio
/// and get handles to WebRender images. /// and get handles to WebRender images.
pub context: &'a LayoutContext<'a>, pub context: &'a LayoutContext<'a>,
/// The [DisplayList] used to collect display list items and metadata. /// The [`wr::DisplayListBuilder`] for this Servo [`DisplayListBuilder`].
pub display_list: &'a mut DisplayList, pub webrender_display_list_builder: &'a mut wr::DisplayListBuilder,
/// The [`CompositorDisplayListInfo`] used to collect display list items and metadata.
pub compositor_info: &'a mut CompositorDisplayListInfo,
/// Data about the fragments that are highlighted by the inspector, if any. /// Data about the fragments that are highlighted by the inspector, if any.
/// ///
@ -171,6 +112,10 @@ pub(crate) struct DisplayListBuilder<'a> {
/// element inherits the `<body>`'s background to paint the page canvas background. /// element inherits the `<body>`'s background to paint the page canvas background.
/// See <https://drafts.csswg.org/css-backgrounds/#body-background>. /// See <https://drafts.csswg.org/css-backgrounds/#body-background>.
paint_body_background: bool, paint_body_background: bool,
/// A mapping from [`ClipId`] To WebRender [`ClipChainId`] used when building this WebRender
/// display list.
clip_map: Vec<ClipChainId>,
} }
struct InspectorHighlight { struct InspectorHighlight {
@ -207,45 +152,224 @@ impl InspectorHighlight {
} }
} }
impl DisplayList { impl DisplayListBuilder<'_> {
pub fn build( pub(crate) fn build(
&mut self,
context: &LayoutContext, context: &LayoutContext,
stacking_context_tree: &mut StackingContextTree,
fragment_tree: &FragmentTree, fragment_tree: &FragmentTree,
root_stacking_context: &StackingContext, debug: &DebugOptions,
) { ) -> BuiltDisplayList {
// Build the rest of the display list which inclues all of the WebRender primitives.
let compositor_info = &mut stacking_context_tree.compositor_info;
compositor_info.hit_test_info.clear();
let mut webrender_display_list_builder =
webrender_api::DisplayListBuilder::new(compositor_info.pipeline_id);
webrender_display_list_builder.begin();
// `dump_serialized_display_list` doesn't actually print anything. It sets up
// the display list for printing the serialized version when `finalize()` is called.
// We need to call this before adding any display items so that they are printed
// during `finalize()`.
if debug.dump_display_list {
webrender_display_list_builder.dump_serialized_display_list();
}
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let _span = tracing::trace_span!("display_list::build", servo_profiling = true).entered(); let _span =
tracing::trace_span!("DisplayListBuilder::build", servo_profiling = true).entered();
let mut builder = DisplayListBuilder { let mut builder = DisplayListBuilder {
current_scroll_node_id: self.compositor_info.root_reference_frame_id, current_scroll_node_id: compositor_info.root_reference_frame_id,
current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id, current_reference_frame_scroll_node_id: compositor_info.root_reference_frame_id,
current_clip_chain_id: ClipChainId::INVALID, current_clip_id: ClipId::INVALID,
context, context,
display_list: self, webrender_display_list_builder: &mut webrender_display_list_builder,
compositor_info,
inspector_highlight: context inspector_highlight: context
.highlighted_dom_node .highlighted_dom_node
.map(InspectorHighlight::for_node), .map(InspectorHighlight::for_node),
paint_body_background: true, paint_body_background: true,
clip_map: Default::default(),
}; };
fragment_tree.build_display_list(&mut builder, root_stacking_context);
if let Some(highlight) = builder builder.add_all_spatial_nodes();
.inspector_highlight
.take() for clip in stacking_context_tree.clip_store.0.iter() {
.and_then(|highlight| highlight.state) builder.add_clip_to_display_list(clip);
{ }
builder.paint_dom_inspector_highlight(highlight);
} // Paint the canvas background (if any) before/under everything else
} stacking_context_tree
.root_stacking_context
.build_canvas_background_display_list(&mut builder, fragment_tree);
stacking_context_tree
.root_stacking_context
.build_display_list(&mut builder);
builder.paint_dom_inspector_highlight();
webrender_display_list_builder.end().1
} }
impl DisplayListBuilder<'_> {
fn wr(&mut self) -> &mut wr::DisplayListBuilder { fn wr(&mut self) -> &mut wr::DisplayListBuilder {
&mut self.display_list.wr self.webrender_display_list_builder
}
fn pipeline_id(&mut self) -> wr::PipelineId {
self.compositor_info.pipeline_id
} }
fn mark_is_contentful(&mut self) { fn mark_is_contentful(&mut self) {
self.display_list.compositor_info.is_contentful = true; self.compositor_info.is_contentful = true;
}
fn spatial_id(&self, id: ScrollTreeNodeId) -> SpatialId {
self.compositor_info.scroll_tree.webrender_id(&id)
}
fn clip_chain_id(&self, id: ClipId) -> ClipChainId {
match id {
ClipId::INVALID => ClipChainId::INVALID,
_ => *self
.clip_map
.get(id.0)
.expect("Should never try to get clip before adding it to WebRender display list"),
}
}
pub(crate) fn add_all_spatial_nodes(&mut self) {
// A count of the number of SpatialTree nodes pushed to the WebRender display
// list. This is merely to ensure that the currently-unused SpatialTreeItemKey
// produced for every SpatialTree node is unique.
let mut spatial_tree_count = 0;
let mut scroll_tree = std::mem::take(&mut self.compositor_info.scroll_tree);
let mut mapping = Vec::with_capacity(scroll_tree.nodes.len());
mapping.push(SpatialId::root_reference_frame(self.pipeline_id()));
mapping.push(SpatialId::root_scroll_node(self.pipeline_id()));
let pipeline_id = self.pipeline_id();
let pipeline_tag = ((pipeline_id.0 as u64) << 32) | pipeline_id.1 as u64;
for node in scroll_tree.nodes.iter().skip(2) {
let parent_scroll_node_id = node
.parent
.expect("Should have already added root reference frame");
let parent_spatial_node_id = mapping
.get(parent_scroll_node_id.index)
.expect("Should add spatial nodes to display list in order");
// Produce a new SpatialTreeItemKey. This is currently unused by WebRender,
// but has to be unique to the entire scene.
spatial_tree_count += 1;
let spatial_tree_item_key = SpatialTreeItemKey::new(pipeline_tag, spatial_tree_count);
mapping.push(match &node.info {
SpatialTreeNodeInfo::ReferenceFrame(info) => {
let spatial_id = self.wr().push_reference_frame(
info.origin,
*parent_spatial_node_id,
info.transform_style,
PropertyBinding::Value(info.transform),
info.kind,
spatial_tree_item_key,
);
self.wr().pop_reference_frame();
spatial_id
},
SpatialTreeNodeInfo::Scroll(info) => {
self.wr().define_scroll_frame(
*parent_spatial_node_id,
info.external_id,
info.content_rect,
info.clip_rect,
LayoutVector2D::zero(), /* external_scroll_offset */
0, /* scroll_offset_generation */
wr::HasScrollLinkedEffect::No,
spatial_tree_item_key,
)
},
SpatialTreeNodeInfo::Sticky(info) => {
self.wr().define_sticky_frame(
*parent_spatial_node_id,
info.frame_rect,
info.margins,
info.vertical_offset_bounds,
info.horizontal_offset_bounds,
LayoutVector2D::zero(), /* previously_applied_offset */
spatial_tree_item_key,
None, /* transform */
)
},
});
}
scroll_tree.update_mapping(mapping);
self.compositor_info.scroll_tree = scroll_tree;
}
/// Add the given [`Clip`] to the WebRender display list and create a mapping from
/// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens:
/// - When WebRender display list construction starts: All clips created during the
/// `StackingContextTree` construction are added in one batch. These clips are used
/// for things such as `overflow: scroll` elements.
/// - When a clip is added during WebRender display list construction for individual
/// items. In that case, this is called by [`Self::maybe_create_clip`].
pub(crate) fn add_clip_to_display_list(&mut self, clip: &Clip) -> ClipChainId {
assert_eq!(
clip.id.0,
self.clip_map.len(),
"Clips should be added in order"
);
let spatial_id = self.spatial_id(clip.parent_scroll_node_id);
let new_clip_id = if clip.radii.is_zero() {
self.wr().define_clip_rect(spatial_id, clip.rect)
} else {
self.wr().define_clip_rounded_rect(
spatial_id,
ComplexClipRegion {
rect: clip.rect,
radii: clip.radii,
mode: ClipMode::Clip,
},
)
};
// WebRender has two different ways of expressing "no clip." ClipChainId::INVALID should be
// used for primitives, but `None` is used for stacking contexts and clip chains. We convert
// to the `Option<ClipChainId>` representation here. Just passing Some(ClipChainId::INVALID)
// leads to a crash.
let parent_clip_chain_id = match self.clip_chain_id(clip.parent_clip_id) {
ClipChainId::INVALID => None,
parent => Some(parent),
};
let clip_chain_id = self
.wr()
.define_clip_chain(parent_clip_chain_id, [new_clip_id]);
self.clip_map.push(clip_chain_id);
clip_chain_id
}
/// Add a new clip to the WebRender display list being built. This only happens during
/// WebRender display list building and these clips should be added after all clips
/// from the `StackingContextTree` have already been processed.
fn maybe_create_clip(
&mut self,
radii: wr::BorderRadius,
rect: units::LayoutRect,
force_clip_creation: bool,
) -> Option<ClipChainId> {
if radii.is_zero() && !force_clip_creation {
return None;
}
Some(self.add_clip_to_display_list(&Clip {
id: ClipId(self.clip_map.len()),
radii,
rect,
parent_scroll_node_id: self.current_scroll_node_id,
parent_clip_id: self.current_clip_id,
}))
} }
fn common_properties( fn common_properties(
@ -258,8 +382,8 @@ impl DisplayListBuilder<'_> {
// for fragments that paint their entire border rectangle. // for fragments that paint their entire border rectangle.
wr::CommonItemProperties { wr::CommonItemProperties {
clip_rect, clip_rect,
spatial_id: self.current_scroll_node_id.spatial_id, spatial_id: self.spatial_id(self.current_scroll_node_id),
clip_chain_id: self.current_clip_chain_id, clip_chain_id: self.clip_chain_id(self.current_clip_id),
flags: style.get_webrender_primitive_flags(), flags: style.get_webrender_primitive_flags(),
} }
} }
@ -277,19 +401,24 @@ impl DisplayListBuilder<'_> {
return None; return None;
} }
let hit_test_index = self.display_list.compositor_info.add_hit_test_info( let hit_test_index = self.compositor_info.add_hit_test_info(
tag?.node.0 as u64, tag?.node.0 as u64,
Some(cursor(inherited_ui.cursor.keyword, auto_cursor)), Some(cursor(inherited_ui.cursor.keyword, auto_cursor)),
self.current_scroll_node_id, self.current_scroll_node_id,
); );
Some(( Some((hit_test_index as u64, self.compositor_info.epoch.as_u16()))
hit_test_index as u64,
self.display_list.compositor_info.epoch.as_u16(),
))
} }
/// Draw highlights around the node that is currently hovered in the devtools. /// Draw highlights around the node that is currently hovered in the devtools.
fn paint_dom_inspector_highlight(&mut self, highlight: HighlightTraversalState) { fn paint_dom_inspector_highlight(&mut self) {
let Some(highlight) = self
.inspector_highlight
.take()
.and_then(|highlight| highlight.state)
else {
return;
};
const CONTENT_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { const CONTENT_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF {
r: 0.23, r: 0.23,
g: 0.7, g: 0.7,
@ -327,8 +456,7 @@ impl DisplayListBuilder<'_> {
flags: wr::PrimitiveFlags::default(), flags: wr::PrimitiveFlags::default(),
}; };
self.display_list self.wr()
.wr
.push_rect(&properties, content_box, CONTENT_BOX_HIGHLIGHT_COLOR); .push_rect(&properties, content_box, CONTENT_BOX_HIGHLIGHT_COLOR);
// Highlight margin, border and padding // Highlight margin, border and padding
@ -441,13 +569,16 @@ impl Fragment {
section: StackingContextSection, section: StackingContextSection,
is_hit_test_for_scrollable_overflow: bool, is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool, is_collapsed_table_borders: bool,
text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) { ) {
let spatial_id = builder.spatial_id(builder.current_scroll_node_id);
let clip_chain_id = builder.clip_chain_id(builder.current_clip_id);
if let Some(inspector_highlight) = &mut builder.inspector_highlight { if let Some(inspector_highlight) = &mut builder.inspector_highlight {
if self.tag() == Some(inspector_highlight.tag) { if self.tag() == Some(inspector_highlight.tag) {
inspector_highlight.register_fragment_of_highlighted_dom_node( inspector_highlight.register_fragment_of_highlighted_dom_node(
self, self,
builder.current_scroll_node_id.spatial_id, spatial_id,
builder.current_clip_chain_id, clip_chain_id,
containing_block, containing_block,
); );
} }
@ -550,9 +681,12 @@ impl Fragment {
.get_inherited_box() .get_inherited_box()
.visibility .visibility
{ {
Visibility::Visible => { Visibility::Visible => self.build_display_list_for_text_fragment(
self.build_display_list_for_text_fragment(text, builder, containing_block) text,
}, builder,
containing_block,
text_decorations,
),
Visibility::Hidden => (), Visibility::Hidden => (),
Visibility::Collapse => (), Visibility::Collapse => (),
} }
@ -574,8 +708,8 @@ impl Fragment {
None => return, None => return,
}; };
let clip_chain_id = builder.current_clip_chain_id; let clip_chain_id = builder.clip_chain_id(builder.current_clip_id);
let spatial_id = builder.current_scroll_node_id.spatial_id; let spatial_id = builder.spatial_id(builder.current_scroll_node_id);
builder.wr().push_hit_test( builder.wr().push_hit_test(
rect.to_webrender(), rect.to_webrender(),
clip_chain_id, clip_chain_id,
@ -590,6 +724,7 @@ impl Fragment {
fragment: &TextFragment, fragment: &TextFragment,
builder: &mut DisplayListBuilder, builder: &mut DisplayListBuilder,
containing_block: &PhysicalRect<Au>, containing_block: &PhysicalRect<Au>,
text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) { ) {
// NB: The order of painting text components (CSS Text Decoration Module Level 3) is: // NB: The order of painting text components (CSS Text Decoration Module Level 3) is:
// shadows, underline, overline, text, text-emphasis, and then line-through. // shadows, underline, overline, text, text-emphasis, and then line-through.
@ -599,6 +734,7 @@ impl Fragment {
let rect = fragment.rect.translate(containing_block.origin.to_vector()); let rect = fragment.rect.translate(containing_block.origin.to_vector());
let mut baseline_origin = rect.origin; let mut baseline_origin = rect.origin;
baseline_origin.y += fragment.font_metrics.ascent; baseline_origin.y += fragment.font_metrics.ascent;
let glyphs = glyphs( let glyphs = glyphs(
&fragment.glyphs, &fragment.glyphs,
baseline_origin, baseline_origin,
@ -641,23 +777,36 @@ impl Fragment {
); );
} }
if fragment for text_decoration in text_decorations.iter() {
.text_decoration_line if text_decoration.line.contains(TextDecorationLine::UNDERLINE) {
.contains(TextDecorationLine::UNDERLINE)
{
let mut rect = rect; let mut rect = rect;
rect.origin.y += font_metrics.ascent - font_metrics.underline_offset; rect.origin.y += font_metrics.ascent - font_metrics.underline_offset;
rect.size.height = Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx)); rect.size.height =
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color); Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx));
self.build_display_list_for_text_decoration(
&parent_style,
builder,
&rect,
text_decoration,
TextDecorationLine::UNDERLINE,
);
}
} }
if fragment for text_decoration in text_decorations.iter() {
.text_decoration_line if text_decoration.line.contains(TextDecorationLine::OVERLINE) {
.contains(TextDecorationLine::OVERLINE)
{
let mut rect = rect; let mut rect = rect;
rect.size.height = Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx)); rect.size.height =
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color); Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx));
self.build_display_list_for_text_decoration(
&parent_style,
builder,
&rect,
text_decoration,
TextDecorationLine::OVERLINE,
);
}
} }
// TODO: This caret/text selection implementation currently does not account for vertical text // TODO: This caret/text selection implementation currently does not account for vertical text
@ -734,14 +883,23 @@ impl Fragment {
None, None,
); );
if fragment for text_decoration in text_decorations.iter() {
.text_decoration_line if text_decoration
.line
.contains(TextDecorationLine::LINE_THROUGH) .contains(TextDecorationLine::LINE_THROUGH)
{ {
let mut rect = rect; let mut rect = rect;
rect.origin.y += font_metrics.ascent - font_metrics.strikeout_offset; rect.origin.y += font_metrics.ascent - font_metrics.strikeout_offset;
rect.size.height = Au::from_f32_px(font_metrics.strikeout_size.to_nearest_pixel(dppx)); rect.size.height =
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color); Au::from_f32_px(font_metrics.strikeout_size.to_nearest_pixel(dppx));
self.build_display_list_for_text_decoration(
&parent_style,
builder,
&rect,
text_decoration,
TextDecorationLine::LINE_THROUGH,
);
}
} }
if !shadows.0.is_empty() { if !shadows.0.is_empty() {
@ -754,26 +912,47 @@ impl Fragment {
parent_style: &ServoArc<ComputedValues>, parent_style: &ServoArc<ComputedValues>,
builder: &mut DisplayListBuilder, builder: &mut DisplayListBuilder,
rect: &PhysicalRect<Au>, rect: &PhysicalRect<Au>,
color: &AbsoluteColor, text_decoration: &FragmentTextDecoration,
line: TextDecorationLine,
) { ) {
let rect = rect.to_webrender(); if text_decoration.style == ComputedTextDecorationStyle::MozNone {
let wavy_line_thickness = (0.33 * rect.size().height).ceil();
let text_decoration_color = parent_style
.clone_text_decoration_color()
.resolve_to_absolute(color);
let text_decoration_style = parent_style.clone_text_decoration_style();
if text_decoration_style == ComputedTextDecorationStyle::MozNone {
return; return;
} }
builder.display_list.wr.push_line(
&builder.common_properties(rect, parent_style), let mut rect = rect.to_webrender();
let line_thickness = rect.height().ceil();
if text_decoration.style == ComputedTextDecorationStyle::Wavy {
rect = rect.inflate(0.0, line_thickness * 1.0);
}
let common_properties = builder.common_properties(rect, parent_style);
builder.wr().push_line(
&common_properties,
&rect, &rect,
wavy_line_thickness, line_thickness,
wr::LineOrientation::Horizontal, wr::LineOrientation::Horizontal,
&rgba(text_decoration_color), &rgba(text_decoration.color),
text_decoration_style.to_webrender(), text_decoration.style.to_webrender(),
); );
// XXX(ferjm) support text-decoration-style: double
if text_decoration.style == TextDecorationStyle::Double {
let half_height = (rect.height() / 2.0).floor().max(1.0);
let y_offset = match line {
TextDecorationLine::OVERLINE => -rect.height() - half_height,
_ => rect.height() + half_height,
};
let rect = rect.translate(Vector2D::new(0.0, y_offset));
let common_properties = builder.common_properties(rect, parent_style);
builder.wr().push_line(
&common_properties,
&rect,
line_thickness,
wr::LineOrientation::Horizontal,
&rgba(text_decoration.color),
text_decoration.style.to_webrender(),
);
}
} }
} }
@ -878,12 +1057,8 @@ impl<'a> BuilderForBoxFragment<'a> {
return Some(clip); return Some(clip);
} }
let maybe_clip = create_clip_chain( let maybe_clip =
self.border_radius, builder.maybe_create_clip(self.border_radius, self.border_rect, force_clip_creation);
self.border_rect,
builder,
force_clip_creation,
);
*self.border_edge_clip_chain_id.borrow_mut() = maybe_clip; *self.border_edge_clip_chain_id.borrow_mut() = maybe_clip;
maybe_clip maybe_clip
} }
@ -899,7 +1074,7 @@ impl<'a> BuilderForBoxFragment<'a> {
let radii = inner_radii(self.border_radius, self.fragment.border.to_webrender()); let radii = inner_radii(self.border_radius, self.fragment.border.to_webrender());
let maybe_clip = let maybe_clip =
create_clip_chain(radii, *self.padding_rect(), builder, force_clip_creation); builder.maybe_create_clip(radii, *self.padding_rect(), force_clip_creation);
*self.padding_edge_clip_chain_id.borrow_mut() = maybe_clip; *self.padding_edge_clip_chain_id.borrow_mut() = maybe_clip;
maybe_clip maybe_clip
} }
@ -918,7 +1093,7 @@ impl<'a> BuilderForBoxFragment<'a> {
(self.fragment.border + self.fragment.padding).to_webrender(), (self.fragment.border + self.fragment.padding).to_webrender(),
); );
let maybe_clip = let maybe_clip =
create_clip_chain(radii, *self.content_rect(), builder, force_clip_creation); builder.maybe_create_clip(radii, *self.content_rect(), force_clip_creation);
*self.content_edge_clip_chain_id.borrow_mut() = maybe_clip; *self.content_edge_clip_chain_id.borrow_mut() = maybe_clip;
maybe_clip maybe_clip
} }
@ -1100,20 +1275,41 @@ impl<'a> BuilderForBoxFragment<'a> {
}, },
} }
}, },
Ok(ResolvedImage::Image(image_info)) => { Ok(ResolvedImage::Image { image, size }) => {
// FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution
let dppx = 1.0; let dppx = 1.0;
let intrinsic = NaturalSizes::from_width_and_height( let intrinsic =
image_info.size.width as f32 / dppx, NaturalSizes::from_width_and_height(size.width / dppx, size.height / dppx);
image_info.size.height as f32 / dppx, let layer = background::layout_layer(self, painter, builder, index, intrinsic);
); let image_wr_key = match image {
let Some(image_key) = image_info.key else { CachedImage::Raster(raster_image) => raster_image.id,
CachedImage::Vector(vector_image) => {
let scale = builder.context.shared_context().device_pixel_ratio().0;
let default_size: DeviceIntSize =
Size2D::new(size.width * scale, size.height * scale).to_i32();
let layer_size = layer.as_ref().map(|layer| {
Size2D::new(
layer.tile_size.width * scale,
layer.tile_size.height * scale,
)
.to_i32()
});
node.and_then(|node| {
let size = layer_size.unwrap_or(default_size);
builder
.context
.rasterize_vector_image(vector_image.id, size, node)
})
.and_then(|rasterized_image| rasterized_image.id)
},
};
let Some(image_key) = image_wr_key else {
continue; continue;
}; };
if let Some(layer) = if let Some(layer) = layer {
background::layout_layer(self, painter, builder, index, intrinsic)
{
if layer.repeat { if layer.repeat {
builder.wr().push_repeating_image( builder.wr().push_repeating_image(
&layer.common, &layer.common,
@ -1289,13 +1485,17 @@ impl<'a> BuilderForBoxFragment<'a> {
.resolve_image(node, &border.border_image_source) .resolve_image(node, &border.border_image_source)
{ {
Err(_) => return false, Err(_) => return false,
Ok(ResolvedImage::Image(image_info)) => { Ok(ResolvedImage::Image { image, size }) => {
let Some(key) = image_info.key else { let Some(image) = image.as_raster_image() else {
return false; return false;
}; };
width = image_info.size.width as f32; let Some(key) = image.id else {
height = image_info.size.height as f32; return false;
};
width = size.width;
height = size.height;
NinePatchBorderSource::Image(key, ImageRendering::Auto) NinePatchBorderSource::Image(key, ImageRendering::Auto)
}, },
Ok(ResolvedImage::Gradient(gradient)) => { Ok(ResolvedImage::Gradient(gradient)) => {
@ -1553,38 +1753,6 @@ fn offset_radii(mut radii: wr::BorderRadius, offset: f32) -> wr::BorderRadius {
radii radii
} }
fn create_clip_chain(
radii: wr::BorderRadius,
rect: units::LayoutRect,
builder: &mut DisplayListBuilder,
force_clip_creation: bool,
) -> Option<ClipChainId> {
if radii.is_zero() && !force_clip_creation {
return None;
}
let spatial_id = builder.current_scroll_node_id.spatial_id;
let parent_clip_chain_id = builder.current_clip_chain_id;
let new_clip_id = if radii.is_zero() {
builder.wr().define_clip_rect(spatial_id, rect)
} else {
builder.wr().define_clip_rounded_rect(
spatial_id,
wr::ComplexClipRegion {
rect,
radii,
mode: wr::ClipMode::Clip,
},
)
};
Some(
builder
.display_list
.define_clip_chain(parent_clip_chain_id, [new_clip_id]),
)
}
/// Resolve the WebRender border-image outset area from the style values. /// Resolve the WebRender border-image outset area from the style values.
fn resolve_border_image_outset( fn resolve_border_image_outset(
outset: BorderImageOutset, outset: BorderImageOutset,

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,12 @@
use std::any::Any; use std::any::Any;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc;
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
use base::id::{BrowsingContextId, PipelineId}; use base::id::{BrowsingContextId, PipelineId};
use html5ever::{local_name, ns}; use html5ever::{local_name, ns};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use pixels::Image; use net_traits::image_cache::Image;
use script::layout_dom::ServoLayoutNode; use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{ use script_layout_interface::wrapper_traits::{
LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
@ -126,15 +125,15 @@ impl LayoutBox {
.repair_style(context, node, new_style); .repair_style(context, node, new_style);
} }
}, },
LayoutBox::FlexLevel(flex_level_box) => { LayoutBox::FlexLevel(flex_level_box) => flex_level_box
flex_level_box.borrow_mut().repair_style(context, new_style) .borrow_mut()
}, .repair_style(context, node, new_style),
LayoutBox::TableLevelBox(table_level_box) => { LayoutBox::TableLevelBox(table_level_box) => {
table_level_box.repair_style(context, new_style) table_level_box.repair_style(context, node, new_style)
},
LayoutBox::TaffyItemBox(taffy_item_box) => {
taffy_item_box.borrow_mut().repair_style(context, new_style)
}, },
LayoutBox::TaffyItemBox(taffy_item_box) => taffy_item_box
.borrow_mut()
.repair_style(context, node, new_style),
} }
} }
} }
@ -197,7 +196,7 @@ impl Drop for BoxSlot<'_> {
pub(crate) trait NodeExt<'dom> { pub(crate) trait NodeExt<'dom> {
/// Returns the image if its loaded, and its size in image pixels /// Returns the image if its loaded, and its size in image pixels
/// adjusted for `image_density`. /// adjusted for `image_density`.
fn as_image(&self) -> Option<(Option<Arc<Image>>, PhysicalSize<f64>)>; fn as_image(&self) -> Option<(Option<Image>, PhysicalSize<f64>)>;
fn as_canvas(&self) -> Option<(CanvasInfo, PhysicalSize<f64>)>; fn as_canvas(&self) -> Option<(CanvasInfo, PhysicalSize<f64>)>;
fn as_iframe(&self) -> Option<(PipelineId, BrowsingContextId)>; fn as_iframe(&self) -> Option<(PipelineId, BrowsingContextId)>;
fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)>; fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)>;
@ -220,12 +219,15 @@ pub(crate) trait NodeExt<'dom> {
} }
impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> { impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> {
fn as_image(&self) -> Option<(Option<Arc<Image>>, PhysicalSize<f64>)> { fn as_image(&self) -> Option<(Option<Image>, PhysicalSize<f64>)> {
let node = self.to_threadsafe(); let node = self.to_threadsafe();
let (resource, metadata) = node.image_data()?; let (resource, metadata) = node.image_data()?;
let (width, height) = resource let (width, height) = resource
.as_ref() .as_ref()
.map(|image| (image.width, image.height)) .map(|image| {
let image_metadata = image.metadata();
(image_metadata.width, image_metadata.height)
})
.or_else(|| metadata.map(|metadata| (metadata.width, metadata.height))) .or_else(|| metadata.map(|metadata| (metadata.width, metadata.height)))
.unwrap_or((0, 0)); .unwrap_or((0, 0));
let (mut width, mut height) = (width as f64, height as f64); let (mut width, mut height) = (width as f64, height as f64);

View file

@ -59,8 +59,14 @@ impl<'dom> NodeAndStyleInfo<'dom> {
} }
} }
/// 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 { 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( pub(crate) fn pseudo(
@ -201,17 +207,7 @@ fn traverse_children_of<'dom>(
) { ) {
traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler); traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler);
let is_text_input_element = matches!( if parent_element.is_text_input() {
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( let info = NodeAndStyleInfo::new(
parent_element, parent_element,
parent_element.style(context.shared_context()), parent_element.style(context.shared_context()),
@ -229,9 +225,7 @@ fn traverse_children_of<'dom>(
} else { } else {
handler.handle_text(&info, node_text_content); handler.handle_text(&info, node_text_content);
} }
} } else {
if !is_text_input_element && !is_textarea_element {
for child in iter_child_nodes(parent_element) { for child in iter_child_nodes(parent_element) {
if child.is_text_node() { if child.is_text_node() {
let info = NodeAndStyleInfo::new(child, child.style(context.shared_context())); let info = NodeAndStyleInfo::new(child, child.style(context.shared_context()));

View file

@ -417,9 +417,8 @@ struct DesiredFlexFractionAndGrowOrShrinkFactor {
#[derive(Default)] #[derive(Default)]
struct FlexItemBoxInlineContentSizesInfo { struct FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size: Au, outer_flex_base_size: Au,
content_min_main_size: Au, outer_min_main_size: Au,
content_max_main_size: Option<Au>, outer_max_main_size: Option<Au>,
pbm_auto_is_zero: FlexRelativeVec2<Au>,
min_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor, min_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor,
max_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor, max_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor,
min_content_main_size_for_multiline_container: Au, min_content_main_size_for_multiline_container: Au,
@ -583,9 +582,8 @@ impl FlexContainer {
for FlexItemBoxInlineContentSizesInfo { for FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size, outer_flex_base_size,
content_min_main_size, outer_min_main_size,
content_max_main_size, outer_max_main_size,
pbm_auto_is_zero,
min_flex_factors, min_flex_factors,
max_flex_factors, max_flex_factors,
min_content_main_size_for_multiline_container, min_content_main_size_for_multiline_container,
@ -595,16 +593,13 @@ impl FlexContainer {
// > 4. Add each items flex base size to the product of its flex grow factor (scaled flex shrink // > 4. Add each items flex base size to the product of its flex grow factor (scaled flex shrink
// > factor, if shrinking) and the chosen flex fraction, then clamp that result by the max main size // > factor, if shrinking) and the chosen flex fraction, then clamp that result by the max main size
// > floored by the min main size. // > floored by the min main size.
let outer_min_main_size = *content_min_main_size + pbm_auto_is_zero.main;
let outer_max_main_size = content_max_main_size.map(|v| v + pbm_auto_is_zero.main);
// > 5. The flex containers max-content size is the largest sum (among all the lines) of the // > 5. The flex containers max-content size is the largest sum (among all the lines) of the
// > afore-calculated sizes of all items within a single line. // > afore-calculated sizes of all items within a single line.
container_max_content_size += (*outer_flex_base_size + container_max_content_size += (*outer_flex_base_size +
Au::from_f32_px( Au::from_f32_px(
max_flex_factors.flex_grow_or_shrink_factor * chosen_max_flex_fraction, max_flex_factors.flex_grow_or_shrink_factor * chosen_max_flex_fraction,
)) ))
.clamp_between_extremums(outer_min_main_size, outer_max_main_size); .clamp_between_extremums(*outer_min_main_size, *outer_max_main_size);
// > The min-content main size of a single-line flex container is calculated // > The min-content main size of a single-line flex container is calculated
// > identically to the max-content main size, except that the flex items // > identically to the max-content main size, except that the flex items
@ -621,7 +616,7 @@ impl FlexContainer {
Au::from_f32_px( Au::from_f32_px(
min_flex_factors.flex_grow_or_shrink_factor * chosen_min_flex_fraction, min_flex_factors.flex_grow_or_shrink_factor * chosen_min_flex_fraction,
)) ))
.clamp_between_extremums(outer_min_main_size, outer_max_main_size); .clamp_between_extremums(*outer_min_main_size, *outer_max_main_size);
} else { } else {
container_min_content_size container_min_content_size
.max_assign(*min_content_main_size_for_multiline_container); .max_assign(*min_content_main_size_for_multiline_container);
@ -1929,7 +1924,11 @@ impl FlexItem<'_> {
} }
} }
let lazy_block_size = if cross_axis_is_item_block_axis { let lazy_block_size = if !cross_axis_is_item_block_axis {
used_main_size.into()
} else if let Some(cross_size) = used_cross_size_override {
cross_size.into()
} else {
// This means that an auto size with stretch alignment will behave different than // This means that an auto size with stretch alignment will behave different than
// a stretch size. That's not what the spec says, but matches other browsers. // a stretch size. That's not what the spec says, but matches other browsers.
// To be discussed in https://github.com/w3c/csswg-drafts/issues/11784. // To be discussed in https://github.com/w3c/csswg-drafts/issues/11784.
@ -1946,8 +1945,6 @@ impl FlexItem<'_> {
stretch_size, stretch_size,
self.is_table(), self.is_table(),
) )
} else {
used_main_size.into()
}; };
let layout = non_replaced.layout( let layout = non_replaced.layout(
@ -2456,6 +2453,8 @@ impl FlexItemBox {
}; };
let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main; let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main;
let outer_min_main_size = content_min_main_size + pbm_auto_is_zero.main;
let outer_max_main_size = content_max_main_size.map(|v| v + pbm_auto_is_zero.main);
let max_flex_factors = self.desired_flex_factors_for_preferred_width( let max_flex_factors = self.desired_flex_factors_for_preferred_width(
content_contribution_sizes.max_content, content_contribution_sizes.max_content,
flex_base_size, flex_base_size,
@ -2481,20 +2480,19 @@ impl FlexItemBox {
content_contribution_sizes.min_content; content_contribution_sizes.min_content;
let style_position = &self.style().get_position(); let style_position = &self.style().get_position();
if style_position.flex_grow.is_zero() { if style_position.flex_grow.is_zero() {
min_content_main_size_for_multiline_container.min_assign(flex_base_size); min_content_main_size_for_multiline_container.min_assign(outer_flex_base_size);
} }
if style_position.flex_shrink.is_zero() { if style_position.flex_shrink.is_zero() {
min_content_main_size_for_multiline_container.max_assign(flex_base_size); min_content_main_size_for_multiline_container.max_assign(outer_flex_base_size);
} }
min_content_main_size_for_multiline_container = min_content_main_size_for_multiline_container =
min_content_main_size_for_multiline_container min_content_main_size_for_multiline_container
.clamp_between_extremums(content_min_main_size, content_max_main_size); .clamp_between_extremums(outer_min_main_size, outer_max_main_size);
FlexItemBoxInlineContentSizesInfo { FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size, outer_flex_base_size,
content_min_main_size, outer_min_main_size,
content_max_main_size, outer_max_main_size,
pbm_auto_is_zero,
min_flex_factors, min_flex_factors,
max_flex_factors, max_flex_factors,
min_content_main_size_for_multiline_container, min_content_main_size_for_multiline_container,

View file

@ -4,6 +4,7 @@
use geom::{FlexAxis, MainStartCrossStart}; use geom::{FlexAxis, MainStartCrossStart};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
use style::context::SharedStyleContext; use style::context::SharedStyleContext;
use style::logical_geometry::WritingMode; use style::logical_geometry::WritingMode;
@ -104,8 +105,7 @@ impl FlexContainer {
contents: NonReplacedContents, contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData, propagated_data: PropagatedBoxTreeData,
) -> Self { ) -> Self {
let mut builder = let mut builder = ModernContainerBuilder::new(context, info, propagated_data);
ModernContainerBuilder::new(context, info, propagated_data.union(&info.style));
contents.traverse(context, info, &mut builder); contents.traverse(context, info, &mut builder);
let items = builder.finish(); let items = builder.finish();
@ -154,16 +154,17 @@ impl FlexLevelBox {
pub(crate) fn repair_style( pub(crate) fn repair_style(
&mut self, &mut self,
context: &SharedStyleContext, context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &ServoArc<ComputedValues>, new_style: &ServoArc<ComputedValues>,
) { ) {
match self { match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box FlexLevelBox::FlexItem(flex_item_box) => flex_item_box
.independent_formatting_context .independent_formatting_context
.repair_style(context, new_style), .repair_style(context, node, new_style),
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut() .borrow_mut()
.context .context
.repair_style(context, new_style), .repair_style(context, node, new_style),
} }
} }

View file

@ -199,7 +199,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
context, context,
info, info,
block_level_boxes: Vec::new(), block_level_boxes: Vec::new(),
propagated_data: propagated_data.union(&info.style), propagated_data,
have_already_seen_first_line_for_text_indent: false, have_already_seen_first_line_for_text_indent: false,
anonymous_box_info: None, anonymous_box_info: None,
anonymous_table_content: Vec::new(), anonymous_table_content: Vec::new(),
@ -228,7 +228,6 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
fn finish_ongoing_inline_formatting_context(&mut self) -> Option<InlineFormattingContext> { fn finish_ongoing_inline_formatting_context(&mut self) -> Option<InlineFormattingContext> {
self.inline_formatting_context_builder.take()?.finish( self.inline_formatting_context_builder.take()?.finish(
self.context, self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent, !self.have_already_seen_first_line_for_text_indent,
self.info.is_single_line_text_input(), self.info.is_single_line_text_input(),
self.info.style.writing_mode.to_bidi_level(), self.info.style.writing_mode.to_bidi_level(),
@ -280,16 +279,6 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
// creation of an inline table. It requires the parent to be an inline box. // creation of an inline table. It requires the parent to be an inline box.
let inline_table = self.currently_processing_inline_box(); let inline_table = self.currently_processing_inline_box();
// Text decorations are not propagated to atomic inline-level descendants.
// From https://drafts.csswg.org/css2/#lining-striking-props:
// > Note that text decorations are not propagated to floating and absolutely
// > positioned descendants, nor to the contents of atomic inline-level descendants
// > such as inline blocks and inline tables.
let propagated_data = match inline_table {
true => self.propagated_data.without_text_decorations(),
false => self.propagated_data,
};
let contents: Vec<AnonymousTableContent<'dom>> = let contents: Vec<AnonymousTableContent<'dom>> =
self.anonymous_table_content.drain(..).collect(); self.anonymous_table_content.drain(..).collect();
let last_text = match contents.last() { let last_text = match contents.last() {
@ -298,7 +287,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
}; };
let (table_info, ifc) = let (table_info, ifc) =
Table::construct_anonymous(self.context, self.info, contents, propagated_data); Table::construct_anonymous(self.context, self.info, contents, self.propagated_data);
if inline_table { if inline_table {
self.ensure_inline_formatting_context_builder() self.ensure_inline_formatting_context_builder()
@ -315,7 +304,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
info: table_info, info: table_info,
box_slot: BoxSlot::dummy(), box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::AnonymousTable { table_block }, kind: BlockLevelCreator::AnonymousTable { table_block },
propagated_data, propagated_data: self.propagated_data,
}); });
} }
@ -464,7 +453,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
contents, contents,
list_item_style, list_item_style,
}, },
propagated_data: self.propagated_data.without_text_decorations(), propagated_data: self.propagated_data,
}); });
} }
@ -480,15 +469,14 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
else { else {
// If this inline element is an atomic, handle it and return. // If this inline element is an atomic, handle it and return.
let context = self.context; let context = self.context;
let propagaged_data = self.propagated_data.without_text_decorations(); let propagated_data = self.propagated_data;
let atomic = self.ensure_inline_formatting_context_builder().push_atomic( let atomic = self.ensure_inline_formatting_context_builder().push_atomic(
IndependentFormattingContext::construct( IndependentFormattingContext::construct(
context, context,
info, info,
display_inside, display_inside,
contents, contents,
// Text decorations are not propagated to atomic inline-level descendants. propagated_data,
propagaged_data,
), ),
); );
box_slot.set(LayoutBox::InlineLevel(vec![atomic])); box_slot.set(LayoutBox::InlineLevel(vec![atomic]));
@ -550,7 +538,6 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
.and_then(|builder| { .and_then(|builder| {
builder.split_around_block_and_finish( builder.split_around_block_and_finish(
self.context, self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent, !self.have_already_seen_first_line_for_text_indent,
self.info.style.writing_mode.to_bidi_level(), self.info.style.writing_mode.to_bidi_level(),
) )
@ -631,7 +618,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
info: info.clone(), info: info.clone(),
box_slot, box_slot,
kind, kind,
propagated_data: self.propagated_data.without_text_decorations(), propagated_data: self.propagated_data,
}); });
} }
@ -664,7 +651,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
info: info.clone(), info: info.clone(),
box_slot, box_slot,
kind, kind,
propagated_data: self.propagated_data.without_text_decorations(), propagated_data: self.propagated_data,
}); });
} }
@ -754,7 +741,7 @@ impl BlockLevelJob<'_> {
context, context,
info, info,
contents, contents,
self.propagated_data.without_text_decorations(), self.propagated_data,
false, /* is_list_item */ false, /* is_list_item */
); );
ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker { ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker {

View file

@ -897,8 +897,7 @@ impl FloatBox {
info, info,
display_inside, display_inside,
contents, contents,
// Text decorations are not propagated to any out-of-flow descendants propagated_data,
propagated_data.without_text_decorations(),
), ),
} }
} }

View file

@ -16,7 +16,6 @@ use super::{
InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem, InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
SharedInlineStyles, SharedInlineStyles,
}; };
use crate::PropagatedBoxTreeData;
use crate::cell::ArcRefCell; use crate::cell::ArcRefCell;
use crate::context::LayoutContext; use crate::context::LayoutContext;
use crate::dom_traversal::NodeAndStyleInfo; use crate::dom_traversal::NodeAndStyleInfo;
@ -31,7 +30,7 @@ pub(crate) struct InlineFormattingContextBuilder {
/// inline box stack, and importantly, one for every `display: contents` element that we are /// 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 /// currently processing. Normally `display: contents` elements don't affect the structure of
/// the [`InlineFormattingContext`], but the styles they provide do style their children. /// the [`InlineFormattingContext`], but the styles they provide do style their children.
shared_inline_styles_stack: Vec<SharedInlineStyles>, pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
/// The collection of text strings that make up this [`InlineFormattingContext`] under /// The collection of text strings that make up this [`InlineFormattingContext`] under
/// construction. /// construction.
@ -344,7 +343,6 @@ impl InlineFormattingContextBuilder {
pub(crate) fn split_around_block_and_finish( pub(crate) fn split_around_block_and_finish(
&mut self, &mut self,
layout_context: &LayoutContext, layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool, has_first_formatted_line: bool,
default_bidi_level: Level, default_bidi_level: Level,
) -> Option<InlineFormattingContext> { ) -> Option<InlineFormattingContext> {
@ -386,7 +384,6 @@ impl InlineFormattingContextBuilder {
inline_builder_from_before_split.finish( inline_builder_from_before_split.finish(
layout_context, layout_context,
propagated_data,
has_first_formatted_line, has_first_formatted_line,
/* is_single_line_text_input = */ false, /* is_single_line_text_input = */ false,
default_bidi_level, default_bidi_level,
@ -397,7 +394,6 @@ impl InlineFormattingContextBuilder {
pub(crate) fn finish( pub(crate) fn finish(
self, self,
layout_context: &LayoutContext, layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool, has_first_formatted_line: bool,
is_single_line_text_input: bool, is_single_line_text_input: bool,
default_bidi_level: Level, default_bidi_level: Level,
@ -410,7 +406,6 @@ impl InlineFormattingContextBuilder {
Some(InlineFormattingContext::new_with_builder( Some(InlineFormattingContext::new_with_builder(
self, self,
layout_context, layout_context,
propagated_data,
has_first_formatted_line, has_first_formatted_line,
is_single_line_text_input, is_single_line_text_input,
default_bidi_level, default_bidi_level,

View file

@ -256,13 +256,7 @@ impl InlineBoxContainerState {
} }
Self { Self {
base: InlineContainerState::new( base: InlineContainerState::new(style, flags, Some(parent_container), font_metrics),
style,
flags,
Some(parent_container),
parent_container.text_decoration_line,
font_metrics,
),
identifier: inline_box.identifier, identifier: inline_box.identifier,
base_fragment_info: inline_box.base.base_fragment_info, base_fragment_info: inline_box.base.base_fragment_info,
pbm, pbm,

View file

@ -15,7 +15,6 @@ use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword};
use style::values::generics::font::LineHeight; use style::values::generics::font::LineHeight;
use style::values::specified::align::AlignFlags; use style::values::specified::align::AlignFlags;
use style::values::specified::box_::DisplayOutside; use style::values::specified::box_::DisplayOutside;
use style::values::specified::text::TextDecorationLine;
use unicode_bidi::{BidiInfo, Level}; use unicode_bidi::{BidiInfo, Level};
use webrender_api::FontInstanceKey; use webrender_api::FontInstanceKey;
@ -572,7 +571,6 @@ impl LineItemLayout<'_, '_> {
font_metrics: text_item.font_metrics, font_metrics: text_item.font_metrics,
font_key: text_item.font_key, font_key: text_item.font_key,
glyphs: text_item.text, glyphs: text_item.text,
text_decoration_line: text_item.text_decoration_line,
justification_adjustment: self.justification_adjustment, justification_adjustment: self.justification_adjustment,
selection_range: text_item.selection_range, selection_range: text_item.selection_range,
})), })),
@ -765,7 +763,6 @@ pub(super) struct TextRunLineItem {
pub text: Vec<std::sync::Arc<GlyphStore>>, pub text: Vec<std::sync::Arc<GlyphStore>>,
pub font_metrics: FontMetrics, pub font_metrics: FontMetrics,
pub font_key: FontInstanceKey, pub font_key: FontInstanceKey,
pub text_decoration_line: TextDecorationLine,
/// The BiDi level of this [`TextRunLineItem`] to enable reordering. /// The BiDi level of this [`TextRunLineItem`] to enable reordering.
pub bidi_level: Level, pub bidi_level: Level,
pub selection_range: Option<Range<ByteIndex>>, pub selection_range: Option<Range<ByteIndex>>,

View file

@ -91,6 +91,7 @@ use line_breaker::LineBreaker;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use range::Range; use range::Range;
use script::layout_dom::ServoLayoutNode; use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode};
use servo_arc::Arc; use servo_arc::Arc;
use style::Zero; use style::Zero;
use style::computed_values::text_wrap_mode::T as TextWrapMode; use style::computed_values::text_wrap_mode::T as TextWrapMode;
@ -102,7 +103,7 @@ use style::properties::style_structs::InheritedText;
use style::values::generics::box_::VerticalAlignKeyword; use style::values::generics::box_::VerticalAlignKeyword;
use style::values::generics::font::LineHeight; use style::values::generics::font::LineHeight;
use style::values::specified::box_::BaselineSource; use style::values::specified::box_::BaselineSource;
use style::values::specified::text::{TextAlignKeyword, TextDecorationLine}; use style::values::specified::text::TextAlignKeyword;
use style::values::specified::{TextAlignLast, TextJustify}; use style::values::specified::{TextAlignLast, TextJustify};
use text_run::{ use text_run::{
TextRun, XI_LINE_BREAKING_CLASS_GL, XI_LINE_BREAKING_CLASS_WJ, XI_LINE_BREAKING_CLASS_ZWJ, TextRun, XI_LINE_BREAKING_CLASS_GL, XI_LINE_BREAKING_CLASS_WJ, XI_LINE_BREAKING_CLASS_ZWJ,
@ -133,7 +134,7 @@ use crate::geom::{LogicalRect, LogicalVec2, ToLogical};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult}; use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult};
use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin}; use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin};
use crate::{ConstraintSpace, ContainingBlock, PropagatedBoxTreeData, SharedStyle}; use crate::{ConstraintSpace, ContainingBlock, SharedStyle};
// From gfxFontConstants.h in Firefox. // From gfxFontConstants.h in Firefox.
static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20; static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20;
@ -158,7 +159,9 @@ pub(crate) struct InlineFormattingContext {
/// context in order to avoid duplicating this information. /// context in order to avoid duplicating this information.
pub font_metrics: Vec<FontKeyAndMetrics>, pub font_metrics: Vec<FontKeyAndMetrics>,
pub(super) text_decoration_line: TextDecorationLine, /// The [`SharedInlineStyles`] for the root of this [`InlineFormattingContext`] that are used to
/// share styles with all [`TextRun`] children.
pub(super) shared_inline_styles: SharedInlineStyles,
/// Whether this IFC contains the 1st formatted line of an element: /// Whether this IFC contains the 1st formatted line of an element:
/// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>. /// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>.
@ -237,12 +240,14 @@ impl InlineItem {
InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => positioned_box InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => positioned_box
.borrow_mut() .borrow_mut()
.context .context
.repair_style(context, new_style), .repair_style(context, node, new_style),
InlineItem::OutOfFlowFloatBox(float_box) => float_box InlineItem::OutOfFlowFloatBox(float_box) => float_box
.borrow_mut() .borrow_mut()
.contents .contents
.repair_style(context, new_style), .repair_style(context, node, new_style),
InlineItem::Atomic(atomic, ..) => atomic.borrow_mut().repair_style(context, new_style), InlineItem::Atomic(atomic, ..) => {
atomic.borrow_mut().repair_style(context, node, new_style)
},
} }
} }
@ -621,12 +626,6 @@ pub(super) struct InlineContainerState {
/// this inline box on the current line OR any previous line. /// this inline box on the current line OR any previous line.
has_content: RefCell<bool>, has_content: RefCell<bool>,
/// Indicates whether this nesting level have text decorations in effect.
/// From <https://drafts.csswg.org/css-text-decor/#line-decoration>
// "When specified on or propagated to a block container that establishes
// an IFC..."
text_decoration_line: TextDecorationLine,
/// The block size contribution of this container's default font ie the size of the /// The block size contribution of this container's default font ie the size of the
/// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`] /// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`]
/// depends on the line-height quirk described in /// depends on the line-height quirk described in
@ -1454,7 +1453,6 @@ impl InlineFormattingContextLayout<'_> {
inline_styles: text_run.inline_styles.clone(), inline_styles: text_run.inline_styles.clone(),
font_metrics, font_metrics,
font_key: ifc_font_info.key, font_key: ifc_font_info.key,
text_decoration_line: self.current_inline_container_state().text_decoration_line,
bidi_level, bidi_level,
selection_range, selection_range,
}, },
@ -1648,7 +1646,6 @@ impl InlineFormattingContext {
pub(super) fn new_with_builder( pub(super) fn new_with_builder(
builder: InlineFormattingContextBuilder, builder: InlineFormattingContextBuilder,
layout_context: &LayoutContext, layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool, has_first_formatted_line: bool,
is_single_line_text_input: bool, is_single_line_text_input: bool,
starting_bidi_level: Level, starting_bidi_level: Level,
@ -1699,7 +1696,11 @@ impl InlineFormattingContext {
inline_items: builder.inline_items, inline_items: builder.inline_items,
inline_boxes: builder.inline_boxes, inline_boxes: builder.inline_boxes,
font_metrics, font_metrics,
text_decoration_line: propagated_data.text_decoration, shared_inline_styles: builder
.shared_inline_styles_stack
.last()
.expect("Should have at least one SharedInlineStyle for the root of an IFC")
.clone(),
has_first_formatted_line, has_first_formatted_line,
contains_floats: builder.contains_floats, contains_floats: builder.contains_floats,
is_single_line_text_input, is_single_line_text_input,
@ -1707,6 +1708,11 @@ impl InlineFormattingContext {
} }
} }
pub(crate) fn repair_style(&self, node: &ServoLayoutNode, new_style: &Arc<ComputedValues>) {
*self.shared_inline_styles.style.borrow_mut() = new_style.clone();
*self.shared_inline_styles.selected.borrow_mut() = node.to_threadsafe().selected_style();
}
pub(super) fn layout( pub(super) fn layout(
&self, &self,
layout_context: &LayoutContext, layout_context: &LayoutContext,
@ -1764,7 +1770,6 @@ impl InlineFormattingContext {
style.to_arc(), style.to_arc(),
inline_container_state_flags, inline_container_state_flags,
None, /* parent_container */ None, /* parent_container */
self.text_decoration_line,
default_font_metrics.as_ref(), default_font_metrics.as_ref(),
), ),
inline_box_state_stack: Vec::new(), inline_box_state_stack: Vec::new(),
@ -1862,10 +1867,8 @@ impl InlineContainerState {
style: Arc<ComputedValues>, style: Arc<ComputedValues>,
flags: InlineContainerStateFlags, flags: InlineContainerStateFlags,
parent_container: Option<&InlineContainerState>, parent_container: Option<&InlineContainerState>,
parent_text_decoration_line: TextDecorationLine,
font_metrics: Option<&FontMetrics>, font_metrics: Option<&FontMetrics>,
) -> Self { ) -> Self {
let text_decoration_line = parent_text_decoration_line | style.clone_text_decoration_line();
let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty); let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty);
let line_height = line_height( let line_height = line_height(
&style, &style,
@ -1902,7 +1905,6 @@ impl InlineContainerState {
style, style,
flags, flags,
has_content: RefCell::new(false), has_content: RefCell::new(false),
text_decoration_line,
nested_strut_block_sizes: nested_block_sizes, nested_strut_block_sizes: nested_block_sizes,
strut_block_sizes, strut_block_sizes,
baseline_offset, baseline_offset,

View file

@ -78,6 +78,15 @@ impl BlockContainer {
BlockContainer::InlineFormattingContext(context) => context.contains_floats, BlockContainer::InlineFormattingContext(context) => context.contains_floats,
} }
} }
pub(crate) fn repair_style(&mut self, node: &ServoLayoutNode, new_style: &Arc<ComputedValues>) {
match self {
BlockContainer::BlockLevelBoxes(..) => {},
BlockContainer::InlineFormattingContext(inline_formatting_context) => {
inline_formatting_context.repair_style(node, new_style)
},
}
}
} }
#[derive(Debug, MallocSizeOf)] #[derive(Debug, MallocSizeOf)]
@ -106,20 +115,21 @@ impl BlockLevelBox {
match self { match self {
BlockLevelBox::Independent(independent_formatting_context) => { BlockLevelBox::Independent(independent_formatting_context) => {
independent_formatting_context.repair_style(context, new_style) independent_formatting_context.repair_style(context, node, new_style)
}, },
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut() .borrow_mut()
.context .context
.repair_style(context, new_style), .repair_style(context, node, new_style),
BlockLevelBox::OutOfFlowFloatBox(float_box) => { BlockLevelBox::OutOfFlowFloatBox(float_box) => {
float_box.contents.repair_style(context, new_style) float_box.contents.repair_style(context, node, new_style)
}, },
BlockLevelBox::OutsideMarker(outside_marker) => { BlockLevelBox::OutsideMarker(outside_marker) => {
outside_marker.repair_style(context, node, new_style) outside_marker.repair_style(context, node, new_style)
}, },
BlockLevelBox::SameFormattingContextBlock { base, .. } => { BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
base.repair_style(new_style); base.repair_style(new_style);
contents.repair_style(node, new_style);
}, },
} }
} }
@ -477,6 +487,10 @@ impl BlockFormattingContext {
pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> { pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
LayoutStyle::Default(&base.style) LayoutStyle::Default(&base.style)
} }
pub(crate) fn repair_style(&mut self, node: &ServoLayoutNode, new_style: &Arc<ComputedValues>) {
self.contents.repair_style(node, new_style);
}
} }
/// Finds the min/max-content inline size of the block-level children of a block container. /// Finds the min/max-content inline size of the block-level children of a block container.

View file

@ -5,6 +5,8 @@
use app_units::Au; use app_units::Au;
use atomic_refcell::AtomicRef; use atomic_refcell::AtomicRef;
use compositing_traits::display_list::AxesScrollSensitivity; use compositing_traits::display_list::AxesScrollSensitivity;
use euclid::Rect;
use euclid::default::Size2D as UntypedSize2D;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode; use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{ use script_layout_interface::wrapper_traits::{
@ -27,7 +29,7 @@ use crate::flow::inline::InlineItem;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext; use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentTree; use crate::fragment_tree::FragmentTree;
use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize}; use crate::geom::{LogicalVec2, PhysicalSize};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContents; use crate::replaced::ReplacedContents;
use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
@ -314,7 +316,7 @@ fn construct_for_root_element(
let contents = ReplacedContents::for_element(root_element, context) let contents = ReplacedContents::for_element(root_element, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced); .map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let propagated_data = PropagatedBoxTreeData::default().union(&info.style); let propagated_data = PropagatedBoxTreeData::default();
let root_box = if box_style.position.is_absolutely_positioned() { let root_box = if box_style.position.is_absolutely_positioned() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new( BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents), AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
@ -348,7 +350,7 @@ impl BoxTree {
pub fn layout( pub fn layout(
&self, &self,
layout_context: &LayoutContext, layout_context: &LayoutContext,
viewport: euclid::Size2D<f32, CSSPixel>, viewport: UntypedSize2D<Au>,
) -> FragmentTree { ) -> FragmentTree {
let style = layout_context let style = layout_context
.style_context .style_context
@ -358,13 +360,8 @@ impl BoxTree {
// FIXME: use the documents mode: // FIXME: use the documents mode:
// https://drafts.csswg.org/css-writing-modes/#principal-flow // https://drafts.csswg.org/css-writing-modes/#principal-flow
let physical_containing_block = PhysicalRect::new( let physical_containing_block: Rect<Au, CSSPixel> =
PhysicalPoint::zero(), PhysicalSize::from_untyped(viewport).into();
PhysicalSize::new(
Au::from_f32_px(viewport.width),
Au::from_f32_px(viewport.height),
),
);
let initial_containing_block = DefiniteContainingBlock { let initial_containing_block = DefiniteContainingBlock {
size: LogicalVec2 { size: LogicalVec2 {
inline: physical_containing_block.size.width, inline: physical_containing_block.size.width,
@ -395,31 +392,9 @@ impl BoxTree {
&mut root_fragments, &mut root_fragments,
); );
let scrollable_overflow = root_fragments
.iter()
.fold(PhysicalRect::zero(), |acc, child| {
let child_overflow = child.scrollable_overflow_for_parent();
// https://drafts.csswg.org/css-overflow/#scrolling-direction
// We want to clip scrollable overflow on box-start and inline-start
// sides of the scroll container.
//
// FIXME(mrobinson, bug 25564): This should take into account writing
// mode.
let child_overflow = PhysicalRect::new(
euclid::Point2D::zero(),
euclid::Size2D::new(
child_overflow.size.width + child_overflow.origin.x,
child_overflow.size.height + child_overflow.origin.y,
),
);
acc.union(&child_overflow)
});
FragmentTree::new( FragmentTree::new(
layout_context, layout_context,
root_fragments, root_fragments,
scrollable_overflow,
physical_containing_block, physical_containing_block,
self.viewport_scroll_sensitivity, self.viewport_scroll_sensitivity,
) )

View file

@ -4,7 +4,7 @@
use app_units::Au; use app_units::Au;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutElement; use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_arc::Arc; use servo_arc::Arc;
use style::context::SharedStyleContext; use style::context::SharedStyleContext;
use style::properties::ComputedValues; use style::properties::ComputedValues;
@ -223,12 +223,13 @@ impl IndependentFormattingContext {
pub(crate) fn repair_style( pub(crate) fn repair_style(
&mut self, &mut self,
context: &SharedStyleContext, context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>, new_style: &Arc<ComputedValues>,
) { ) {
self.base.repair_style(new_style); self.base.repair_style(new_style);
match &mut self.contents { match &mut self.contents {
IndependentFormattingContextContents::NonReplaced(content) => { IndependentFormattingContextContents::NonReplaced(content) => {
content.repair_style(context, new_style); content.repair_style(context, node, new_style);
}, },
IndependentFormattingContextContents::Replaced(..) => {}, IndependentFormattingContextContents::Replaced(..) => {},
} }
@ -356,9 +357,16 @@ impl IndependentNonReplacedContents {
matches!(self, Self::Table(_)) matches!(self, Self::Table(_))
} }
fn repair_style(&mut self, context: &SharedStyleContext, new_style: &Arc<ComputedValues>) { fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
match self { match self {
IndependentNonReplacedContents::Flow(..) => {}, IndependentNonReplacedContents::Flow(block_formatting_context) => {
block_formatting_context.repair_style(node, new_style);
},
IndependentNonReplacedContents::Flex(flex_container) => { IndependentNonReplacedContents::Flex(flex_container) => {
flex_container.repair_style(new_style) flex_container.repair_style(new_style)
}, },

View file

@ -89,10 +89,10 @@ pub(crate) struct BoxFragment {
block_margins_collapsed_with_children: Option<Box<CollapsedBlockMargins>>, block_margins_collapsed_with_children: Option<Box<CollapsedBlockMargins>>,
/// The scrollable overflow of this box fragment. /// The scrollable overflow of this box fragment.
pub scrollable_overflow_from_children: PhysicalRect<Au>, scrollable_overflow: Option<PhysicalRect<Au>>,
/// The resolved box insets if this box is `position: sticky`. These are calculated /// The resolved box insets if this box is `position: sticky`. These are calculated
/// during stacking context tree construction because they rely on the size of the /// during `StackingContextTree` construction because they rely on the size of the
/// scroll container. /// scroll container.
pub(crate) resolved_sticky_insets: AtomicRefCell<Option<PhysicalSides<AuOrAuto>>>, pub(crate) resolved_sticky_insets: AtomicRefCell<Option<PhysicalSides<AuOrAuto>>>,
@ -114,11 +114,6 @@ impl BoxFragment {
margin: PhysicalSides<Au>, margin: PhysicalSides<Au>,
clearance: Option<Au>, clearance: Option<Au>,
) -> BoxFragment { ) -> BoxFragment {
let scrollable_overflow_from_children =
children.iter().fold(PhysicalRect::zero(), |acc, child| {
acc.union(&child.scrollable_overflow_for_parent())
});
BoxFragment { BoxFragment {
base: base_fragment_info.into(), base: base_fragment_info.into(),
style, style,
@ -131,7 +126,7 @@ impl BoxFragment {
clearance, clearance,
baselines: Baselines::default(), baselines: Baselines::default(),
block_margins_collapsed_with_children: None, block_margins_collapsed_with_children: None,
scrollable_overflow_from_children, scrollable_overflow: None,
resolved_sticky_insets: AtomicRefCell::default(), resolved_sticky_insets: AtomicRefCell::default(),
background_mode: BackgroundMode::Normal, background_mode: BackgroundMode::Normal,
specific_layout_info: None, specific_layout_info: None,
@ -203,13 +198,23 @@ impl BoxFragment {
/// Get the scrollable overflow for this [`BoxFragment`] relative to its /// Get the scrollable overflow for this [`BoxFragment`] relative to its
/// containing block. /// containing block.
pub fn scrollable_overflow(&self) -> PhysicalRect<Au> { pub fn scrollable_overflow(&self) -> PhysicalRect<Au> {
self.scrollable_overflow
.expect("Should only call `scrollable_overflow()` after calculating overflow")
}
pub(crate) fn calculate_scrollable_overflow(&mut self) {
let scrollable_overflow_from_children = self
.children
.iter()
.fold(PhysicalRect::zero(), |acc, child| {
acc.union(&child.calculate_scrollable_overflow_for_parent())
});
let physical_padding_rect = self.padding_rect(); let physical_padding_rect = self.padding_rect();
let content_origin = self.content_rect.origin.to_vector(); let content_origin = self.content_rect.origin.to_vector();
physical_padding_rect.union( self.scrollable_overflow = Some(
&self physical_padding_rect
.scrollable_overflow_from_children .union(&scrollable_overflow_from_children.translate(content_origin)),
.translate(content_origin), );
)
} }
pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) { pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) {
@ -275,7 +280,12 @@ impl BoxFragment {
tree.end_level(); tree.end_level();
} }
pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
// TODO: Properly handle absolutely positioned fragments.
if self.style.get_box().position.is_absolutely_positioned() {
return PhysicalRect::zero();
}
let mut overflow = self.border_rect(); let mut overflow = self.border_rect();
if !self.style.establishes_scroll_container(self.base.flags) { if !self.style.establishes_scroll_container(self.base.flags) {
// https://www.w3.org/TR/css-overflow-3/#scrollable // https://www.w3.org/TR/css-overflow-3/#scrollable
@ -328,7 +338,7 @@ impl BoxFragment {
/// ///
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
/// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block. /// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block.
pub fn clip_unreachable_scrollable_overflow_region( pub(crate) fn clip_unreachable_scrollable_overflow_region(
&self, &self,
scrollable_overflow: PhysicalRect<Au>, scrollable_overflow: PhysicalRect<Au>,
clipping_rect: PhysicalRect<Au>, clipping_rect: PhysicalRect<Au>,
@ -362,7 +372,7 @@ impl BoxFragment {
/// ///
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
/// This will coincides with the scrollport if the fragment is a scroll container. /// This will coincides with the scrollport if the fragment is a scroll container.
pub fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> { pub(crate) fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> {
self.clip_unreachable_scrollable_overflow_region( self.clip_unreachable_scrollable_overflow_region(
self.scrollable_overflow(), self.scrollable_overflow(),
self.padding_rect(), self.padding_rect(),
@ -421,9 +431,7 @@ impl BoxFragment {
return convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left)); return convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left));
} }
debug_assert!( debug_assert!(position.is_absolutely_positioned());
position == ComputedPosition::Fixed || position == ComputedPosition::Absolute
);
let margin_rect = self.margin_rect(); let margin_rect = self.margin_rect();
let (top, bottom) = match (&insets.top, &insets.bottom) { let (top, bottom) = match (&insets.top, &insets.bottom) {

View file

@ -14,7 +14,6 @@ use range::Range as ServoRange;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
use style::Zero; use style::Zero;
use style::properties::ComputedValues; use style::properties::ComputedValues;
use style::values::specified::text::TextDecorationLine;
use webrender_api::{FontInstanceKey, ImageKey}; use webrender_api::{FontInstanceKey, ImageKey};
use super::{ use super::{
@ -72,9 +71,6 @@ pub(crate) struct TextFragment {
#[conditional_malloc_size_of] #[conditional_malloc_size_of]
pub glyphs: Vec<Arc<GlyphStore>>, pub glyphs: Vec<Arc<GlyphStore>>,
/// A flag that represents the _used_ value of the text-decoration property.
pub text_decoration_line: TextDecorationLine,
/// Extra space to add for each justification opportunity. /// Extra space to add for each justification opportunity.
pub justification_adjustment: Au, pub justification_adjustment: Au,
pub selection_range: Option<ServoRange<ByteIndex>>, pub selection_range: Option<ServoRange<ByteIndex>>,
@ -187,19 +183,36 @@ impl Fragment {
} }
} }
pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
match self { match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => { Fragment::Box(fragment) | Fragment::Float(fragment) => {
fragment.borrow().scrollable_overflow_for_parent() return fragment.borrow().scrollable_overflow_for_parent();
}, },
Fragment::AbsoluteOrFixedPositioned(_) => PhysicalRect::zero(), Fragment::AbsoluteOrFixedPositioned(_) => PhysicalRect::zero(),
Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow, Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow_for_parent(),
Fragment::Text(fragment) => fragment.borrow().rect, Fragment::Text(fragment) => fragment.borrow().rect,
Fragment::Image(fragment) => fragment.borrow().rect, Fragment::Image(fragment) => fragment.borrow().rect,
Fragment::IFrame(fragment) => fragment.borrow().rect, Fragment::IFrame(fragment) => fragment.borrow().rect,
} }
} }
pub(crate) fn calculate_scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
self.calculate_scrollable_overflow();
self.scrollable_overflow_for_parent()
}
pub(crate) fn calculate_scrollable_overflow(&self) {
match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => {
fragment.borrow_mut().calculate_scrollable_overflow()
},
Fragment::Positioning(fragment) => {
fragment.borrow_mut().calculate_scrollable_overflow()
},
_ => {},
}
}
pub(crate) fn cumulative_border_box_rect(&self) -> Option<PhysicalRect<Au>> { pub(crate) fn cumulative_border_box_rect(&self) -> Option<PhysicalRect<Au>> {
match self { match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => { Fragment::Box(fragment) | Fragment::Float(fragment) => {

View file

@ -2,19 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use app_units::Au; use app_units::Au;
use base::print_tree::PrintTree; use base::print_tree::PrintTree;
use compositing_traits::display_list::AxesScrollSensitivity; use compositing_traits::display_list::AxesScrollSensitivity;
use euclid::default::Size2D;
use fxhash::FxHashSet; use fxhash::FxHashSet;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use style::animation::AnimationSetKey; use style::animation::AnimationSetKey;
use webrender_api::units;
use super::{BoxFragment, ContainingBlockManager, Fragment}; use super::{BoxFragment, ContainingBlockManager, Fragment};
use crate::ArcRefCell; use crate::ArcRefCell;
use crate::context::LayoutContext; use crate::context::LayoutContext;
use crate::display_list::StackingContext;
use crate::geom::PhysicalRect; use crate::geom::PhysicalRect;
#[derive(MallocSizeOf)] #[derive(MallocSizeOf)]
@ -31,7 +30,7 @@ pub struct FragmentTree {
/// The scrollable overflow rectangle for the entire tree /// The scrollable overflow rectangle for the entire tree
/// <https://drafts.csswg.org/css-overflow/#scrollable> /// <https://drafts.csswg.org/css-overflow/#scrollable>
pub(crate) scrollable_overflow: PhysicalRect<Au>, scrollable_overflow: Cell<Option<PhysicalRect<Au>>>,
/// The containing block used in the layout of this fragment tree. /// The containing block used in the layout of this fragment tree.
pub(crate) initial_containing_block: PhysicalRect<Au>, pub(crate) initial_containing_block: PhysicalRect<Au>,
@ -44,13 +43,12 @@ impl FragmentTree {
pub(crate) fn new( pub(crate) fn new(
layout_context: &LayoutContext, layout_context: &LayoutContext,
root_fragments: Vec<Fragment>, root_fragments: Vec<Fragment>,
scrollable_overflow: PhysicalRect<Au>,
initial_containing_block: PhysicalRect<Au>, initial_containing_block: PhysicalRect<Au>,
viewport_scroll_sensitivity: AxesScrollSensitivity, viewport_scroll_sensitivity: AxesScrollSensitivity,
) -> Self { ) -> Self {
let fragment_tree = Self { let fragment_tree = Self {
root_fragments, root_fragments,
scrollable_overflow, scrollable_overflow: Cell::default(),
initial_containing_block, initial_containing_block,
viewport_scroll_sensitivity, viewport_scroll_sensitivity,
}; };
@ -91,16 +89,6 @@ impl FragmentTree {
fragment_tree fragment_tree
} }
pub(crate) fn build_display_list(
&self,
builder: &mut crate::display_list::DisplayListBuilder,
root_stacking_context: &StackingContext,
) {
// Paint the canvas background (if any) before/under everything else
root_stacking_context.build_canvas_background_display_list(builder, self);
root_stacking_context.build_display_list(builder);
}
pub fn print(&self) { pub fn print(&self) {
let mut print_tree = PrintTree::new("Fragment Tree".to_string()); let mut print_tree = PrintTree::new("Fragment Tree".to_string());
for fragment in &self.root_fragments { for fragment in &self.root_fragments {
@ -108,11 +96,35 @@ impl FragmentTree {
} }
} }
pub fn scrollable_overflow(&self) -> units::LayoutSize { pub(crate) fn scrollable_overflow(&self) -> PhysicalRect<Au> {
units::LayoutSize::from_untyped(Size2D::new( self.scrollable_overflow
self.scrollable_overflow.size.width.to_f32_px(), .get()
self.scrollable_overflow.size.height.to_f32_px(), .expect("Should only call `scrollable_overflow()` after calculating overflow")
)) }
pub(crate) fn calculate_scrollable_overflow(&self) {
self.scrollable_overflow
.set(Some(self.root_fragments.iter().fold(
PhysicalRect::zero(),
|acc, child| {
let child_overflow = child.calculate_scrollable_overflow_for_parent();
// https://drafts.csswg.org/css-overflow/#scrolling-direction
// We want to clip scrollable overflow on box-start and inline-start
// sides of the scroll container.
//
// FIXME(mrobinson, bug 25564): This should take into account writing
// mode.
let child_overflow = PhysicalRect::new(
euclid::Point2D::zero(),
euclid::Size2D::new(
child_overflow.size.width + child_overflow.origin.x,
child_overflow.size.height + child_overflow.origin.y,
),
);
acc.union(&child_overflow)
},
)));
} }
pub(crate) fn find<T>( pub(crate) fn find<T>(

View file

@ -22,7 +22,7 @@ pub(crate) struct PositioningFragment {
pub children: Vec<Fragment>, pub children: Vec<Fragment>,
/// The scrollable overflow of this anonymous fragment's children. /// The scrollable overflow of this anonymous fragment's children.
pub scrollable_overflow: PhysicalRect<Au>, scrollable_overflow: Option<PhysicalRect<Au>>,
/// The style of the fragment. /// The style of the fragment.
pub style: ServoArc<ComputedValues>, pub style: ServoArc<ComputedValues>,
@ -55,20 +55,12 @@ impl PositioningFragment {
rect: PhysicalRect<Au>, rect: PhysicalRect<Au>,
children: Vec<Fragment>, children: Vec<Fragment>,
) -> ArcRefCell<Self> { ) -> ArcRefCell<Self> {
let content_origin = rect.origin;
let scrollable_overflow = children.iter().fold(PhysicalRect::zero(), |acc, child| {
acc.union(
&child
.scrollable_overflow_for_parent()
.translate(content_origin.to_vector()),
)
});
ArcRefCell::new(PositioningFragment { ArcRefCell::new(PositioningFragment {
base, base,
style, style,
rect, rect,
children, children,
scrollable_overflow, scrollable_overflow: None,
cumulative_containing_block_rect: PhysicalRect::zero(), cumulative_containing_block_rect: PhysicalRect::zero(),
}) })
} }
@ -81,6 +73,25 @@ impl PositioningFragment {
rect.translate(self.cumulative_containing_block_rect.origin.to_vector()) rect.translate(self.cumulative_containing_block_rect.origin.to_vector())
} }
pub(crate) fn calculate_scrollable_overflow(&mut self) {
self.scrollable_overflow = Some(self.children.iter().fold(
PhysicalRect::zero(),
|acc, child| {
acc.union(
&child
.calculate_scrollable_overflow_for_parent()
.translate(self.rect.origin.to_vector()),
)
},
));
}
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
self.scrollable_overflow.expect(
"Should only call `scrollable_overflow_for_parent()` after calculating overflow",
)
}
pub fn print(&self, tree: &mut PrintTree) { pub fn print(&self, tree: &mut PrintTree) {
tree.new_level(format!( tree.new_level(format!(
"PositioningFragment\ "PositioningFragment\

View file

@ -8,6 +8,7 @@ use std::cell::{Cell, RefCell};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use std::process; use std::process;
use std::rc::Rc;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use app_units::Au; use app_units::Au;
@ -15,8 +16,8 @@ use base::Epoch;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::ScrollState; use constellation_traits::ScrollState;
use embedder_traits::{UntrustedNodeAddress, ViewportDetails}; use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D}; use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D, Vector2D}; use euclid::{Point2D, Scale, Size2D, Vector2D};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use fonts::{FontContext, FontContextWebFontMethods, WebFontDocumentContext}; use fonts::{FontContext, FontContextWebFontMethods, WebFontDocumentContext};
@ -76,8 +77,8 @@ use url::Url;
use webrender_api::units::{DevicePixel, DevicePoint, LayoutPixel, LayoutPoint, LayoutSize}; use webrender_api::units::{DevicePixel, DevicePoint, LayoutPixel, LayoutPoint, LayoutSize};
use webrender_api::{ExternalScrollId, HitTestFlags}; use webrender_api::{ExternalScrollId, HitTestFlags};
use crate::context::LayoutContext; use crate::context::{CachedImageOrError, LayoutContext};
use crate::display_list::{DisplayList, WebRenderImageInfo}; use crate::display_list::{DisplayListBuilder, StackingContextTree};
use crate::query::{ use crate::query::{
get_the_text_steps, process_client_rect_request, process_content_box_request, get_the_text_steps, process_client_rect_request, process_content_box_request,
process_content_boxes_request, process_node_scroll_area_request, process_offset_parent_query, process_content_boxes_request, process_node_scroll_area_request, process_offset_parent_query,
@ -142,19 +143,21 @@ pub struct LayoutThread {
box_tree: RefCell<Option<Arc<BoxTree>>>, box_tree: RefCell<Option<Arc<BoxTree>>>,
/// The fragment tree. /// The fragment tree.
fragment_tree: RefCell<Option<Arc<FragmentTree>>>, fragment_tree: RefCell<Option<Rc<FragmentTree>>>,
/// The [`StackingContextTree`] cached from previous layouts.
stacking_context_tree: RefCell<Option<StackingContextTree>>,
/// A counter for epoch messages /// A counter for epoch messages
epoch: Cell<Epoch>, epoch: Cell<Epoch>,
/// The size of the viewport. This may be different from the size of the screen due to viewport
/// constraints.
viewport_size: UntypedSize2D<Au>,
/// Scroll offsets of nodes that scroll. /// Scroll offsets of nodes that scroll.
scroll_offsets: RefCell<HashMap<ExternalScrollId, Vector2D<f32, LayoutPixel>>>, scroll_offsets: RefCell<HashMap<ExternalScrollId, Vector2D<f32, LayoutPixel>>>,
webrender_image_cache: Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), WebRenderImageInfo>>>, // A cache that maps image resources specified in CSS (e.g as the `url()` value
// for `background-image` or `content` properties) to either the final resolved
// image data, or an error if the image cache failed to load/decode the image.
resolved_images_cache: Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), CachedImageOrError>>>,
/// The executors for paint worklets. /// The executors for paint worklets.
registered_painters: RegisteredPaintersImpl, registered_painters: RegisteredPaintersImpl,
@ -510,8 +513,7 @@ impl LayoutThread {
Scale::new(config.viewport_details.hidpi_scale_factor.get()), Scale::new(config.viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(config.font_context.clone())), Box::new(LayoutFontMetricsProvider(config.font_context.clone())),
ComputedValues::initial_values_with_font_override(font), ComputedValues::initial_values_with_font_override(font),
// TODO: obtain preferred color scheme from embedder config.theme.into(),
PrefersColorScheme::Light,
); );
LayoutThread { LayoutThread {
@ -527,16 +529,13 @@ impl LayoutThread {
first_reflow: Cell::new(true), first_reflow: Cell::new(true),
box_tree: Default::default(), box_tree: Default::default(),
fragment_tree: Default::default(), fragment_tree: Default::default(),
stacking_context_tree: Default::default(),
// Epoch starts at 1 because of the initial display list for epoch 0 that we send to WR // Epoch starts at 1 because of the initial display list for epoch 0 that we send to WR
epoch: Cell::new(Epoch(1)), epoch: Cell::new(Epoch(1)),
viewport_size: Size2D::new(
Au::from_f32_px(config.viewport_details.size.width),
Au::from_f32_px(config.viewport_details.size.height),
),
compositor_api: config.compositor_api, compositor_api: config.compositor_api,
scroll_offsets: Default::default(), scroll_offsets: Default::default(),
stylist: Stylist::new(device, QuirksMode::NoQuirks), stylist: Stylist::new(device, QuirksMode::NoQuirks),
webrender_image_cache: Default::default(), resolved_images_cache: Default::default(),
debug: opts::get().debug.clone(), debug: opts::get().debug.clone(),
} }
} }
@ -615,7 +614,8 @@ impl LayoutThread {
ua_or_user: &ua_or_user_guard, ua_or_user: &ua_or_user_guard,
}; };
if self.update_device_if_necessary(&reflow_request, &guards) { let viewport_changed = self.viewport_did_change(reflow_request.viewport_details);
if self.update_device_if_necessary(&reflow_request, viewport_changed, &guards) {
if let Some(mut data) = root_element.mutate_data() { if let Some(mut data) = root_element.mutate_data() {
data.hint.insert(RestyleHint::recascade_subtree()); data.hint.insert(RestyleHint::recascade_subtree());
} }
@ -647,8 +647,9 @@ impl LayoutThread {
), ),
image_cache: self.image_cache.clone(), image_cache: self.image_cache.clone(),
font_context: self.font_context.clone(), font_context: self.font_context.clone(),
webrender_image_cache: self.webrender_image_cache.clone(), resolved_images_cache: self.resolved_images_cache.clone(),
pending_images: Mutex::default(), pending_images: Mutex::default(),
pending_rasterization_images: Mutex::default(),
node_image_animation_map: Arc::new(RwLock::new(std::mem::take( node_image_animation_map: Arc::new(RwLock::new(std::mem::take(
&mut reflow_request.node_to_image_animation_map, &mut reflow_request.node_to_image_animation_map,
))), ))),
@ -657,25 +658,32 @@ impl LayoutThread {
highlighted_dom_node: reflow_request.highlighted_dom_node, highlighted_dom_node: reflow_request.highlighted_dom_node,
}; };
self.restyle_and_build_trees( let damage = self.restyle_and_build_trees(
&reflow_request, &reflow_request,
root_element, root_element,
rayon_pool, rayon_pool,
&mut layout_context, &mut layout_context,
viewport_changed,
); );
self.calculate_overflow(damage);
self.build_stacking_context_tree(&reflow_request, damage);
self.build_display_list(&reflow_request, &mut layout_context); self.build_display_list(&reflow_request, &mut layout_context);
self.first_reflow.set(false);
self.first_reflow.set(false);
if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal { if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal {
self.update_scroll_node_state(&scroll_state); self.update_scroll_node_state(&scroll_state);
} }
let pending_images = std::mem::take(&mut *layout_context.pending_images.lock()); let pending_images = std::mem::take(&mut *layout_context.pending_images.lock());
let pending_rasterization_images =
std::mem::take(&mut *layout_context.pending_rasterization_images.lock());
let iframe_sizes = std::mem::take(&mut *layout_context.iframe_sizes.lock()); let iframe_sizes = std::mem::take(&mut *layout_context.iframe_sizes.lock());
let node_to_image_animation_map = let node_to_image_animation_map =
std::mem::take(&mut *layout_context.node_image_animation_map.write()); std::mem::take(&mut *layout_context.node_image_animation_map.write());
Some(ReflowResult { Some(ReflowResult {
pending_images, pending_images,
pending_rasterization_images,
iframe_sizes, iframe_sizes,
node_to_image_animation_map, node_to_image_animation_map,
}) })
@ -684,12 +692,12 @@ impl LayoutThread {
fn update_device_if_necessary( fn update_device_if_necessary(
&mut self, &mut self,
reflow_request: &ReflowRequest, reflow_request: &ReflowRequest,
viewport_changed: bool,
guards: &StylesheetGuards, guards: &StylesheetGuards,
) -> bool { ) -> bool {
let had_used_viewport_units = self.stylist.device().used_viewport_units(); let had_used_viewport_units = self.stylist.device().used_viewport_units();
let viewport_size_changed = self.viewport_did_change(reflow_request.viewport_details);
let theme_changed = self.theme_did_change(reflow_request.theme); let theme_changed = self.theme_did_change(reflow_request.theme);
if !viewport_size_changed && !theme_changed { if !viewport_changed && !theme_changed {
return false; return false;
} }
self.update_device( self.update_device(
@ -697,7 +705,7 @@ impl LayoutThread {
reflow_request.theme, reflow_request.theme,
guards, guards,
); );
(viewport_size_changed && had_used_viewport_units) || theme_changed (viewport_changed && had_used_viewport_units) || theme_changed
} }
fn prepare_stylist_for_reflow<'dom>( fn prepare_stylist_for_reflow<'dom>(
@ -755,7 +763,8 @@ impl LayoutThread {
root_element: ServoLayoutElement<'_>, root_element: ServoLayoutElement<'_>,
rayon_pool: Option<&ThreadPool>, rayon_pool: Option<&ThreadPool>,
layout_context: &mut LayoutContext<'_>, layout_context: &mut LayoutContext<'_>,
) { viewport_changed: bool,
) -> RestyleDamage {
let dirty_root = unsafe { let dirty_root = unsafe {
ServoLayoutNode::new(&reflow_request.dirty_root.unwrap()) ServoLayoutNode::new(&reflow_request.dirty_root.unwrap())
.as_element() .as_element()
@ -771,17 +780,20 @@ impl LayoutThread {
if !token.should_traverse() { if !token.should_traverse() {
layout_context.style_context.stylist.rule_tree().maybe_gc(); layout_context.style_context.stylist.rule_tree().maybe_gc();
return; return RestyleDamage::empty();
} }
let dirty_root: ServoLayoutNode = let dirty_root: ServoLayoutNode =
driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node(); driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node();
let root_node = root_element.as_node(); let root_node = root_element.as_node();
let damage = compute_damage_and_repair_style(layout_context.shared_context(), root_node); let mut damage =
if damage == RestyleDamage::REPAINT { compute_damage_and_repair_style(layout_context.shared_context(), root_node);
if viewport_changed {
damage = RestyleDamage::REBUILD_BOX;
} else if !damage.contains(RestyleDamage::REBUILD_BOX) {
layout_context.style_context.stylist.rule_tree().maybe_gc(); layout_context.style_context.stylist.rule_tree().maybe_gc();
return; return damage;
} }
let mut box_tree = self.box_tree.borrow_mut(); let mut box_tree = self.box_tree.borrow_mut();
@ -800,17 +812,14 @@ impl LayoutThread {
build_box_tree() build_box_tree()
}; };
let viewport_size = Size2D::new( let viewport_size = self.stylist.device().au_viewport_size();
self.viewport_size.width.to_f32_px(),
self.viewport_size.height.to_f32_px(),
);
let run_layout = || { let run_layout = || {
box_tree box_tree
.as_ref() .as_ref()
.unwrap() .unwrap()
.layout(recalc_style_traversal.context(), viewport_size) .layout(recalc_style_traversal.context(), viewport_size)
}; };
let fragment_tree = Arc::new(if let Some(pool) = rayon_pool { let fragment_tree = Rc::new(if let Some(pool) = rayon_pool {
pool.install(run_layout) pool.install(run_layout)
} else { } else {
run_layout() run_layout()
@ -818,6 +827,10 @@ impl LayoutThread {
*self.fragment_tree.borrow_mut() = Some(fragment_tree); *self.fragment_tree.borrow_mut() = Some(fragment_tree);
// The FragmentTree has been updated, so any existing StackingContext tree that layout
// had is now out of date and should be rebuilt.
*self.stacking_context_tree.borrow_mut() = None;
if self.debug.dump_style_tree { if self.debug.dump_style_tree {
println!( println!(
"{:?}", "{:?}",
@ -835,6 +848,61 @@ impl LayoutThread {
// GC the rule tree if some heuristics are met. // GC the rule tree if some heuristics are met.
layout_context.style_context.stylist.rule_tree().maybe_gc(); layout_context.style_context.stylist.rule_tree().maybe_gc();
damage
}
fn calculate_overflow(&self, damage: RestyleDamage) {
if !damage.contains(RestyleDamage::RECALCULATE_OVERFLOW) {
return;
}
if let Some(fragment_tree) = &*self.fragment_tree.borrow() {
fragment_tree.calculate_scrollable_overflow();
if self.debug.dump_flow_tree {
fragment_tree.print();
}
}
}
fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) {
if !reflow_request.reflow_goal.needs_display_list() &&
!reflow_request.reflow_goal.needs_display()
{
return;
}
let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
return;
};
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) &&
self.stacking_context_tree.borrow().is_some()
{
return;
}
let viewport_size = self.stylist.device().au_viewport_size();
let viewport_size = LayoutSize::new(
viewport_size.width.to_f32_px(),
viewport_size.height.to_f32_px(),
);
let scrollable_overflow = fragment_tree.scrollable_overflow();
let scrollable_overflow = LayoutSize::from_untyped(Size2D::new(
scrollable_overflow.size.width.to_f32_px(),
scrollable_overflow.size.height.to_f32_px(),
));
// Build the StackingContextTree. This turns the `FragmentTree` into a
// tree of fragments in CSS painting order and also creates all
// applicable spatial and clip nodes.
*self.stacking_context_tree.borrow_mut() = Some(StackingContextTree::new(
fragment_tree,
viewport_size,
scrollable_overflow,
self.id.into(),
fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
&self.debug,
));
} }
fn build_display_list( fn build_display_list(
@ -842,60 +910,33 @@ impl LayoutThread {
reflow_request: &ReflowRequest, reflow_request: &ReflowRequest,
layout_context: &mut LayoutContext<'_>, layout_context: &mut LayoutContext<'_>,
) { ) {
if !reflow_request.reflow_goal.needs_display() {
return;
}
let Some(fragment_tree) = &*self.fragment_tree.borrow() else { let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
return; return;
}; };
if !reflow_request.reflow_goal.needs_display_list() {
let mut stacking_context_tree = self.stacking_context_tree.borrow_mut();
let Some(stacking_context_tree) = stacking_context_tree.as_mut() else {
return; return;
} };
let mut epoch = self.epoch.get(); let mut epoch = self.epoch.get();
epoch.next(); epoch.next();
self.epoch.set(epoch); self.epoch.set(epoch);
stacking_context_tree.compositor_info.epoch = epoch.into();
let viewport_size = LayoutSize::from_untyped(Size2D::new( let built_display_list = DisplayListBuilder::build(
self.viewport_size.width.to_f32_px(), layout_context,
self.viewport_size.height.to_f32_px(), stacking_context_tree,
)); fragment_tree,
let mut display_list = DisplayList::new( &self.debug,
viewport_size,
fragment_tree.scrollable_overflow(),
self.id.into(),
epoch.into(),
fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
); );
display_list.wr.begin();
// `dump_serialized_display_list` doesn't actually print anything. It sets up
// the display list for printing the serialized version when `finalize()` is called.
// We need to call this before adding any display items so that they are printed
// during `finalize()`.
if self.debug.dump_display_list {
display_list.wr.dump_serialized_display_list();
}
// Build the root stacking context. This turns the `FragmentTree` into a
// tree of fragments in CSS painting order and also creates all
// applicable spatial and clip nodes.
let root_stacking_context =
display_list.build_stacking_context_tree(fragment_tree, &self.debug);
// Build the rest of the display list which inclues all of the WebRender primitives.
display_list.build(layout_context, fragment_tree, &root_stacking_context);
if self.debug.dump_flow_tree {
fragment_tree.print();
}
if self.debug.dump_stacking_context_tree {
root_stacking_context.debug_print();
}
if reflow_request.reflow_goal.needs_display() {
self.compositor_api.send_display_list( self.compositor_api.send_display_list(
self.webview_id, self.webview_id,
display_list.compositor_info, &stacking_context_tree.compositor_info,
display_list.wr.end().1, built_display_list,
); );
let (keys, instance_keys) = self let (keys, instance_keys) = self
@ -904,7 +945,6 @@ impl LayoutThread {
self.compositor_api self.compositor_api
.remove_unused_font_resources(keys, instance_keys) .remove_unused_font_resources(keys, instance_keys)
} }
}
fn update_scroll_node_state(&self, state: &ScrollState) { fn update_scroll_node_state(&self, state: &ScrollState) {
self.scroll_offsets self.scroll_offsets
@ -943,9 +983,6 @@ impl LayoutThread {
Au::from_f32_px(viewport_details.size.height), Au::from_f32_px(viewport_details.size.height),
); );
// TODO: eliminate self.viewport_size in favour of using self.device.au_viewport_size()
self.viewport_size = new_viewport_size;
let device = self.stylist.device(); let device = self.stylist.device();
let size_did_change = device.au_viewport_size() != new_viewport_size; let size_did_change = device.au_viewport_size() != new_viewport_size;
let pixel_ratio_did_change = device.device_pixel_ratio().get() != new_pixel_ratio; let pixel_ratio_did_change = device.device_pixel_ratio().get() != new_pixel_ratio;
@ -953,7 +990,8 @@ impl LayoutThread {
size_did_change || pixel_ratio_did_change size_did_change || pixel_ratio_did_change
} }
fn theme_did_change(&self, theme: PrefersColorScheme) -> bool { fn theme_did_change(&self, theme: Theme) -> bool {
let theme: PrefersColorScheme = theme.into();
theme != self.device().color_scheme() theme != self.device().color_scheme()
} }
@ -961,7 +999,7 @@ impl LayoutThread {
fn update_device( fn update_device(
&mut self, &mut self,
viewport_details: ViewportDetails, viewport_details: ViewportDetails,
theme: PrefersColorScheme, theme: Theme,
guards: &StylesheetGuards, guards: &StylesheetGuards,
) { ) {
let device = Device::new( let device = Device::new(
@ -971,7 +1009,7 @@ impl LayoutThread {
Scale::new(viewport_details.hidpi_scale_factor.get()), Scale::new(viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(self.font_context.clone())), Box::new(LayoutFontMetricsProvider(self.font_context.clone())),
self.stylist.device().default_computed_values().to_arc(), self.stylist.device().default_computed_values().to_arc(),
theme, theme.into(),
); );
// Preserve any previously computed root font size. // Preserve any previously computed root font size.

View file

@ -41,7 +41,6 @@ use malloc_size_of_derive::MallocSizeOf;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
use style::logical_geometry::WritingMode; use style::logical_geometry::WritingMode;
use style::properties::ComputedValues; use style::properties::ComputedValues;
use style::values::computed::TextDecorationLine;
use crate::geom::{LogicalVec2, SizeConstraint}; use crate::geom::{LogicalVec2, SizeConstraint};
use crate::style_ext::AspectRatio; use crate::style_ext::AspectRatio;
@ -163,39 +162,20 @@ impl<'a> From<&'_ DefiniteContainingBlock<'a>> for ContainingBlock<'a> {
/// propoagation, but only during `BoxTree` construction. /// propoagation, but only during `BoxTree` construction.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
struct PropagatedBoxTreeData { struct PropagatedBoxTreeData {
text_decoration: TextDecorationLine,
allow_percentage_column_in_tables: bool, allow_percentage_column_in_tables: bool,
} }
impl Default for PropagatedBoxTreeData { impl Default for PropagatedBoxTreeData {
fn default() -> Self { fn default() -> Self {
Self { Self {
text_decoration: Default::default(),
allow_percentage_column_in_tables: true, allow_percentage_column_in_tables: true,
} }
} }
} }
impl PropagatedBoxTreeData { impl PropagatedBoxTreeData {
pub(crate) fn union(&self, style: &ComputedValues) -> Self {
Self {
// FIXME(#31736): This is only taking into account the line style and not the decoration
// color. This should collect information about both so that they can be rendered properly.
text_decoration: self.text_decoration | style.clone_text_decoration_line(),
allow_percentage_column_in_tables: self.allow_percentage_column_in_tables,
}
}
pub(crate) fn without_text_decorations(&self) -> Self {
Self {
text_decoration: TextDecorationLine::NONE,
allow_percentage_column_in_tables: self.allow_percentage_column_in_tables,
}
}
fn disallowing_percentage_table_columns(&self) -> PropagatedBoxTreeData { fn disallowing_percentage_table_columns(&self) -> PropagatedBoxTreeData {
Self { Self {
text_decoration: self.text_decoration,
allow_percentage_column_in_tables: false, allow_percentage_column_in_tables: false,
} }
} }

View file

@ -305,10 +305,7 @@ impl PositioningContext {
} }
pub(crate) fn push(&mut self, hoisted_box: HoistedAbsolutelyPositionedBox) { pub(crate) fn push(&mut self, hoisted_box: HoistedAbsolutelyPositionedBox) {
debug_assert!(matches!( debug_assert!(hoisted_box.position().is_absolutely_positioned());
hoisted_box.position(),
Position::Absolute | Position::Fixed
));
self.absolutes.push(hoisted_box); self.absolutes.push(hoisted_box);
} }
@ -380,7 +377,7 @@ impl HoistedAbsolutelyPositionedBox {
.context .context
.style() .style()
.clone_position(); .clone_position();
assert!(position == Position::Fixed || position == Position::Absolute); assert!(position.is_absolutely_positioned());
position position
} }

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Utilities for querying the layout, as needed by layout. //! Utilities for querying the layout, as needed by layout.
use std::sync::Arc; use std::rc::Rc;
use app_units::Au; use app_units::Au;
use euclid::default::{Point2D, Rect}; use euclid::default::{Point2D, Rect};
@ -80,7 +80,7 @@ pub fn process_client_rect_request(node: ServoLayoutNode<'_>) -> Rect<i32> {
/// <https://drafts.csswg.org/cssom-view/#scrolling-area> /// <https://drafts.csswg.org/cssom-view/#scrolling-area>
pub fn process_node_scroll_area_request( pub fn process_node_scroll_area_request(
requested_node: Option<ServoLayoutNode<'_>>, requested_node: Option<ServoLayoutNode<'_>>,
fragment_tree: Option<Arc<FragmentTree>>, fragment_tree: Option<Rc<FragmentTree>>,
) -> Rect<i32> { ) -> Rect<i32> {
let Some(tree) = fragment_tree else { let Some(tree) = fragment_tree else {
return Rect::zero(); return Rect::zero();

View file

@ -3,7 +3,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::LazyCell; use std::cell::LazyCell;
use std::sync::Arc;
use app_units::Au; use app_units::Au;
use base::id::{BrowsingContextId, PipelineId}; use base::id::{BrowsingContextId, PipelineId};
@ -11,8 +10,7 @@ use data_url::DataUrl;
use embedder_traits::ViewportDetails; use embedder_traits::ViewportDetails;
use euclid::{Scale, Size2D}; use euclid::{Scale, Size2D};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder}; use net_traits::image_cache::{Image, ImageOrMetadataAvailable, UsePlaceholder};
use pixels::Image;
use script::layout_dom::ServoLayoutNode; use script::layout_dom::ServoLayoutNode;
use script_layout_interface::IFrameSize; use script_layout_interface::IFrameSize;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
@ -28,7 +26,7 @@ use url::Url;
use webrender_api::ImageKey; use webrender_api::ImageKey;
use crate::cell::ArcRefCell; use crate::cell::ArcRefCell;
use crate::context::LayoutContext; use crate::context::{LayoutContext, LayoutImageCacheResult};
use crate::dom::NodeExt; use crate::dom::NodeExt;
use crate::fragment_tree::{BaseFragmentInfo, Fragment, IFrameFragment, ImageFragment}; use crate::fragment_tree::{BaseFragmentInfo, Fragment, IFrameFragment, ImageFragment};
use crate::geom::{ use crate::geom::{
@ -115,7 +113,7 @@ pub(crate) struct VideoInfo {
#[derive(Debug, MallocSizeOf)] #[derive(Debug, MallocSizeOf)]
pub(crate) enum ReplacedContentKind { pub(crate) enum ReplacedContentKind {
Image(#[conditional_malloc_size_of] Option<Arc<Image>>), Image(Option<Image>),
IFrame(IFrameInfo), IFrame(IFrameInfo),
Canvas(CanvasInfo), Canvas(CanvasInfo),
Video(Option<VideoInfo>), Video(Option<VideoInfo>),
@ -162,7 +160,7 @@ impl ReplacedContents {
} }
}; };
if let ReplacedContentKind::Image(Some(ref image)) = kind { if let ReplacedContentKind::Image(Some(Image::Raster(ref image))) = kind {
context.handle_animated_image(element.opaque(), image.clone()); context.handle_animated_image(element.opaque(), image.clone());
} }
@ -197,13 +195,20 @@ impl ReplacedContents {
image_url.clone().into(), image_url.clone().into(),
UsePlaceholder::No, UsePlaceholder::No,
) { ) {
Ok(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => { LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
(Some(image.clone()), image.width as f32, image.height as f32) ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
let metadata = image.metadata();
(
Some(image.clone()),
metadata.width as f32,
metadata.height as f32,
)
}, },
Ok(ImageOrMetadataAvailable::MetadataAvailable(metadata, _id)) => { ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
(None, metadata.width as f32, metadata.height as f32) (None, metadata.width as f32, metadata.height as f32)
}, },
Err(_) => return None, },
LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
}; };
return Some(Self { return Some(Self {
@ -315,7 +320,19 @@ impl ReplacedContents {
match &self.kind { match &self.kind {
ReplacedContentKind::Image(image) => image ReplacedContentKind::Image(image) => image
.as_ref() .as_ref()
.and_then(|image| image.id) .and_then(|image| match image {
Image::Raster(raster_image) => raster_image.id,
Image::Vector(vector_image) => {
let scale = layout_context.shared_context().device_pixel_ratio();
let width = object_fit_size.width.scale_by(scale.0).to_px();
let height = object_fit_size.height.scale_by(scale.0).to_px();
let size = Size2D::new(width, height);
let tag = self.base_fragment_info.tag?;
layout_context
.rasterize_vector_image(vector_image.id, size, tag.node)
.and_then(|i| i.id)
},
})
.map(|image_key| { .map(|image_key| {
Fragment::Image(ArcRefCell::new(ImageFragment { Fragment::Image(ArcRefCell::new(ImageFragment {
base: self.base_fragment_info.into(), base: self.base_fragment_info.into(),

View file

@ -87,6 +87,15 @@ input[type="file"] {
border-style: none; border-style: none;
} }
input[type="color"] {
padding: 6px;
width: 64px;
height: 32px;
border-radius: 2px;
background: lightgrey;
border: 1px solid gray;
}
td[align="left"] { text-align: left; } td[align="left"] { text-align: left; }
td[align="center"] { text-align: center; } td[align="center"] { text-align: center; }
td[align="right"] { text-align: right; } td[align="right"] { text-align: right; }
@ -257,3 +266,7 @@ select {
/* Don't show a text cursor when hovering selected option */ /* Don't show a text cursor when hovering selected option */
cursor: default; cursor: default;
} }
slot {
display: contents;
}

View file

@ -81,12 +81,7 @@ impl Table {
contents: NonReplacedContents, contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData, propagated_data: PropagatedBoxTreeData,
) -> Self { ) -> Self {
let mut traversal = TableBuilderTraversal::new( let mut traversal = TableBuilderTraversal::new(context, info, grid_style, propagated_data);
context,
info,
grid_style,
propagated_data.union(&info.style),
);
contents.traverse(context, info, &mut traversal); contents.traverse(context, info, &mut traversal);
traversal.finish() traversal.finish()
} }
@ -771,9 +766,6 @@ impl<'dom> TraversalHandler<'dom> for TableBuilderTraversal<'_, 'dom> {
}); });
self.builder.table.row_groups.push(row_group.clone()); self.builder.table.row_groups.push(row_group.clone());
let previous_propagated_data = self.current_propagated_data;
self.current_propagated_data = self.current_propagated_data.union(&info.style);
let new_row_group_index = self.builder.table.row_groups.len() - 1; let new_row_group_index = self.builder.table.row_groups.len() - 1;
self.current_row_group_index = Some(new_row_group_index); self.current_row_group_index = Some(new_row_group_index);
@ -785,7 +777,6 @@ impl<'dom> TraversalHandler<'dom> for TableBuilderTraversal<'_, 'dom> {
self.finish_anonymous_row_if_needed(); self.finish_anonymous_row_if_needed();
self.current_row_group_index = None; self.current_row_group_index = None;
self.current_propagated_data = previous_propagated_data;
self.builder.incoming_rowspans.clear(); self.builder.incoming_rowspans.clear();
box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::TrackGroup( box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::TrackGroup(
@ -936,7 +927,7 @@ impl<'style, 'builder, 'dom, 'a> TableRowBuilder<'style, 'builder, 'dom, 'a> {
table_traversal, table_traversal,
info, info,
current_anonymous_cell_content: Vec::new(), current_anonymous_cell_content: Vec::new(),
propagated_data: propagated_data.union(&info.style), propagated_data,
} }
} }

View file

@ -76,7 +76,7 @@ pub(crate) use construct::AnonymousTableContent;
pub use construct::TableBuilder; pub use construct::TableBuilder;
use euclid::{Point2D, Size2D, UnknownUnit, Vector2D}; use euclid::{Point2D, Size2D, UnknownUnit, Vector2D};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutElement; use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_arc::Arc; use servo_arc::Arc;
use style::context::SharedStyleContext; use style::context::SharedStyleContext;
use style::properties::ComputedValues; use style::properties::ComputedValues;
@ -425,13 +425,14 @@ impl TableLevelBox {
pub(crate) fn repair_style( pub(crate) fn repair_style(
&self, &self,
context: &SharedStyleContext<'_>, context: &SharedStyleContext<'_>,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>, new_style: &Arc<ComputedValues>,
) { ) {
match self { match self {
TableLevelBox::Caption(caption) => caption TableLevelBox::Caption(caption) => caption
.borrow_mut() .borrow_mut()
.context .context
.repair_style(context, new_style), .repair_style(context, node, new_style),
TableLevelBox::Cell(cell) => cell.borrow_mut().repair_style(new_style), TableLevelBox::Cell(cell) => cell.borrow_mut().repair_style(new_style),
TableLevelBox::TrackGroup(track_group) => { TableLevelBox::TrackGroup(track_group) => {
track_group.borrow_mut().repair_style(new_style); track_group.borrow_mut().repair_style(new_style);

View file

@ -7,6 +7,7 @@ use std::fmt;
use app_units::Au; use app_units::Au;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc; use servo_arc::Arc;
use style::context::SharedStyleContext; use style::context::SharedStyleContext;
use style::properties::ComputedValues; use style::properties::ComputedValues;
@ -35,8 +36,7 @@ impl TaffyContainer {
contents: NonReplacedContents, contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData, propagated_data: PropagatedBoxTreeData,
) -> Self { ) -> Self {
let mut builder = let mut builder = ModernContainerBuilder::new(context, info, propagated_data);
ModernContainerBuilder::new(context, info, propagated_data.union(&info.style));
contents.traverse(context, info, &mut builder); contents.traverse(context, info, &mut builder);
let items = builder.finish(); let items = builder.finish();
@ -152,17 +152,18 @@ impl TaffyItemBox {
pub(crate) fn repair_style( pub(crate) fn repair_style(
&mut self, &mut self,
context: &SharedStyleContext, context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>, new_style: &Arc<ComputedValues>,
) { ) {
self.style = new_style.clone(); self.style = new_style.clone();
match &mut self.taffy_level_box { match &mut self.taffy_level_box {
TaffyItemBoxInner::InFlowBox(independent_formatting_context) => { TaffyItemBoxInner::InFlowBox(independent_formatting_context) => {
independent_formatting_context.repair_style(context, new_style) independent_formatting_context.repair_style(context, node, new_style)
}, },
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut() .borrow_mut()
.context .context
.repair_style(context, new_style), .repair_style(context, node, new_style),
} }
} }
} }

View file

@ -105,23 +105,24 @@ pub(crate) fn compute_damage_and_repair_style_inner(
parent_restyle_damage: RestyleDamage, parent_restyle_damage: RestyleDamage,
) -> RestyleDamage { ) -> RestyleDamage {
let original_damage; let original_damage;
let damage = { let damage;
{
let mut element_data = node let mut element_data = node
.style_data() .style_data()
.expect("Should not run `compute_damage` before styling.") .expect("Should not run `compute_damage` before styling.")
.element_data .element_data
.borrow_mut(); .borrow_mut();
original_damage = std::mem::take(&mut element_data.damage);
damage = original_damage | parent_restyle_damage;
if let Some(ref style) = element_data.styles.primary { if let Some(ref style) = element_data.styles.primary {
if style.get_box().display == Display::None { if style.get_box().display == Display::None {
return parent_restyle_damage; return damage;
}
} }
} }
original_damage = std::mem::take(&mut element_data.damage);
element_data.damage |= parent_restyle_damage;
element_data.damage
};
let mut propagated_damage = damage; let mut propagated_damage = damage;
for child in iter_child_nodes(node) { for child in iter_child_nodes(node) {
@ -130,7 +131,9 @@ pub(crate) fn compute_damage_and_repair_style_inner(
} }
} }
if propagated_damage == RestyleDamage::REPAINT && original_damage == RestyleDamage::REPAINT { if !propagated_damage.contains(RestyleDamage::REBUILD_BOX) &&
!original_damage.contains(RestyleDamage::REBUILD_BOX)
{
node.repair_style(context); node.repair_style(context);
} }

View file

@ -22,6 +22,8 @@ indexmap = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
keyboard-types = { workspace = true } keyboard-types = { workspace = true }
markup5ever = { workspace = true } markup5ever = { workspace = true }
mime = { workspace = true }
resvg = { workspace = true }
servo_allocator = { path = "../allocator" } servo_allocator = { path = "../allocator" }
servo_arc = { workspace = true } servo_arc = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }

View file

@ -774,6 +774,7 @@ malloc_size_of_is_0!(content_security_policy::Destination);
malloc_size_of_is_0!(http::StatusCode); malloc_size_of_is_0!(http::StatusCode);
malloc_size_of_is_0!(app_units::Au); malloc_size_of_is_0!(app_units::Au);
malloc_size_of_is_0!(keyboard_types::Modifiers); malloc_size_of_is_0!(keyboard_types::Modifiers);
malloc_size_of_is_0!(mime::Mime);
malloc_size_of_is_0!(std::num::NonZeroU64); malloc_size_of_is_0!(std::num::NonZeroU64);
malloc_size_of_is_0!(std::num::NonZeroUsize); malloc_size_of_is_0!(std::num::NonZeroUsize);
malloc_size_of_is_0!(std::sync::atomic::AtomicBool); malloc_size_of_is_0!(std::sync::atomic::AtomicBool);
@ -782,6 +783,7 @@ malloc_size_of_is_0!(std::sync::atomic::AtomicUsize);
malloc_size_of_is_0!(std::time::Duration); malloc_size_of_is_0!(std::time::Duration);
malloc_size_of_is_0!(std::time::Instant); malloc_size_of_is_0!(std::time::Instant);
malloc_size_of_is_0!(std::time::SystemTime); malloc_size_of_is_0!(std::time::SystemTime);
malloc_size_of_is_0!(resvg::usvg::Tree);
malloc_size_of_is_0!(style::data::ElementData); malloc_size_of_is_0!(style::data::ElementData);
malloc_size_of_is_0!(style::font_face::SourceList); malloc_size_of_is_0!(style::font_face::SourceList);
malloc_size_of_is_0!(style::properties::ComputedValues); malloc_size_of_is_0!(style::properties::ComputedValues);

View file

@ -13,7 +13,6 @@ path = "lib.rs"
[dependencies] [dependencies]
base = { workspace = true } base = { workspace = true }
constellation_traits = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
log = { workspace = true } log = { workspace = true }
malloc_size_of = { workspace = true } malloc_size_of = { workspace = true }

View file

@ -29,6 +29,7 @@ crossbeam-channel = { workspace = true }
data-url = { workspace = true } data-url = { workspace = true }
devtools_traits = { workspace = true } devtools_traits = { workspace = true }
embedder_traits = { workspace = true } embedder_traits = { workspace = true }
fst = "0.4"
futures = { version = "0.3", package = "futures" } futures = { version = "0.3", package = "futures" }
futures-core = { version = "0.3.30", default-features = false } futures-core = { version = "0.3.30", default-features = false }
futures-util = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.30", default-features = false }
@ -55,9 +56,9 @@ rayon = { workspace = true }
rustls = { workspace = true } rustls = { workspace = true }
rustls-pemfile = { workspace = true } rustls-pemfile = { workspace = true }
rustls-pki-types = { workspace = true } rustls-pki-types = { workspace = true }
resvg = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
servo_allocator = { path = "../allocator" }
servo_arc = { workspace = true } servo_arc = { workspace = true }
servo_config = { path = "../config" } servo_config = { path = "../config" }
servo_url = { path = "../url" } servo_url = { path = "../url" }
@ -77,6 +78,7 @@ webrender_api = { workspace = true }
[dev-dependencies] [dev-dependencies]
embedder_traits = { workspace = true, features = ["baked-default-resources"] } embedder_traits = { workspace = true, features = ["baked-default-resources"] }
flate2 = "1" flate2 = "1"
fst = "0.4"
futures = { version = "0.3", features = ["compat"] } futures = { version = "0.3", features = ["compat"] }
hyper = { workspace = true, features = ["full"] } hyper = { workspace = true, features = ["full"] }
hyper-util = { workspace = true, features = ["server-graceful"] } hyper-util = { workspace = true, features = ["server-graceful"] }

View file

@ -18,6 +18,7 @@ use http::{HeaderValue, Method, StatusCode};
use ipc_channel::ipc; use ipc_channel::ipc;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use mime::{self, Mime}; use mime::{self, Mime};
use net_traits::fetch::headers::extract_mime_type_as_mime;
use net_traits::filemanager_thread::{FileTokenCheck, RelativePos}; use net_traits::filemanager_thread::{FileTokenCheck, RelativePos};
use net_traits::http_status::HttpStatus; use net_traits::http_status::HttpStatus;
use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer}; use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
@ -886,7 +887,7 @@ pub fn should_be_blocked_due_to_nosniff(
// Step 2 // Step 2
// Note: an invalid MIME type will produce a `None`. // Note: an invalid MIME type will produce a `None`.
let content_type_header = response_headers.typed_get::<ContentType>(); let mime_type = extract_mime_type_as_mime(response_headers);
/// <https://html.spec.whatwg.org/multipage/#scriptingLanguages> /// <https://html.spec.whatwg.org/multipage/#scriptingLanguages>
#[inline] #[inline]
@ -915,16 +916,12 @@ pub fn should_be_blocked_due_to_nosniff(
.any(|mime| mime.type_() == mime_type.type_() && mime.subtype() == mime_type.subtype()) .any(|mime| mime.type_() == mime_type.type_() && mime.subtype() == mime_type.subtype())
} }
match content_type_header { match mime_type {
// Step 4 // Step 4
Some(ref ct) if destination.is_script_like() => { Some(ref mime_type) if destination.is_script_like() => !is_javascript_mime_type(mime_type),
!is_javascript_mime_type(&ct.clone().into())
},
// Step 5 // Step 5
Some(ref ct) if destination == Destination::Style => { Some(ref mime_type) if destination == Destination::Style => {
let m: mime::Mime = ct.clone().into(); mime_type.type_() != mime::TEXT && mime_type.subtype() != mime::CSS
m.type_() != mime::TEXT && m.subtype() != mime::CSS
}, },
None if destination == Destination::Style || destination.is_script_like() => true, None if destination == Destination::Style || destination.is_script_like() => true,
@ -938,18 +935,22 @@ fn should_be_blocked_due_to_mime_type(
destination: Destination, destination: Destination,
response_headers: &HeaderMap, response_headers: &HeaderMap,
) -> bool { ) -> bool {
// Step 1 // Step 1: Let mimeType be the result of extracting a MIME type from responses header list.
let mime_type: mime::Mime = match response_headers.typed_get::<ContentType>() { let mime_type: mime::Mime = match extract_mime_type_as_mime(response_headers) {
Some(header) => header.into(), Some(mime_type) => mime_type,
// Step 2: If mimeType is failure, then return allowed.
None => return false, None => return false,
}; };
// Step 2-3 // Step 3: Let destination be requests destination.
// Step 4: If destination is script-like and one of the following is true, then return blocked:
// - mimeTypes essence starts with "audio/", "image/", or "video/".
// - mimeTypes essence is "text/csv".
// Step 5: Return allowed.
destination.is_script_like() && destination.is_script_like() &&
match mime_type.type_() { match mime_type.type_() {
mime::AUDIO | mime::VIDEO | mime::IMAGE => true, mime::AUDIO | mime::VIDEO | mime::IMAGE => true,
mime::TEXT if mime_type.subtype() == mime::CSV => true, mime::TEXT if mime_type.subtype() == mime::CSV => true,
// Step 4
_ => false, _ => false,
} }
} }

View file

@ -9,9 +9,11 @@ use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use embedder_traits::resources::{self, Resource}; use embedder_traits::resources::{self, Resource};
use fst::{Map, MapBuilder};
use headers::{HeaderMapExt, StrictTransportSecurity}; use headers::{HeaderMapExt, StrictTransportSecurity};
use http::HeaderMap; use http::HeaderMap;
use log::{debug, error, info}; use log::{debug, error, info};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use net_traits::IncludeSubdomains; use net_traits::IncludeSubdomains;
use net_traits::pub_domains::reg_suffix; use net_traits::pub_domains::reg_suffix;
@ -85,99 +87,67 @@ pub struct HstsList {
/// it is split out to allow sharing between the private and public http state /// it is split out to allow sharing between the private and public http state
/// as well as potentially swpaping out the underlying type to something immutable /// as well as potentially swpaping out the underlying type to something immutable
/// and more efficient like FSTs or DAFSA/DAWGs. /// and more efficient like FSTs or DAFSA/DAWGs.
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)] /// To generate a new version of the FST map file run `./mach update-hsts-preload`
pub struct HstsPreloadList { #[derive(Clone, Debug)]
pub entries_map: HashMap<String, Vec<HstsEntry>>, pub struct HstsPreloadList(pub fst::Map<Vec<u8>>);
impl MallocSizeOf for HstsPreloadList {
#[allow(unsafe_code)]
fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
unsafe { ops.malloc_size_of(self.0.as_fst().as_inner().as_ptr()) }
}
} }
pub static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> = static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> =
LazyLock::new(HstsPreloadList::from_servo_preload); LazyLock::new(HstsPreloadList::from_servo_preload);
pub fn hsts_preload_size_of(ops: &mut MallocSizeOfOps) -> usize {
PRELOAD_LIST_ENTRIES.size_of(ops)
}
impl HstsPreloadList { impl HstsPreloadList {
/// Create an `HstsList` from the bytes of a JSON preload file. /// Create an `HstsList` from the bytes of a JSON preload file.
pub fn from_preload(preload_content: &str) -> Option<HstsPreloadList> { pub fn from_preload(preload_content: Vec<u8>) -> Option<HstsPreloadList> {
#[derive(Deserialize)] Map::new(preload_content).map(HstsPreloadList).ok()
struct HstsEntries {
entries: Vec<HstsEntry>,
}
let hsts_entries: Option<HstsEntries> = serde_json::from_str(preload_content).ok();
hsts_entries.map(|hsts_entries| {
let mut hsts_list: HstsPreloadList = HstsPreloadList::default();
for hsts_entry in hsts_entries.entries {
hsts_list.push(hsts_entry);
}
hsts_list
})
} }
pub fn from_servo_preload() -> HstsPreloadList { pub fn from_servo_preload() -> HstsPreloadList {
debug!("Intializing HSTS Preload list"); debug!("Intializing HSTS Preload list");
let list = resources::read_string(Resource::HstsPreloadList); let map_bytes = resources::read_bytes(Resource::HstsPreloadList);
HstsPreloadList::from_preload(&list).unwrap_or_else(|| { HstsPreloadList::from_preload(map_bytes).unwrap_or_else(|| {
error!("HSTS preload file is invalid. Setting HSTS list to default values"); error!("HSTS preload file is invalid. Setting HSTS list to default values");
HstsPreloadList { HstsPreloadList(MapBuilder::memory().into_map())
entries_map: Default::default(),
}
}) })
} }
pub fn is_host_secure(&self, host: &str) -> bool { pub fn is_host_secure(&self, host: &str) -> bool {
let base_domain = reg_suffix(host); let base_domain = reg_suffix(host);
self.entries_map.get(base_domain).is_some_and(|entries| { let parts = host[..host.len() - base_domain.len()].rsplit_terminator('.');
// No need to check for expiration in the preload list let mut domain_to_test = base_domain.to_owned();
entries.iter().any(|e| {
if e.include_subdomains { if self.0.get(&domain_to_test).is_some_and(|id| {
e.matches_subdomain(host) || e.matches_domain(host) // The FST map ids were constructed such that the parity represents the includeSubdomain flag
} else { id % 2 == 1 || domain_to_test == host
e.matches_domain(host) }) {
} return true;
})
})
} }
pub fn has_domain(&self, host: &str, base_domain: &str) -> bool { // Check all further subdomains up to the passed host
self.entries_map for part in parts {
.get(base_domain) domain_to_test = format!("{}.{}", part, domain_to_test);
.is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host))) if self.0.get(&domain_to_test).is_some_and(|id| {
} // The FST map ids were constructed such that the parity represents the includeSubdomain flag
id % 2 == 1 || domain_to_test == host
pub fn has_subdomain(&self, host: &str, base_domain: &str) -> bool { }) {
self.entries_map.get(base_domain).is_some_and(|entries| { return true;
entries
.iter()
.any(|e| e.include_subdomains && e.matches_subdomain(host))
})
}
pub fn push(&mut self, entry: HstsEntry) {
let host = entry.host.clone();
let base_domain = reg_suffix(&host);
let have_domain = self.has_domain(&entry.host, base_domain);
let have_subdomain = self.has_subdomain(&entry.host, base_domain);
let entries = self.entries_map.entry(base_domain.to_owned()).or_default();
if !have_domain && !have_subdomain {
entries.push(entry);
} else if !have_subdomain {
for e in entries {
if e.matches_domain(&entry.host) {
e.include_subdomains = entry.include_subdomains;
// TODO(sebsebmc): We could shrink the the HSTS preload memory use further by using a type
// that doesn't store an expiry since all preload entries should be "forever"
e.expires_at = entry.expires_at;
}
} }
} }
false
} }
} }
impl HstsList { impl HstsList {
pub fn is_host_secure(&self, host: &str) -> bool { pub fn is_host_secure(&self, host: &str) -> bool {
debug!("HSTS: is {host} secure?");
if PRELOAD_LIST_ENTRIES.is_host_secure(host) { if PRELOAD_LIST_ENTRIES.is_host_secure(host) {
info!("{host} is in the preload list"); info!("{host} is in the preload list");
return true; return true;

View file

@ -7,21 +7,25 @@ use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{mem, thread}; use std::{mem, thread};
use base::id::PipelineId;
use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData}; use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData};
use imsz::imsz_from_reader; use imsz::imsz_from_reader;
use ipc_channel::ipc::IpcSharedMemory; use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
use log::{debug, warn}; use log::{debug, warn};
use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps}; use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use mime::Mime;
use net_traits::image_cache::{ use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse, Image, ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener,
PendingImageId, UsePlaceholder, ImageOrMetadataAvailable, ImageResponse, PendingImageId, RasterizationCompleteResponse,
UsePlaceholder, VectorImage,
}; };
use net_traits::request::CorsSettings; use net_traits::request::CorsSettings;
use net_traits::{FetchMetadata, FetchResponseMsg, FilteredMetadata, NetworkError}; use net_traits::{FetchMetadata, FetchResponseMsg, FilteredMetadata, NetworkError};
use pixels::{CorsStatus, Image, ImageMetadata, PixelFormat, load_from_memory}; use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage, load_from_memory};
use profile_traits::mem::{Report, ReportKind}; use profile_traits::mem::{Report, ReportKind};
use profile_traits::path; use profile_traits::path;
use resvg::{tiny_skia, usvg};
use servo_config::pref; use servo_config::pref;
use servo_url::{ImmutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, ServoUrl};
use webrender_api::units::DeviceIntSize; use webrender_api::units::DeviceIntSize;
@ -48,12 +52,53 @@ const FALLBACK_RIPPY: &[u8] = include_bytes!("../../resources/rippy.png");
// Helper functions. // Helper functions.
// ====================================================================== // ======================================================================
fn decode_bytes_sync(key: LoadKey, bytes: &[u8], cors: CorsStatus) -> DecoderMsg { fn parse_svg_document_in_memory(bytes: &[u8]) -> Result<usvg::Tree, &'static str> {
let image = load_from_memory(bytes, cors); let image_string_href_resolver = Box::new(move |_: &str, _: &usvg::Options| {
// Do not try to load `href` in <image> as local file path.
None
});
let mut opt = usvg::Options {
image_href_resolver: usvg::ImageHrefResolver {
resolve_data: usvg::ImageHrefResolver::default_data_resolver(),
resolve_string: image_string_href_resolver,
},
..usvg::Options::default()
};
opt.fontdb_mut().load_system_fonts();
usvg::Tree::from_data(bytes, &opt)
.inspect_err(|error| {
warn!("Error when parsing SVG data: {error}");
})
.map_err(|_| "Not a valid SVG document")
}
fn decode_bytes_sync(
key: LoadKey,
bytes: &[u8],
cors: CorsStatus,
content_type: Option<Mime>,
) -> DecoderMsg {
let image = if content_type == Some(mime::IMAGE_SVG) {
parse_svg_document_in_memory(bytes).ok().map(|svg_tree| {
DecodedImage::Vector(VectorImageData {
svg_tree: Arc::new(svg_tree),
cors_status: cors,
})
})
} else {
load_from_memory(bytes, cors).map(DecodedImage::Raster)
};
DecoderMsg { key, image } DecoderMsg { key, image }
} }
fn get_placeholder_image(compositor_api: &CrossProcessCompositorApi, data: &[u8]) -> Arc<Image> { fn get_placeholder_image(
compositor_api: &CrossProcessCompositorApi,
data: &[u8],
) -> Arc<RasterImage> {
let mut image = load_from_memory(data, CorsStatus::Unsafe) let mut image = load_from_memory(data, CorsStatus::Unsafe)
.or_else(|| load_from_memory(FALLBACK_RIPPY, CorsStatus::Unsafe)) .or_else(|| load_from_memory(FALLBACK_RIPPY, CorsStatus::Unsafe))
.expect("load fallback image failed"); .expect("load fallback image failed");
@ -61,15 +106,15 @@ fn get_placeholder_image(compositor_api: &CrossProcessCompositorApi, data: &[u8]
Arc::new(image) Arc::new(image)
} }
fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &mut Image) { fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &mut RasterImage) {
if image.id.is_some() { if image.id.is_some() {
return; return;
} }
let mut bytes = Vec::new(); let mut bytes = Vec::new();
let frame_bytes = image.bytes(); let frame_bytes = image.first_frame().bytes;
let is_opaque = match image.format { let is_opaque = match image.format {
PixelFormat::BGRA8 => { PixelFormat::BGRA8 | PixelFormat::RGBA8 => {
bytes.extend_from_slice(&frame_bytes); bytes.extend_from_slice(frame_bytes);
pixels::rgba8_premultiply_inplace(bytes.as_mut_slice()) pixels::rgba8_premultiply_inplace(bytes.as_mut_slice())
}, },
PixelFormat::RGB8 => { PixelFormat::RGB8 => {
@ -80,16 +125,24 @@ fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &m
true true
}, },
PixelFormat::K8 | PixelFormat::KA8 | PixelFormat::RGBA8 => { PixelFormat::K8 | PixelFormat::KA8 => {
panic!("Not support by webrender yet"); panic!("Not support by webrender yet");
}, },
}; };
let format = if matches!(image.format, PixelFormat::RGBA8) {
ImageFormat::RGBA8
} else {
ImageFormat::BGRA8
};
let mut flags = ImageDescriptorFlags::ALLOW_MIPMAPS; let mut flags = ImageDescriptorFlags::ALLOW_MIPMAPS;
flags.set(ImageDescriptorFlags::IS_OPAQUE, is_opaque); flags.set(ImageDescriptorFlags::IS_OPAQUE, is_opaque);
let size = DeviceIntSize::new(image.metadata.width as i32, image.metadata.height as i32);
let descriptor = ImageDescriptor { let descriptor = ImageDescriptor {
size: DeviceIntSize::new(image.width as i32, image.height as i32), size,
stride: None, stride: None,
format: ImageFormat::BGRA8, format,
offset: 0, offset: 0,
flags, flags,
}; };
@ -204,10 +257,22 @@ impl CompletedLoad {
} }
} }
#[derive(Clone, Debug, MallocSizeOf)]
struct VectorImageData {
#[conditional_malloc_size_of]
svg_tree: Arc<usvg::Tree>,
cors_status: CorsStatus,
}
enum DecodedImage {
Raster(RasterImage),
Vector(VectorImageData),
}
/// Message that the decoder worker threads send to the image cache. /// Message that the decoder worker threads send to the image cache.
struct DecoderMsg { struct DecoderMsg {
key: LoadKey, key: LoadKey,
image: Option<Image>, image: Option<DecodedImage>,
} }
#[derive(MallocSizeOf)] #[derive(MallocSizeOf)]
@ -265,8 +330,9 @@ impl LoadKeyGenerator {
#[derive(Debug)] #[derive(Debug)]
enum LoadResult { enum LoadResult {
Loaded(Image), LoadedRasterImage(RasterImage),
PlaceholderLoaded(Arc<Image>), LoadedVectorImage(VectorImageData),
PlaceholderLoaded(Arc<RasterImage>),
None, None,
} }
@ -285,7 +351,7 @@ struct PendingLoad {
result: Option<Result<(), NetworkError>>, result: Option<Result<(), NetworkError>>,
/// The listeners that are waiting for this response to complete. /// The listeners that are waiting for this response to complete.
listeners: Vec<ImageResponder>, listeners: Vec<ImageLoadListener>,
/// The url being loaded. Do not forget that this may be several Mb /// The url being loaded. Do not forget that this may be several Mb
/// if we are loading a data: url. /// if we are loading a data: url.
@ -302,6 +368,9 @@ struct PendingLoad {
/// The URL of the final response that contains a body. /// The URL of the final response that contains a body.
final_url: Option<ServoUrl>, final_url: Option<ServoUrl>,
/// The MIME type from the `Content-type` header of the HTTP response, if any.
content_type: Option<Mime>,
} }
impl PendingLoad { impl PendingLoad {
@ -320,33 +389,48 @@ impl PendingLoad {
final_url: None, final_url: None,
cors_setting, cors_setting,
cors_status: CorsStatus::Unsafe, cors_status: CorsStatus::Unsafe,
content_type: None,
} }
} }
fn add_listener(&mut self, listener: ImageResponder) { fn add_listener(&mut self, listener: ImageLoadListener) {
self.listeners.push(listener); self.listeners.push(listener);
} }
} }
// ====================================================================== #[derive(Default, MallocSizeOf)]
// Image cache implementation. struct RasterizationTask {
// ====================================================================== listeners: Vec<(PipelineId, IpcSender<ImageCacheResponseMessage>)>,
result: Option<RasterImage>,
}
/// ## Image cache implementation.
#[derive(MallocSizeOf)] #[derive(MallocSizeOf)]
struct ImageCacheStore { struct ImageCacheStore {
// Images that are loading over network, or decoding. /// Images that are loading over network, or decoding.
pending_loads: AllPendingLoads, pending_loads: AllPendingLoads,
// Images that have finished loading (successful or not) /// Images that have finished loading (successful or not)
completed_loads: HashMap<ImageKey, CompletedLoad>, completed_loads: HashMap<ImageKey, CompletedLoad>,
// The placeholder image used when an image fails to load /// Vector (e.g. SVG) images that have been sucessfully loaded and parsed
#[conditional_malloc_size_of] /// but are yet to be rasterized. Since the same SVG data can be used for
placeholder_image: Arc<Image>, /// rasterizing at different sizes, we use this hasmap to share the data.
vector_images: HashMap<PendingImageId, VectorImageData>,
// The URL used for the placeholder image /// Vector images for which rasterization at a particular size has started
/// or completed. If completed, the `result` member of `RasterizationTask`
/// contains the rasterized image.
rasterized_vector_images: HashMap<(PendingImageId, DeviceIntSize), RasterizationTask>,
/// The placeholder image used when an image fails to load
#[conditional_malloc_size_of]
placeholder_image: Arc<RasterImage>,
/// The URL used for the placeholder image
placeholder_url: ServoUrl, placeholder_url: ServoUrl,
// Cross-process compositor API instance. /// Cross-process compositor API instance.
#[ignore_malloc_size_of = "Channel from another crate"] #[ignore_malloc_size_of = "Channel from another crate"]
compositor_api: CrossProcessCompositorApi, compositor_api: CrossProcessCompositorApi,
} }
@ -361,15 +445,34 @@ impl ImageCacheStore {
}; };
match load_result { match load_result {
LoadResult::Loaded(ref mut image) => { LoadResult::LoadedRasterImage(ref mut raster_image) => {
set_webrender_image_key(&self.compositor_api, image) set_webrender_image_key(&self.compositor_api, raster_image)
},
LoadResult::LoadedVectorImage(ref vector_image) => {
self.vector_images.insert(key, vector_image.clone());
}, },
LoadResult::PlaceholderLoaded(..) | LoadResult::None => {}, LoadResult::PlaceholderLoaded(..) | LoadResult::None => {},
} }
let url = pending_load.final_url.clone(); let url = pending_load.final_url.clone();
let image_response = match load_result { let image_response = match load_result {
LoadResult::Loaded(image) => ImageResponse::Loaded(Arc::new(image), url.unwrap()), LoadResult::LoadedRasterImage(raster_image) => {
ImageResponse::Loaded(Image::Raster(Arc::new(raster_image)), url.unwrap())
},
LoadResult::LoadedVectorImage(vector_image) => {
let natural_dimensions = vector_image.svg_tree.size().to_int_size();
let metadata = ImageMetadata {
width: natural_dimensions.width(),
height: natural_dimensions.height(),
};
let vector_image = VectorImage {
id: key,
metadata,
cors_status: vector_image.cors_status,
};
ImageResponse::Loaded(Image::Vector(vector_image), url.unwrap())
},
LoadResult::PlaceholderLoaded(image) => { LoadResult::PlaceholderLoaded(image) => {
ImageResponse::PlaceholderLoaded(image, self.placeholder_url.clone()) ImageResponse::PlaceholderLoaded(image, self.placeholder_url.clone())
}, },
@ -399,19 +502,18 @@ impl ImageCacheStore {
origin: ImmutableOrigin, origin: ImmutableOrigin,
cors_setting: Option<CorsSettings>, cors_setting: Option<CorsSettings>,
placeholder: UsePlaceholder, placeholder: UsePlaceholder,
) -> Option<Result<(Arc<Image>, ServoUrl), ()>> { ) -> Option<Result<(Image, ServoUrl), ()>> {
self.completed_loads self.completed_loads
.get(&(url, origin, cors_setting)) .get(&(url, origin, cors_setting))
.map( .map(
|completed_load| match (&completed_load.image_response, placeholder) { |completed_load| match (&completed_load.image_response, placeholder) {
(&ImageResponse::Loaded(ref image, ref url), _) | (ImageResponse::Loaded(image, url), _) => Ok((image.clone(), url.clone())),
( (ImageResponse::PlaceholderLoaded(image, url), UsePlaceholder::Yes) => {
&ImageResponse::PlaceholderLoaded(ref image, ref url), Ok((Image::Raster(image.clone()), url.clone()))
UsePlaceholder::Yes, },
) => Ok((image.clone(), url.clone())), (ImageResponse::PlaceholderLoaded(_, _), UsePlaceholder::No) |
(&ImageResponse::PlaceholderLoaded(_, _), UsePlaceholder::No) | (ImageResponse::None, _) |
(&ImageResponse::None, _) | (ImageResponse::MetadataLoaded(_), _) => Err(()),
(&ImageResponse::MetadataLoaded(_), _) => Err(()),
}, },
) )
} }
@ -421,7 +523,10 @@ impl ImageCacheStore {
fn handle_decoder(&mut self, msg: DecoderMsg) { fn handle_decoder(&mut self, msg: DecoderMsg) {
let image = match msg.image { let image = match msg.image {
None => LoadResult::None, None => LoadResult::None,
Some(image) => LoadResult::Loaded(image), Some(DecodedImage::Raster(raster_image)) => LoadResult::LoadedRasterImage(raster_image),
Some(DecodedImage::Vector(vector_image_data)) => {
LoadResult::LoadedVectorImage(vector_image_data)
},
}; };
self.complete_load(msg.key, image); self.complete_load(msg.key, image);
} }
@ -450,6 +555,8 @@ impl ImageCache for ImageCacheImpl {
store: Arc::new(Mutex::new(ImageCacheStore { store: Arc::new(Mutex::new(ImageCacheStore {
pending_loads: AllPendingLoads::new(), pending_loads: AllPendingLoads::new(),
completed_loads: HashMap::new(), completed_loads: HashMap::new(),
vector_images: HashMap::new(),
rasterized_vector_images: HashMap::new(),
placeholder_image: get_placeholder_image(&compositor_api, &rippy_data), placeholder_image: get_placeholder_image(&compositor_api, &rippy_data),
placeholder_url: ServoUrl::parse("chrome://resources/rippy.png").unwrap(), placeholder_url: ServoUrl::parse("chrome://resources/rippy.png").unwrap(),
compositor_api, compositor_api,
@ -475,7 +582,7 @@ impl ImageCache for ImageCacheImpl {
url: ServoUrl, url: ServoUrl,
origin: ImmutableOrigin, origin: ImmutableOrigin,
cors_setting: Option<CorsSettings>, cors_setting: Option<CorsSettings>,
) -> Option<Arc<Image>> { ) -> Option<Image> {
let store = self.store.lock().unwrap(); let store = self.store.lock().unwrap();
let result = let result =
store.get_completed_image_if_available(url, origin, cors_setting, UsePlaceholder::No); store.get_completed_image_if_available(url, origin, cors_setting, UsePlaceholder::No);
@ -524,12 +631,17 @@ impl ImageCache for ImageCacheImpl {
CacheResult::Hit(key, pl) => match (&pl.result, &pl.metadata) { CacheResult::Hit(key, pl) => match (&pl.result, &pl.metadata) {
(&Some(Ok(_)), _) => { (&Some(Ok(_)), _) => {
debug!("Sync decoding {} ({:?})", url, key); debug!("Sync decoding {} ({:?})", url, key);
decode_bytes_sync(key, pl.bytes.as_slice(), pl.cors_status) decode_bytes_sync(
key,
pl.bytes.as_slice(),
pl.cors_status,
pl.content_type.clone(),
)
}, },
(&None, Some(meta)) => { (&None, Some(meta)) => {
debug!("Metadata available for {} ({:?})", url, key); debug!("Metadata available for {} ({:?})", url, key);
return ImageCacheResult::Available( return ImageCacheResult::Available(
ImageOrMetadataAvailable::MetadataAvailable(meta.clone(), key), ImageOrMetadataAvailable::MetadataAvailable(*meta, key),
); );
}, },
(&Some(Err(_)), _) | (&None, &None) => { (&Some(Err(_)), _) | (&None, &None) => {
@ -566,9 +678,137 @@ impl ImageCache for ImageCacheImpl {
} }
} }
fn add_rasterization_complete_listener(
&self,
pipeline_id: PipelineId,
image_id: PendingImageId,
requested_size: DeviceIntSize,
sender: IpcSender<ImageCacheResponseMessage>,
) {
let completed = {
let mut store = self.store.lock().unwrap();
let key = (image_id, requested_size);
if !store.vector_images.contains_key(&image_id) {
warn!("Unknown image requested for rasterization for key {key:?}");
return;
};
let Some(task) = store.rasterized_vector_images.get_mut(&key) else {
warn!("Image rasterization task not found in the cache for key {key:?}");
return;
};
match task.result {
Some(_) => true,
None => {
task.listeners.push((pipeline_id, sender.clone()));
false
},
}
};
if completed {
let _ = sender.send(ImageCacheResponseMessage::VectorImageRasterizationComplete(
RasterizationCompleteResponse {
pipeline_id,
image_id,
requested_size,
},
));
}
}
fn rasterize_vector_image(
&self,
image_id: PendingImageId,
requested_size: DeviceIntSize,
) -> Option<RasterImage> {
let mut store = self.store.lock().unwrap();
let Some(vector_image) = store.vector_images.get(&image_id).cloned() else {
warn!("Unknown image id {image_id:?} requested for rasterization");
return None;
};
// This early return relies on the fact that the result of image rasterization cannot
// ever be `None`. If that were the case we would need to check whether the entry
// in the `HashMap` was `Occupied` or not.
let entry = store
.rasterized_vector_images
.entry((image_id, requested_size))
.or_default();
if let Some(result) = entry.result.as_ref() {
return Some(result.clone());
}
let store = self.store.clone();
self.thread_pool.spawn(move || {
let natural_size = vector_image.svg_tree.size().to_int_size();
let tinyskia_requested_size = {
let width = requested_size.width.try_into().unwrap_or(0);
let height = requested_size.height.try_into().unwrap_or(0);
tiny_skia::IntSize::from_wh(width, height).unwrap_or(natural_size)
};
let transform = tiny_skia::Transform::from_scale(
tinyskia_requested_size.width() as f32 / natural_size.width() as f32,
tinyskia_requested_size.height() as f32 / natural_size.height() as f32,
);
let mut pixmap = tiny_skia::Pixmap::new(
tinyskia_requested_size.width(),
tinyskia_requested_size.height(),
)
.unwrap();
resvg::render(&vector_image.svg_tree, transform, &mut pixmap.as_mut());
let bytes = pixmap.take();
let frame = ImageFrame {
delay: None,
byte_range: 0..bytes.len(),
width: tinyskia_requested_size.width(),
height: tinyskia_requested_size.height(),
};
let mut rasterized_image = RasterImage {
metadata: ImageMetadata {
width: tinyskia_requested_size.width(),
height: tinyskia_requested_size.height(),
},
format: PixelFormat::RGBA8,
frames: vec![frame],
bytes: IpcSharedMemory::from_bytes(&bytes),
id: None,
cors_status: vector_image.cors_status,
};
let listeners = {
let mut store = store.lock().unwrap();
set_webrender_image_key(&store.compositor_api, &mut rasterized_image);
store
.rasterized_vector_images
.get_mut(&(image_id, requested_size))
.map(|task| {
task.result = Some(rasterized_image);
std::mem::take(&mut task.listeners)
})
.unwrap_or_default()
};
for (pipeline_id, sender) in listeners {
let _ = sender.send(ImageCacheResponseMessage::VectorImageRasterizationComplete(
RasterizationCompleteResponse {
pipeline_id,
image_id,
requested_size,
},
));
}
});
None
}
/// Add a new listener for the given pending image id. If the image is already present, /// Add a new listener for the given pending image id. If the image is already present,
/// the responder will still receive the expected response. /// the responder will still receive the expected response.
fn add_listener(&self, listener: ImageResponder) { fn add_listener(&self, listener: ImageLoadListener) {
let mut store = self.store.lock().unwrap(); let mut store = self.store.lock().unwrap();
self.add_listener_with_store(&mut store, listener); self.add_listener_with_store(&mut store, listener);
} }
@ -603,6 +843,10 @@ impl ImageCache for ImageCacheImpl {
let final_url = metadata.as_ref().map(|m| m.final_url.clone()); let final_url = metadata.as_ref().map(|m| m.final_url.clone());
pending_load.final_url = final_url; pending_load.final_url = final_url;
pending_load.cors_status = cors_status; pending_load.cors_status = cors_status;
pending_load.content_type = metadata
.as_ref()
.and_then(|metadata| metadata.content_type.clone())
.map(|content_type| content_type.into_inner().into());
}, },
(FetchResponseMsg::ProcessResponseChunk(_, data), _) => { (FetchResponseMsg::ProcessResponseChunk(_, data), _) => {
debug!("Got some data for {:?}", id); debug!("Got some data for {:?}", id);
@ -619,7 +863,7 @@ impl ImageCache for ImageCacheImpl {
height: info.height as u32, height: info.height as u32,
}; };
for listener in &pending_load.listeners { for listener in &pending_load.listeners {
listener.respond(ImageResponse::MetadataLoaded(img_metadata.clone())); listener.respond(ImageResponse::MetadataLoaded(img_metadata));
} }
pending_load.metadata = Some(img_metadata); pending_load.metadata = Some(img_metadata);
} }
@ -629,17 +873,21 @@ impl ImageCache for ImageCacheImpl {
debug!("Received EOF for {:?}", key); debug!("Received EOF for {:?}", key);
match result { match result {
Ok(_) => { Ok(_) => {
let (bytes, cors_status) = { let (bytes, cors_status, content_type) = {
let mut store = self.store.lock().unwrap(); let mut store = self.store.lock().unwrap();
let pending_load = store.pending_loads.get_by_key_mut(&id).unwrap(); let pending_load = store.pending_loads.get_by_key_mut(&id).unwrap();
pending_load.result = Some(Ok(())); pending_load.result = Some(Ok(()));
debug!("Async decoding {} ({:?})", pending_load.url, key); debug!("Async decoding {} ({:?})", pending_load.url, key);
(pending_load.bytes.mark_complete(), pending_load.cors_status) (
pending_load.bytes.mark_complete(),
pending_load.cors_status,
pending_load.content_type.clone(),
)
}; };
let local_store = self.store.clone(); let local_store = self.store.clone();
self.thread_pool.spawn(move || { self.thread_pool.spawn(move || {
let msg = decode_bytes_sync(key, &bytes, cors_status); let msg = decode_bytes_sync(key, &bytes, cors_status, content_type);
debug!("Image decoded"); debug!("Image decoded");
local_store.lock().unwrap().handle_decoder(msg); local_store.lock().unwrap().handle_decoder(msg);
}); });
@ -669,6 +917,8 @@ impl ImageCache for ImageCacheImpl {
placeholder_image, placeholder_image,
placeholder_url, placeholder_url,
compositor_api, compositor_api,
vector_images: HashMap::new(),
rasterized_vector_images: HashMap::new(),
})), })),
thread_pool: self.thread_pool.clone(), thread_pool: self.thread_pool.clone(),
}) })
@ -681,9 +931,16 @@ impl Drop for ImageCacheStore {
.completed_loads .completed_loads
.values() .values()
.filter_map(|load| match &load.image_response { .filter_map(|load| match &load.image_response {
ImageResponse::Loaded(image, _) => image.id.map(ImageUpdate::DeleteImage), ImageResponse::Loaded(Image::Raster(image), _) => {
image.id.map(ImageUpdate::DeleteImage)
},
_ => None, _ => None,
}) })
.chain(
self.rasterized_vector_images
.values()
.filter_map(|task| task.result.as_ref()?.id.map(ImageUpdate::DeleteImage)),
)
.collect(); .collect();
self.compositor_api.update_images(image_updates); self.compositor_api.update_images(image_updates);
} }
@ -691,11 +948,11 @@ impl Drop for ImageCacheStore {
impl ImageCacheImpl { impl ImageCacheImpl {
/// Require self.store.lock() before calling. /// Require self.store.lock() before calling.
fn add_listener_with_store(&self, store: &mut ImageCacheStore, listener: ImageResponder) { fn add_listener_with_store(&self, store: &mut ImageCacheStore, listener: ImageLoadListener) {
let id = listener.id; let id = listener.id;
if let Some(load) = store.pending_loads.get_by_key_mut(&id) { if let Some(load) = store.pending_loads.get_by_key_mut(&id) {
if let Some(ref metadata) = load.metadata { if let Some(ref metadata) = load.metadata {
listener.respond(ImageResponse::MetadataLoaded(metadata.clone())); listener.respond(ImageResponse::MetadataLoaded(*metadata));
} }
load.add_listener(listener); load.add_listener(listener);
return; return;

View file

@ -16,7 +16,6 @@ pub mod http_cache;
pub mod http_loader; pub mod http_loader;
pub mod image_cache; pub mod image_cache;
pub mod local_directory_listing; pub mod local_directory_listing;
pub mod mime_classifier;
pub mod protocols; pub mod protocols;
pub mod request_interceptor; pub mod request_interceptor;
pub mod resource_thread; pub mod resource_thread;

View file

@ -21,9 +21,9 @@ use embedder_traits::EmbedderProxy;
use hyper_serde::Serde; use hyper_serde::Serde;
use ipc_channel::ipc::{self, IpcReceiver, IpcReceiverSet, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcReceiverSet, IpcSender};
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use malloc_size_of::MallocSizeOf;
use net_traits::blob_url_store::parse_blob_url; use net_traits::blob_url_store::parse_blob_url;
use net_traits::filemanager_thread::FileTokenCheck; use net_traits::filemanager_thread::FileTokenCheck;
use net_traits::pub_domains::public_suffix_list_size_of;
use net_traits::request::{Destination, RequestBuilder, RequestId}; use net_traits::request::{Destination, RequestBuilder, RequestId};
use net_traits::response::{Response, ResponseInit}; use net_traits::response::{Response, ResponseInit};
use net_traits::storage_thread::StorageThreadMsg; use net_traits::storage_thread::StorageThreadMsg;
@ -97,14 +97,15 @@ pub fn new_resource_threads(
let (public_core, private_core) = new_core_resource_thread( let (public_core, private_core) = new_core_resource_thread(
devtools_sender, devtools_sender,
time_profiler_chan, time_profiler_chan,
mem_profiler_chan, mem_profiler_chan.clone(),
embedder_proxy, embedder_proxy,
config_dir.clone(), config_dir.clone(),
ca_certificates, ca_certificates,
ignore_certificate_errors, ignore_certificate_errors,
protocols, protocols,
); );
let storage: IpcSender<StorageThreadMsg> = StorageThreadFactory::new(config_dir); let storage: IpcSender<StorageThreadMsg> =
StorageThreadFactory::new(config_dir, mem_profiler_chan);
( (
ResourceThreads::new(public_core, storage.clone()), ResourceThreads::new(public_core, storage.clone()),
ResourceThreads::new(private_core, storage), ResourceThreads::new(private_core, storage),
@ -287,11 +288,18 @@ impl ResourceChannelManager {
perform_memory_report(|ops| { perform_memory_report(|ops| {
let mut reports = public_http_state.memory_reports("public", ops); let mut reports = public_http_state.memory_reports("public", ops);
reports.extend(private_http_state.memory_reports("private", ops)); reports.extend(private_http_state.memory_reports("private", ops));
reports.push(Report { reports.extend(vec![
Report {
path: path!["hsts-preload-list"], path: path!["hsts-preload-list"],
kind: ReportKind::ExplicitJemallocHeapSize, kind: ReportKind::ExplicitJemallocHeapSize,
size: hsts::PRELOAD_LIST_ENTRIES.size_of(ops), size: hsts::hsts_preload_size_of(ops),
}); },
Report {
path: path!["public-suffix-list"],
kind: ReportKind::ExplicitJemallocHeapSize,
size: public_suffix_list_size_of(ops),
},
]);
msg.send(ProcessReports::new(reports)); msg.send(ProcessReports::new(reports));
}) })
} }
@ -445,9 +453,6 @@ impl ResourceChannelManager {
history_states.remove(&history_state); history_states.remove(&history_state);
} }
}, },
CoreResourceMsg::Synchronize(sender) => {
let _ = sender.send(());
},
CoreResourceMsg::ClearCache => { CoreResourceMsg::ClearCache => {
http_state.http_cache.write().unwrap().clear(); http_state.http_cache.write().unwrap().clear();
}, },

View file

@ -8,7 +8,12 @@ use std::path::PathBuf;
use std::thread; use std::thread;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use malloc_size_of::MallocSizeOf;
use net_traits::storage_thread::{StorageThreadMsg, StorageType}; use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use profile_traits::mem::{
ProcessReports, ProfilerChan as MemProfilerChan, Report, ReportKind, perform_memory_report,
};
use profile_traits::path;
use servo_url::ServoUrl; use servo_url::ServoUrl;
use crate::resource_thread; use crate::resource_thread;
@ -16,17 +21,26 @@ use crate::resource_thread;
const QUOTA_SIZE_LIMIT: usize = 5 * 1024 * 1024; const QUOTA_SIZE_LIMIT: usize = 5 * 1024 * 1024;
pub trait StorageThreadFactory { pub trait StorageThreadFactory {
fn new(config_dir: Option<PathBuf>) -> Self; fn new(config_dir: Option<PathBuf>, mem_profiler_chan: MemProfilerChan) -> Self;
} }
impl StorageThreadFactory for IpcSender<StorageThreadMsg> { impl StorageThreadFactory for IpcSender<StorageThreadMsg> {
/// Create a storage thread /// Create a storage thread
fn new(config_dir: Option<PathBuf>) -> IpcSender<StorageThreadMsg> { fn new(
config_dir: Option<PathBuf>,
mem_profiler_chan: MemProfilerChan,
) -> IpcSender<StorageThreadMsg> {
let (chan, port) = ipc::channel().unwrap(); let (chan, port) = ipc::channel().unwrap();
let chan2 = chan.clone();
thread::Builder::new() thread::Builder::new()
.name("StorageManager".to_owned()) .name("StorageManager".to_owned())
.spawn(move || { .spawn(move || {
StorageManager::new(port, config_dir).start(); mem_profiler_chan.run_with_memory_reporting(
|| StorageManager::new(port, config_dir).start(),
String::from("storage-reporter"),
chan2,
StorageThreadMsg::CollectMemoryReport,
);
}) })
.expect("Thread spawning failed"); .expect("Thread spawning failed");
chan chan
@ -83,6 +97,10 @@ impl StorageManager {
self.clear(sender, url, storage_type); self.clear(sender, url, storage_type);
self.save_state() self.save_state()
}, },
StorageThreadMsg::CollectMemoryReport(sender) => {
let reports = self.collect_memory_reports();
sender.send(ProcessReports::new(reports));
},
StorageThreadMsg::Exit(sender) => { StorageThreadMsg::Exit(sender) => {
// Nothing to do since we save localstorage set eagerly. // Nothing to do since we save localstorage set eagerly.
let _ = sender.send(()); let _ = sender.send(());
@ -92,6 +110,24 @@ impl StorageManager {
} }
} }
fn collect_memory_reports(&self) -> Vec<Report> {
let mut reports = vec![];
perform_memory_report(|ops| {
reports.push(Report {
path: path!["storage", "local"],
kind: ReportKind::ExplicitJemallocHeapSize,
size: self.local_data.size_of(ops),
});
reports.push(Report {
path: path!["storage", "session"],
kind: ReportKind::ExplicitJemallocHeapSize,
size: self.session_data.size_of(ops),
});
});
reports
}
fn save_state(&self) { fn save_state(&self) {
if let Some(ref config_dir) = self.config_dir { if let Some(ref config_dir) = self.config_dir {
resource_thread::write_json_to_file(&self.local_data, config_dir, "local_data.json"); resource_thread::write_json_to_file(&self.local_data, config_dir, "local_data.json");

View file

@ -6,13 +6,14 @@ use std::collections::HashMap;
use std::num::NonZeroU64; use std::num::NonZeroU64;
use std::time::Duration as StdDuration; use std::time::Duration as StdDuration;
use base64::Engine;
use net::hsts::{HstsEntry, HstsList, HstsPreloadList}; use net::hsts::{HstsEntry, HstsList, HstsPreloadList};
use net_traits::IncludeSubdomains; use net_traits::IncludeSubdomains;
#[test] #[test]
fn test_hsts_entry_is_not_expired_when_it_has_no_expires_at() { fn test_hsts_entry_is_not_expired_when_it_has_no_expires_at() {
let entry = HstsEntry { let entry = HstsEntry {
host: "mozilla.org".to_owned(), host: "example.com".to_owned(),
include_subdomains: false, include_subdomains: false,
expires_at: None, expires_at: None,
}; };
@ -23,7 +24,7 @@ fn test_hsts_entry_is_not_expired_when_it_has_no_expires_at() {
#[test] #[test]
fn test_hsts_entry_is_expired_when_it_has_reached_its_max_age() { fn test_hsts_entry_is_expired_when_it_has_reached_its_max_age() {
let entry = HstsEntry { let entry = HstsEntry {
host: "mozilla.org".to_owned(), host: "example.com".to_owned(),
include_subdomains: false, include_subdomains: false,
expires_at: Some(NonZeroU64::new(1).unwrap()), expires_at: Some(NonZeroU64::new(1).unwrap()),
}; };
@ -59,7 +60,7 @@ fn test_base_domain_in_entries_map() {
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"servo.mozilla.org".to_owned(), "servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -67,7 +68,7 @@ fn test_base_domain_in_entries_map() {
); );
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"firefox.mozilla.org".to_owned(), "firefox.example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -75,7 +76,7 @@ fn test_base_domain_in_entries_map() {
); );
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"bugzilla.org".to_owned(), "example.org".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -83,17 +84,17 @@ fn test_base_domain_in_entries_map() {
); );
assert_eq!(list.entries_map.len(), 2); assert_eq!(list.entries_map.len(), 2);
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 2); assert_eq!(list.entries_map.get("example.com").unwrap().len(), 2);
} }
#[test] #[test]
fn test_push_entry_with_0_max_age_is_not_secure() { fn test_push_entry_with_0_max_age_is_not_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
Some(StdDuration::from_secs(500000)), Some(StdDuration::from_secs(500000)),
) )
@ -106,23 +107,23 @@ fn test_push_entry_with_0_max_age_is_not_secure() {
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
Some(StdDuration::ZERO), Some(StdDuration::ZERO),
) )
.unwrap(), .unwrap(),
); );
assert_eq!(list.is_host_secure("mozilla.org"), false) assert_eq!(list.is_host_secure("example.com"), false)
} }
fn test_push_entry_with_0_max_age_evicts_entry_from_list() { fn test_push_entry_with_0_max_age_evicts_entry_from_list() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
Some(StdDuration::from_secs(500000)), Some(StdDuration::from_secs(500000)),
) )
@ -133,25 +134,25 @@ fn test_push_entry_with_0_max_age_evicts_entry_from_list() {
entries_map: entries_map, entries_map: entries_map,
}; };
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1); assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1);
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
Some(StdDuration::ZERO), Some(StdDuration::ZERO),
) )
.unwrap(), .unwrap(),
); );
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 0); assert_eq!(list.entries_map.get("example.com").unwrap().len(), 0);
} }
#[test] #[test]
fn test_push_entry_to_hsts_list_should_not_add_subdomains_whose_superdomain_is_already_matched() { fn test_push_entry_to_hsts_list_should_not_add_subdomains_whose_superdomain_is_already_matched() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()], vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
); );
let mut list = HstsList { let mut list = HstsList {
entries_map: entries_map, entries_map: entries_map,
@ -159,24 +160,24 @@ fn test_push_entry_to_hsts_list_should_not_add_subdomains_whose_superdomain_is_a
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"servo.mozilla.org".to_owned(), "servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
.unwrap(), .unwrap(),
); );
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1) assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1)
} }
#[test] #[test]
fn test_push_entry_to_hsts_list_should_add_subdomains_whose_superdomain_doesnt_include() { fn test_push_entry_to_hsts_list_should_add_subdomains_whose_superdomain_doesnt_include() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -189,49 +190,49 @@ fn test_push_entry_to_hsts_list_should_add_subdomains_whose_superdomain_doesnt_i
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"servo.mozilla.org".to_owned(), "servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
.unwrap(), .unwrap(),
); );
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 2) assert_eq!(list.entries_map.get("example.com").unwrap().len(), 2)
} }
#[test] #[test]
fn test_push_entry_to_hsts_list_should_update_existing_domain_entrys_include_subdomains() { fn test_push_entry_to_hsts_list_should_update_existing_domain_entrys_include_subdomains() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()], vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
); );
let mut list = HstsList { let mut list = HstsList {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(list.is_host_secure("servo.mozilla.org")); assert!(list.is_host_secure("servo.example.com"));
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
.unwrap(), .unwrap(),
); );
assert!(!list.is_host_secure("servo.mozilla.org")) assert!(!list.is_host_secure("servo.example.com"))
} }
#[test] #[test]
fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() { fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -244,14 +245,14 @@ fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
list.push( list.push(
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
.unwrap(), .unwrap(),
); );
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1) assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1)
} }
#[test] #[test]
@ -260,16 +261,14 @@ fn test_push_multiple_entrie_to_hsts_list_should_add_them_all() {
entries_map: HashMap::new(), entries_map: HashMap::new(),
}; };
assert!(!list.is_host_secure("mozilla.org")); assert!(!list.is_host_secure("example.com"));
assert!(!list.is_host_secure("bugzilla.org")); assert!(!list.is_host_secure("example.org"));
list.push(HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()); list.push(HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap());
list.push( list.push(HstsEntry::new("example.org".to_owned(), IncludeSubdomains::Included, None).unwrap());
HstsEntry::new("bugzilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap(),
);
assert!(list.is_host_secure("mozilla.org")); assert!(list.is_host_secure("example.com"));
assert!(list.is_host_secure("bugzilla.org")); assert!(list.is_host_secure("example.org"));
} }
#[test] #[test]
@ -278,25 +277,16 @@ fn test_push_entry_to_hsts_list_should_add_an_entry() {
entries_map: HashMap::new(), entries_map: HashMap::new(),
}; };
assert!(!list.is_host_secure("mozilla.org")); assert!(!list.is_host_secure("example.com"));
list.push(HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()); list.push(HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap());
assert!(list.is_host_secure("mozilla.org")); assert!(list.is_host_secure("example.com"));
} }
#[test] #[test]
fn test_parse_hsts_preload_should_return_none_when_json_invalid() { fn test_parse_hsts_preload_should_return_none_when_json_invalid() {
let mock_preload_content = "derp"; let mock_preload_content = "derp".as_bytes().to_vec();
assert!(
HstsPreloadList::from_preload(mock_preload_content).is_none(),
"invalid preload list should not have parsed"
)
}
#[test]
fn test_parse_hsts_preload_should_return_none_when_json_contains_no_entries_map_key() {
let mock_preload_content = "{\"nothing\": \"to see here\"}";
assert!( assert!(
HstsPreloadList::from_preload(mock_preload_content).is_none(), HstsPreloadList::from_preload(mock_preload_content).is_none(),
"invalid preload list should not have parsed" "invalid preload list should not have parsed"
@ -305,20 +295,17 @@ fn test_parse_hsts_preload_should_return_none_when_json_contains_no_entries_map_
#[test] #[test]
fn test_parse_hsts_preload_should_decode_host_and_includes_subdomains() { fn test_parse_hsts_preload_should_decode_host_and_includes_subdomains() {
let mock_preload_content = "{\ // Generated with `fst map --sorted` on a csv of "example.com,0\nexample.org,3"
\"entries\": [\ let mock_preload_content = base64::engine::general_purpose::STANDARD
{\"host\": \"mozilla.org\",\ .decode("AwAAAAAAAAAAAAAAAAAAAAAQkMQAEJfHAwABBW9jEQLNws/J0MXqwgIAAAAAAAAAJwAAAAAAAADVOFe6")
\"include_subdomains\": false}\ .unwrap();
]\ let hsts_list = HstsPreloadList::from_preload(mock_preload_content).unwrap();
}";
let hsts_list = HstsPreloadList::from_preload(mock_preload_content);
let entries_map = hsts_list.unwrap().entries_map;
assert_eq!( assert_eq!(hsts_list.is_host_secure("derp"), false);
entries_map.get("mozilla.org").unwrap()[0].host, assert_eq!(hsts_list.is_host_secure("example.com"), true);
"mozilla.org" assert_eq!(hsts_list.is_host_secure("servo.example.com"), false);
); assert_eq!(hsts_list.is_host_secure("example.org"), true);
assert!(!entries_map.get("mozilla.org").unwrap()[0].include_subdomains); assert_eq!(hsts_list.is_host_secure("servo.example.org"), true);
} }
#[test] #[test]
@ -327,17 +314,17 @@ fn test_hsts_list_with_no_entries_map_does_not_is_host_secure() {
entries_map: HashMap::new(), entries_map: HashMap::new(),
}; };
assert!(!hsts_list.is_host_secure("mozilla.org")); assert!(!hsts_list.is_host_secure("example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_exact_domain_entry_is_is_host_secure() { fn test_hsts_list_with_exact_domain_entry_is_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -349,31 +336,31 @@ fn test_hsts_list_with_exact_domain_entry_is_is_host_secure() {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(hsts_list.is_host_secure("mozilla.org")); assert!(hsts_list.is_host_secure("example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_subdomain_when_include_subdomains_is_true_is_is_host_secure() { fn test_hsts_list_with_subdomain_when_include_subdomains_is_true_is_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()], vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
); );
let hsts_list = HstsList { let hsts_list = HstsList {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(hsts_list.is_host_secure("servo.mozilla.org")); assert!(hsts_list.is_host_secure("servo.example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_subdomain_when_include_subdomains_is_false_is_not_is_host_secure() { fn test_hsts_list_with_subdomain_when_include_subdomains_is_false_is_not_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![ vec![
HstsEntry::new( HstsEntry::new(
"mozilla.org".to_owned(), "example.com".to_owned(),
IncludeSubdomains::NotIncluded, IncludeSubdomains::NotIncluded,
None, None,
) )
@ -384,44 +371,44 @@ fn test_hsts_list_with_subdomain_when_include_subdomains_is_false_is_not_is_host
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(!hsts_list.is_host_secure("servo.mozilla.org")); assert!(!hsts_list.is_host_secure("servo.example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_subdomain_when_host_is_not_a_subdomain_is_not_is_host_secure() { fn test_hsts_list_with_subdomain_when_host_is_not_a_subdomain_is_not_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()], vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
); );
let hsts_list = HstsList { let hsts_list = HstsList {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(!hsts_list.is_host_secure("servo-mozilla.org")); assert!(!hsts_list.is_host_secure("servo-example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_subdomain_when_host_is_exact_match_is_is_host_secure() { fn test_hsts_list_with_subdomain_when_host_is_exact_match_is_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()], vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
); );
let hsts_list = HstsList { let hsts_list = HstsList {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(hsts_list.is_host_secure("mozilla.org")); assert!(hsts_list.is_host_secure("example.com"));
} }
#[test] #[test]
fn test_hsts_list_with_expired_entry_is_not_is_host_secure() { fn test_hsts_list_with_expired_entry_is_not_is_host_secure() {
let mut entries_map = HashMap::new(); let mut entries_map = HashMap::new();
entries_map.insert( entries_map.insert(
"mozilla.org".to_owned(), "example.com".to_owned(),
vec![HstsEntry { vec![HstsEntry {
host: "mozilla.org".to_owned(), host: "example.com".to_owned(),
include_subdomains: false, include_subdomains: false,
expires_at: Some(NonZeroU64::new(1).unwrap()), expires_at: Some(NonZeroU64::new(1).unwrap()),
}], }],
@ -430,11 +417,11 @@ fn test_hsts_list_with_expired_entry_is_not_is_host_secure() {
entries_map: entries_map, entries_map: entries_map,
}; };
assert!(!hsts_list.is_host_secure("mozilla.org")); assert!(!hsts_list.is_host_secure("example.com"));
} }
#[test] #[test]
fn test_preload_hsts_domains_well_formed() { fn test_preload_hsts_domains_well_formed() {
let hsts_list = HstsPreloadList::from_servo_preload(); let hsts_list = HstsPreloadList::from_servo_preload();
assert!(!hsts_list.entries_map.is_empty()); assert_ne!(hsts_list.0.len(), 0);
} }

View file

@ -14,7 +14,6 @@ mod filemanager_thread;
mod hsts; mod hsts;
mod http_cache; mod http_cache;
mod http_loader; mod http_loader;
mod mime_classifier;
mod resource_thread; mod resource_thread;
mod subresource_integrity; mod subresource_integrity;

View file

@ -4,6 +4,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::io::Cursor; use std::io::Cursor;
use std::ops::Range;
use std::time::Duration; use std::time::Duration;
use std::{cmp, fmt, vec}; use std::{cmp, fmt, vec};
@ -30,6 +31,20 @@ pub enum PixelFormat {
BGRA8, BGRA8,
} }
// Computes image byte length, returning None if overflow occurred or the total length exceeds
// the maximum image allocation size.
pub fn compute_rgba8_byte_length_if_within_limit(width: usize, height: usize) -> Option<usize> {
// Maximum allowed image allocation size (2^31-1 ~ 2GB).
const MAX_IMAGE_BYTE_LENGTH: usize = 2147483647;
// The color components of each pixel must be stored in four sequential
// elements in the order of red, green, blue, and then alpha.
4usize
.checked_mul(width)
.and_then(|v| v.checked_mul(height))
.filter(|v| *v <= MAX_IMAGE_BYTE_LENGTH)
}
pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u64>, rect: Rect<u64>) -> Cow<[u8]> { pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u64>, rect: Rect<u64>) -> Cow<[u8]> {
assert!(!rect.is_empty()); assert!(!rect.is_empty());
assert!(Rect::from_size(size).contains_rect(&rect)); assert!(Rect::from_size(size).contains_rect(&rect));
@ -120,48 +135,65 @@ pub enum CorsStatus {
} }
#[derive(Clone, Deserialize, MallocSizeOf, Serialize)] #[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
pub struct Image { pub struct RasterImage {
pub width: u32, pub metadata: ImageMetadata,
pub height: u32,
pub format: PixelFormat, pub format: PixelFormat,
pub id: Option<ImageKey>, pub id: Option<ImageKey>,
pub cors_status: CorsStatus, pub cors_status: CorsStatus,
pub bytes: IpcSharedMemory,
pub frames: Vec<ImageFrame>, pub frames: Vec<ImageFrame>,
} }
#[derive(Clone, Deserialize, MallocSizeOf, Serialize)] #[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
pub struct ImageFrame { pub struct ImageFrame {
pub delay: Option<Duration>, pub delay: Option<Duration>,
pub bytes: IpcSharedMemory, /// References a range of the `bytes` field from the image that this
/// frame belongs to.
pub byte_range: Range<usize>,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
} }
impl Image { /// A non-owning reference to the data of an [ImageFrame]
pub struct ImageFrameView<'a> {
pub delay: Option<Duration>,
pub bytes: &'a [u8],
pub width: u32,
pub height: u32,
}
impl RasterImage {
pub fn should_animate(&self) -> bool { pub fn should_animate(&self) -> bool {
self.frames.len() > 1 self.frames.len() > 1
} }
pub fn bytes(&self) -> IpcSharedMemory { pub fn frames(&self) -> impl Iterator<Item = ImageFrameView> {
self.frames self.frames.iter().map(|frame| ImageFrameView {
.first() delay: frame.delay,
.expect("Should have at least one frame") bytes: self.bytes.get(frame.byte_range.clone()).unwrap(),
.bytes width: frame.width,
.clone() height: frame.height,
})
}
pub fn first_frame(&self) -> ImageFrameView {
self.frames()
.next()
.expect("All images should have at least one frame")
} }
} }
impl fmt::Debug for Image { impl fmt::Debug for RasterImage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
"Image {{ width: {}, height: {}, format: {:?}, ..., id: {:?} }}", "Image {{ width: {}, height: {}, format: {:?}, ..., id: {:?} }}",
self.width, self.height, self.format, self.id self.metadata.width, self.metadata.height, self.format, self.id
) )
} }
} }
#[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub struct ImageMetadata { pub struct ImageMetadata {
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
@ -170,7 +202,7 @@ pub struct ImageMetadata {
// FIXME: Images must not be copied every frame. Instead we should atomically // FIXME: Images must not be copied every frame. Instead we should atomically
// reference count them. // reference count them.
pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> { pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> {
if buffer.is_empty() { if buffer.is_empty() {
return None; return None;
} }
@ -189,15 +221,18 @@ pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Image>
rgba8_byte_swap_colors_inplace(&mut rgba); rgba8_byte_swap_colors_inplace(&mut rgba);
let frame = ImageFrame { let frame = ImageFrame {
delay: None, delay: None,
bytes: IpcSharedMemory::from_bytes(&rgba), byte_range: 0..rgba.len(),
width: rgba.width(), width: rgba.width(),
height: rgba.height(), height: rgba.height(),
}; };
Some(Image { Some(RasterImage {
metadata: ImageMetadata {
width: rgba.width(), width: rgba.width(),
height: rgba.height(), height: rgba.height(),
},
format: PixelFormat::BGRA8, format: PixelFormat::BGRA8,
frames: vec![frame], frames: vec![frame],
bytes: IpcSharedMemory::from_bytes(&rgba),
id: None, id: None,
cors_status, cors_status,
}) })
@ -354,7 +389,7 @@ fn is_webp(buffer: &[u8]) -> bool {
buffer[8..].len() >= len && &buffer[8..12] == b"WEBP" buffer[8..].len() >= len && &buffer[8..12] == b"WEBP"
} }
fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> { fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> {
let Ok(decoded_gif) = GifDecoder::new(Cursor::new(buffer)) else { let Ok(decoded_gif) = GifDecoder::new(Cursor::new(buffer)) else {
return None; return None;
}; };
@ -364,46 +399,61 @@ fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> {
// This uses `map_while`, because the first non-decodable frame seems to // This uses `map_while`, because the first non-decodable frame seems to
// send the frame iterator into an infinite loop. See // send the frame iterator into an infinite loop. See
// <https://github.com/image-rs/image/issues/2442>. // <https://github.com/image-rs/image/issues/2442>.
let mut frame_data = vec![];
let mut total_number_of_bytes = 0;
let frames: Vec<ImageFrame> = decoded_gif let frames: Vec<ImageFrame> = decoded_gif
.into_frames() .into_frames()
.map_while(|decoded_frame| { .map_while(|decoded_frame| {
let mut frame = match decoded_frame { let mut gif_frame = match decoded_frame {
Ok(decoded_frame) => decoded_frame, Ok(decoded_frame) => decoded_frame,
Err(error) => { Err(error) => {
debug!("decode GIF frame error: {error}"); debug!("decode GIF frame error: {error}");
return None; return None;
}, },
}; };
rgba8_byte_swap_colors_inplace(frame.buffer_mut()); rgba8_byte_swap_colors_inplace(gif_frame.buffer_mut());
let frame_start = total_number_of_bytes;
let frame = ImageFrame { total_number_of_bytes += gif_frame.buffer().len();
bytes: IpcSharedMemory::from_bytes(frame.buffer()),
delay: Some(Duration::from(frame.delay())),
width: frame.buffer().width(),
height: frame.buffer().height(),
};
// The image size should be at least as large as the largest frame. // The image size should be at least as large as the largest frame.
width = cmp::max(width, frame.width); let frame_width = gif_frame.buffer().width();
height = cmp::max(height, frame.height); let frame_height = gif_frame.buffer().height();
width = cmp::max(width, frame_width);
height = cmp::max(height, frame_height);
let frame = ImageFrame {
byte_range: frame_start..total_number_of_bytes,
delay: Some(Duration::from(gif_frame.delay())),
width: frame_width,
height: frame_height,
};
frame_data.push(gif_frame);
Some(frame) Some(frame)
}) })
.collect(); .collect();
if frames.is_empty() { if frames.is_empty() {
debug!("Animated Image decoding error"); debug!("Animated Image decoding error");
None return None;
} else { }
Some(Image {
width, // Coalesce the frame data into one single shared memory region.
height, let mut bytes = Vec::with_capacity(total_number_of_bytes);
for frame in frame_data {
bytes.extend_from_slice(frame.buffer());
}
Some(RasterImage {
metadata: ImageMetadata { width, height },
cors_status, cors_status,
frames, frames,
id: None, id: None,
format: PixelFormat::BGRA8, format: PixelFormat::BGRA8,
bytes: IpcSharedMemory::from_bytes(&bytes),
}) })
} }
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {

View file

@ -15,7 +15,6 @@ path = "lib.rs"
base = { workspace = true } base = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
log = { workspace = true } log = { workspace = true }
parking_lot = { workspace = true }
profile_traits = { workspace = true } profile_traits = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }

View file

@ -12,9 +12,9 @@ use ipc_channel::ipc::{self, IpcReceiver};
use ipc_channel::router::ROUTER; use ipc_channel::router::ROUTER;
use log::debug; use log::debug;
use profile_traits::mem::{ use profile_traits::mem::{
MemoryReportResult, ProfilerChan, ProfilerMsg, Report, Reporter, ReporterRequest, ReportsChan, MemoryReport, MemoryReportResult, ProfilerChan, ProfilerMsg, Report, Reporter, ReporterRequest,
ReportsChan,
}; };
use serde::Serialize;
use crate::system_reporter; use crate::system_reporter;
@ -100,31 +100,20 @@ impl Profiler {
ProfilerMsg::Report(sender) => { ProfilerMsg::Report(sender) => {
let main_pid = std::process::id(); let main_pid = std::process::id();
#[derive(Serialize)]
struct JsonReport {
pid: u32,
#[serde(rename = "isMainProcess")]
is_main_process: bool,
reports: Vec<Report>,
}
let reports = self.collect_reports(); let reports = self.collect_reports();
// Turn the pid -> reports map into a vector and add the // Turn the pid -> reports map into a vector and add the
// hint to find the main process. // hint to find the main process.
let json_reports: Vec<JsonReport> = reports let results: Vec<MemoryReport> = reports
.into_iter() .into_iter()
.map(|(pid, reports)| JsonReport { .map(|(pid, reports)| MemoryReport {
pid, pid,
reports, reports,
is_main_process: pid == main_pid, is_main_process: pid == main_pid,
}) })
.collect(); .collect();
let content = serde_json::to_string(&json_reports) let _ = sender.send(MemoryReportResult { results });
.unwrap_or_else(|_| "{ error: \"failed to create memory report\"}".to_owned());
let _ = sender.send(MemoryReportResult { content });
true true
}, },
ProfilerMsg::Exit => false, ProfilerMsg::Exit => false,
} }
} }

View file

@ -69,27 +69,6 @@ impl Formattable for Option<TimerMetadata> {
} }
} }
impl Formattable for ProfilerCategory {
// some categories are subcategories of LayoutPerformCategory
// and should be printed to indicate this
fn format(&self, _output: &Option<OutputOptions>) -> String {
let padding = match *self {
ProfilerCategory::LayoutStyleRecalc |
ProfilerCategory::LayoutRestyleDamagePropagation |
ProfilerCategory::LayoutGeneratedContent |
ProfilerCategory::LayoutFloatPlacementSpeculation |
ProfilerCategory::LayoutMain |
ProfilerCategory::LayoutStoreOverflow |
ProfilerCategory::LayoutDispListBuild |
ProfilerCategory::LayoutParallelWarmup |
ProfilerCategory::LayoutTextShaping => "| + ",
_ => "",
};
let name: &'static str = self.into();
format!("{padding}{name}")
}
}
type ProfilerBuckets = BTreeMap<(ProfilerCategory, Option<TimerMetadata>), Vec<Duration>>; type ProfilerBuckets = BTreeMap<(ProfilerCategory, Option<TimerMetadata>), Vec<Duration>>;
// back end of the profiler that handles data aggregation and performance metrics // back end of the profiler that handles data aggregation and performance metrics
@ -276,7 +255,7 @@ impl Profiler {
writeln!( writeln!(
file, file,
"{}\t{}\t{:15.4}\t{:15.4}\t{:15.4}\t{:15.4}\t{:15}", "{}\t{}\t{:15.4}\t{:15.4}\t{:15.4}\t{:15.4}\t{:15}",
category.format(&self.output), category.variant_name(),
meta.format(&self.output), meta.format(&self.output),
mean.as_seconds_f64() * 1000., mean.as_seconds_f64() * 1000.,
median.as_seconds_f64() * 1000., median.as_seconds_f64() * 1000.,
@ -319,7 +298,7 @@ impl Profiler {
writeln!( writeln!(
&mut lock, &mut lock,
"{:-35}{} {:15.4} {:15.4} {:15.4} {:15.4} {:15}", "{:-35}{} {:15.4} {:15.4} {:15.4} {:15.4} {:15}",
category.format(&self.output), category.variant_name(),
meta.format(&self.output), meta.format(&self.output),
mean.as_seconds_f64() * 1000., mean.as_seconds_f64() * 1000.,
median.as_seconds_f64() * 1000., median.as_seconds_f64() * 1000.,

View file

@ -18,6 +18,7 @@ crown = ['js/crown']
debugmozjs = ['js/debugmozjs'] debugmozjs = ['js/debugmozjs']
jitspew = ['js/jitspew'] jitspew = ['js/jitspew']
profilemozjs = ['js/profilemozjs'] profilemozjs = ['js/profilemozjs']
testbinding = ["script_bindings/testbinding"]
tracing = ["dep:tracing", "script_bindings/tracing"] tracing = ["dep:tracing", "script_bindings/tracing"]
webgl_backtrace = ["canvas_traits/webgl_backtrace"] webgl_backtrace = ["canvas_traits/webgl_backtrace"]
js_backtrace = [] js_backtrace = []
@ -93,13 +94,11 @@ net_traits = { workspace = true }
nom = "7.1.3" nom = "7.1.3"
num-traits = { workspace = true } num-traits = { workspace = true }
num_cpus = { workspace = true } num_cpus = { workspace = true }
parking_lot = { workspace = true }
percent-encoding = { workspace = true } percent-encoding = { workspace = true }
phf = "0.11" phf = "0.11"
pixels = { path = "../pixels" } pixels = { path = "../pixels" }
profile_traits = { workspace = true } profile_traits = { workspace = true }
range = { path = "../range" } range = { path = "../range" }
ref_filter_map = "1.0.1"
regex = { workspace = true } regex = { workspace = true }
script_bindings = { path = "../script_bindings" } script_bindings = { path = "../script_bindings" }
script_layout_interface = { workspace = true } script_layout_interface = { workspace = true }

View file

@ -76,6 +76,10 @@ impl Animations {
self.pending_events.borrow_mut().clear(); self.pending_events.borrow_mut().clear();
} }
pub(crate) fn animations_present(&self) -> bool {
self.has_running_animations.get() || !self.pending_events.borrow().is_empty()
}
// Mark all animations dirty, if they haven't been marked dirty since the // Mark all animations dirty, if they haven't been marked dirty since the
// specified `current_timeline_value`. Returns true if animations were marked // specified `current_timeline_value`. Returns true if animations were marked
// dirty or false otherwise. // dirty or false otherwise.

View file

@ -17,7 +17,7 @@ use cssparser::color::clamp_unit_f32;
use cssparser::{Parser, ParserInput}; use cssparser::{Parser, ParserInput};
use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use euclid::vec2; use euclid::vec2;
use ipc_channel::ipc::{self, IpcSender}; use ipc_channel::ipc::{self, IpcSender, IpcSharedMemory};
use net_traits::image_cache::{ImageCache, ImageResponse}; use net_traits::image_cache::{ImageCache, ImageResponse};
use net_traits::request::CorsSettings; use net_traits::request::CorsSettings;
use pixels::PixelFormat; use pixels::PixelFormat;
@ -54,6 +54,7 @@ use crate::dom::dommatrix::DOMMatrix;
use crate::dom::element::{Element, cors_setting_for_element}; use crate::dom::element::{Element, cors_setting_for_element};
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement; use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::htmlvideoelement::HTMLVideoElement;
use crate::dom::imagedata::ImageData; use crate::dom::imagedata::ImageData;
use crate::dom::node::{Node, NodeTraits}; use crate::dom::node::{Node, NodeTraits};
use crate::dom::offscreencanvas::OffscreenCanvas; use crate::dom::offscreencanvas::OffscreenCanvas;
@ -310,14 +311,15 @@ impl CanvasState {
self.origin_clean.set(false) self.origin_clean.set(false)
} }
// https://html.spec.whatwg.org/multipage/#the-image-argument-is-not-origin-clean /// <https://html.spec.whatwg.org/multipage/#the-image-argument-is-not-origin-clean>
fn is_origin_clean(&self, image: CanvasImageSource) -> bool { fn is_origin_clean(&self, source: CanvasImageSource) -> bool {
match image { match source {
CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(),
CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(),
CanvasImageSource::HTMLImageElement(image) => { CanvasImageSource::HTMLImageElement(image) => {
image.same_origin(GlobalScope::entry().origin()) image.same_origin(GlobalScope::entry().origin())
}, },
CanvasImageSource::HTMLVideoElement(video) => video.origin_is_clean(),
CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(),
CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(),
CanvasImageSource::CSSStyleValue(_) => true, CanvasImageSource::CSSStyleValue(_) => true,
} }
} }
@ -328,7 +330,15 @@ impl CanvasState {
cors_setting: Option<CorsSettings>, cors_setting: Option<CorsSettings>,
) -> Option<snapshot::Snapshot> { ) -> Option<snapshot::Snapshot> {
let img = match self.request_image_from_cache(url, cors_setting) { let img = match self.request_image_from_cache(url, cors_setting) {
ImageResponse::Loaded(img, _) => img, ImageResponse::Loaded(image, _) => {
if let Some(image) = image.as_raster_image() {
image
} else {
// TODO: https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
warn!("Vector images are not supported as image source in canvas2d");
return None;
}
},
ImageResponse::PlaceholderLoaded(_, _) | ImageResponse::PlaceholderLoaded(_, _) |
ImageResponse::None | ImageResponse::None |
ImageResponse::MetadataLoaded(_) => { ImageResponse::MetadataLoaded(_) => {
@ -336,7 +346,7 @@ impl CanvasState {
}, },
}; };
let size = Size2D::new(img.width, img.height); let size = Size2D::new(img.metadata.width, img.metadata.height);
let format = match img.format { let format = match img.format {
PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA, PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA,
PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA, PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA,
@ -350,7 +360,7 @@ impl CanvasState {
size.cast(), size.cast(),
format, format,
alpha_mode, alpha_mode,
img.bytes(), IpcSharedMemory::from_bytes(img.first_frame().bytes),
)) ))
} }
@ -430,6 +440,17 @@ impl CanvasState {
} }
let result = match image { let result = match image {
CanvasImageSource::HTMLVideoElement(ref video) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
// Step 2. Let usability be the result of checking the usability of image.
// Step 3. If usability is bad, then return (without drawing anything).
if !video.is_usable() {
return Ok(());
}
self.draw_html_video_element(video, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh);
Ok(())
},
CanvasImageSource::HTMLCanvasElement(ref canvas) => { CanvasImageSource::HTMLCanvasElement(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() { if canvas.get_size().is_empty() {
@ -490,6 +511,52 @@ impl CanvasState {
result result
} }
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
#[allow(clippy::too_many_arguments)]
fn draw_html_video_element(
&self,
video: &HTMLVideoElement,
canvas: Option<&HTMLCanvasElement>,
sx: f64,
sy: f64,
sw: Option<f64>,
sh: Option<f64>,
dx: f64,
dy: f64,
dw: Option<f64>,
dh: Option<f64>,
) {
let Some(snapshot) = video.get_current_frame_data() else {
return;
};
// Step 4. Establish the source and destination rectangles.
let video_size = snapshot.size().to_f64();
let dw = dw.unwrap_or(video_size.width);
let dh = dh.unwrap_or(video_size.height);
let sw = sw.unwrap_or(video_size.width);
let sh = sh.unwrap_or(video_size.height);
let (source_rect, dest_rect) =
self.adjust_source_dest_rects(video_size, sx, sy, sw, sh, dx, dy, dw, dh);
// Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
return;
}
let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
snapshot.as_ipc(),
dest_rect,
source_rect,
smoothing_enabled,
));
self.mark_as_dirty(canvas);
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn draw_offscreen_canvas( fn draw_offscreen_canvas(
&self, &self,
@ -958,7 +1025,7 @@ impl CanvasState {
)) ))
} }
// https://html.spec.whatwg.org/multipage/#dom-context-2d-createpattern /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createpattern>
pub(crate) fn create_pattern( pub(crate) fn create_pattern(
&self, &self,
global: &GlobalScope, global: &GlobalScope,
@ -968,7 +1035,7 @@ impl CanvasState {
) -> Fallible<Option<DomRoot<CanvasPattern>>> { ) -> Fallible<Option<DomRoot<CanvasPattern>>> {
let snapshot = match image { let snapshot = match image {
CanvasImageSource::HTMLImageElement(ref image) => { CanvasImageSource::HTMLImageElement(ref image) => {
// https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if !image.is_usable()? { if !image.is_usable()? {
return Ok(None); return Ok(None);
} }
@ -980,10 +1047,28 @@ impl CanvasState {
}) })
.ok_or(Error::InvalidState)? .ok_or(Error::InvalidState)?
}, },
CanvasImageSource::HTMLVideoElement(ref video) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if !video.is_usable() {
return Ok(None);
}
video.get_current_frame_data().ok_or(Error::InvalidState)?
},
CanvasImageSource::HTMLCanvasElement(ref canvas) => { CanvasImageSource::HTMLCanvasElement(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
return Err(Error::InvalidState);
}
canvas.get_image_data().ok_or(Error::InvalidState)? canvas.get_image_data().ok_or(Error::InvalidState)?
}, },
CanvasImageSource::OffscreenCanvas(ref canvas) => { CanvasImageSource::OffscreenCanvas(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
return Err(Error::InvalidState);
}
canvas.get_image_data().ok_or(Error::InvalidState)? canvas.get_image_data().ok_or(Error::InvalidState)?
}, },
CanvasImageSource::CSSStyleValue(ref value) => value CanvasImageSource::CSSStyleValue(ref value) => value

View file

@ -3,24 +3,33 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct; use dom_struct::dom_struct;
use js::jsapi::Value; use js::rust::{HandleObject, HandleValue};
use js::rust::{Handle, HandleObject};
use crate::dom::abortsignal::AbortSignal;
use crate::dom::bindings::codegen::Bindings::AbortControllerBinding::AbortControllerMethods; use crate::dom::bindings::codegen::Bindings::AbortControllerBinding::AbortControllerMethods;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::{CanGc, JSContext}; use crate::script_runtime::{CanGc, JSContext};
/// <https://dom.spec.whatwg.org/#abortcontroller>
#[dom_struct] #[dom_struct]
pub(crate) struct AbortController { pub(crate) struct AbortController {
reflector_: Reflector, reflector_: Reflector,
/// An AbortController object has an associated signal (an AbortSignal object).
signal: Dom<AbortSignal>,
} }
impl AbortController { impl AbortController {
/// <https://dom.spec.whatwg.org/#dom-abortcontroller-abortcontroller>
fn new_inherited() -> AbortController { fn new_inherited() -> AbortController {
// The new AbortController() constructor steps are:
// Let signal be a new AbortSignal object.
// Set thiss signal to signal.
AbortController { AbortController {
reflector_: Reflector::new(), reflector_: Reflector::new(),
signal: Dom::from_ref(&AbortSignal::new_inherited()),
} }
} }
@ -36,6 +45,12 @@ impl AbortController {
can_gc, can_gc,
) )
} }
/// <https://dom.spec.whatwg.org/#abortcontroller-signal-abort>
fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
// signal abort on controllers signal with reason if it is given.
self.signal.signal_abort(cx, reason, can_gc);
}
} }
impl AbortControllerMethods<crate::DomTypeHolder> for AbortController { impl AbortControllerMethods<crate::DomTypeHolder> for AbortController {
@ -49,5 +64,9 @@ impl AbortControllerMethods<crate::DomTypeHolder> for AbortController {
} }
/// <https://dom.spec.whatwg.org/#dom-abortcontroller-abort> /// <https://dom.spec.whatwg.org/#dom-abortcontroller-abort>
fn Abort(&self, _cx: JSContext, _reason: Handle<'_, Value>) {} fn Abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
// The abort(reason) method steps are
// to signal abort on this with reason if it is given.
self.signal_abort(cx, reason, can_gc);
}
} }

View file

@ -0,0 +1,140 @@
/* 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::mem;
use dom_struct::dom_struct;
use js::jsapi::Heap;
use js::jsval::{JSVal, UndefinedValue};
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
use script_bindings::inheritance::Castable;
use crate::dom::bindings::codegen::Bindings::AbortSignalBinding::AbortSignalMethods;
use crate::dom::bindings::error::{Error, ErrorToJsval};
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::{CanGc, JSContext};
/// <https://dom.spec.whatwg.org/#abortcontroller-api-integration>
/// TODO: implement algorithms at call point,
/// in order to integrate the abort signal with its various use cases.
#[derive(JSTraceable, MallocSizeOf)]
#[allow(dead_code)]
enum AbortAlgorithm {
/// <https://dom.spec.whatwg.org/#add-an-event-listener>
DomEventLister,
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
StreamPiping,
/// <https://fetch.spec.whatwg.org/#dom-global-fetch>
Fetch,
}
/// <https://dom.spec.whatwg.org/#abortsignal>
#[dom_struct]
pub(crate) struct AbortSignal {
eventtarget: EventTarget,
/// <https://dom.spec.whatwg.org/#abortsignal-abort-reason>
#[ignore_malloc_size_of = "mozjs"]
abort_reason: Heap<JSVal>,
/// <https://dom.spec.whatwg.org/#abortsignal-abort-algorithms>
abort_algorithms: RefCell<Vec<AbortAlgorithm>>,
}
impl AbortSignal {
pub(crate) fn new_inherited() -> AbortSignal {
AbortSignal {
eventtarget: EventTarget::new_inherited(),
abort_reason: Default::default(),
abort_algorithms: Default::default(),
}
}
#[allow(dead_code)]
fn new_with_proto(
global: &GlobalScope,
proto: Option<HandleObject>,
can_gc: CanGc,
) -> DomRoot<AbortSignal> {
reflect_dom_object_with_proto(
Box::new(AbortSignal::new_inherited()),
global,
proto,
can_gc,
)
}
/// <https://dom.spec.whatwg.org/#abortsignal-signal-abort>
pub(crate) fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
// If signal is aborted, then return.
if self.Aborted() {
return;
}
let abort_reason = reason.get();
// Set signals abort reason to reason if it is given;
if !abort_reason.is_undefined() {
self.abort_reason.set(abort_reason);
} else {
// otherwise to a new "AbortError" DOMException.
rooted!(in(*cx) let mut rooted_error = UndefinedValue());
Error::Abort.to_jsval(cx, &self.global(), rooted_error.handle_mut(), can_gc);
self.abort_reason.set(rooted_error.get())
}
// Let dependentSignalsToAbort be a new list.
// For each dependentSignal of signals dependent signals:
// TODO: #36936
// Run the abort steps for signal.
self.run_the_abort_steps(can_gc);
// For each dependentSignal of dependentSignalsToAbort, run the abort steps for dependentSignal.
// TODO: #36936
}
/// <https://dom.spec.whatwg.org/#run-the-abort-steps>
fn run_the_abort_steps(&self, can_gc: CanGc) {
// For each algorithm of signals abort algorithms: run algorithm.
let algos = mem::take(&mut *self.abort_algorithms.borrow_mut());
for _algo in algos {
// TODO: match on variant and implement algo steps.
// See the various items of #34866
}
// Empty signals abort algorithms.
// Done above with `take`.
// Fire an event named abort at signal.
self.upcast::<EventTarget>()
.fire_event(atom!("abort"), can_gc);
}
}
impl AbortSignalMethods<crate::DomTypeHolder> for AbortSignal {
/// <https://dom.spec.whatwg.org/#dom-abortsignal-aborted>
fn Aborted(&self) -> bool {
// TODO
false
}
/// <https://dom.spec.whatwg.org/#dom-abortsignal-reason>
fn Reason(&self, _: JSContext, _rval: MutableHandleValue) {
// TODO
}
/// <https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted>
#[allow(unsafe_code)]
fn ThrowIfAborted(&self) {
// TODO
}
// <https://dom.spec.whatwg.org/#dom-abortsignal-onabort>
event_handler!(abort, GetOnabort, SetOnabort);
}

View file

@ -9,10 +9,8 @@ use std::cell::{BorrowError, BorrowMutError};
pub(crate) use std::cell::{Ref, RefCell, RefMut}; pub(crate) use std::cell::{Ref, RefCell, RefMut};
#[cfg(feature = "refcell_backtrace")] #[cfg(feature = "refcell_backtrace")]
pub(crate) use accountable_refcell::{Ref, RefCell, RefMut, ref_filter_map}; pub(crate) use accountable_refcell::{Ref, RefCell, RefMut};
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOfOps}; use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOfOps};
#[cfg(not(feature = "refcell_backtrace"))]
pub(crate) use ref_filter_map::ref_filter_map;
use crate::dom::bindings::root::{assert_in_layout, assert_in_script}; use crate::dom::bindings::root::{assert_in_layout, assert_in_script};

View file

@ -11,7 +11,7 @@ use crate::DomTypes;
use crate::dom::bindings::conversions::DerivedFrom; use crate::dom::bindings::conversions::DerivedFrom;
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::realms::InRealm; use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
/// Create the reflector for a new DOM object and yield ownership to the /// Create the reflector for a new DOM object and yield ownership to the
@ -42,7 +42,16 @@ where
} }
pub(crate) trait DomGlobal { pub(crate) trait DomGlobal {
/// Returns the [relevant global] in whatever realm is currently active.
///
/// [relevant global]: https://html.spec.whatwg.org/multipage/#concept-relevant-global
fn global_(&self, realm: InRealm) -> DomRoot<GlobalScope>; fn global_(&self, realm: InRealm) -> DomRoot<GlobalScope>;
/// Returns the [relevant global] in the same realm as the callee object.
/// If you know the callee's realm is already the current realm, it is
/// more efficient to call [DomGlobal::global_] instead.
///
/// [relevant global]: https://html.spec.whatwg.org/multipage/#concept-relevant-global
fn global(&self) -> DomRoot<GlobalScope>; fn global(&self) -> DomRoot<GlobalScope>;
} }
@ -51,7 +60,8 @@ impl<T: DomGlobalGeneric<crate::DomTypeHolder>> DomGlobal for T {
<Self as DomGlobalGeneric<crate::DomTypeHolder>>::global_(self, realm) <Self as DomGlobalGeneric<crate::DomTypeHolder>>::global_(self, realm)
} }
fn global(&self) -> DomRoot<GlobalScope> { fn global(&self) -> DomRoot<GlobalScope> {
<Self as DomGlobalGeneric<crate::DomTypeHolder>>::global(self) let realm = enter_realm(self);
<Self as DomGlobalGeneric<crate::DomTypeHolder>>::global_(self, InRealm::entered(&realm))
} }
} }

View file

@ -10,11 +10,13 @@ use std::os::raw;
use std::ptr; use std::ptr;
use base::id::{ use base::id::{
BlobId, DomExceptionId, DomPointId, Index, MessagePortId, NamespaceIndex, PipelineNamespaceId, BlobId, DomExceptionId, DomPointId, ImageBitmapId, Index, MessagePortId, NamespaceIndex,
PipelineNamespaceId,
}; };
use constellation_traits::{ use constellation_traits::{
BlobImpl, DomException, DomPoint, MessagePortImpl, Serializable as SerializableInterface, BlobImpl, DomException, DomPoint, MessagePortImpl, Serializable as SerializableInterface,
StructuredSerializedData, Transferrable as TransferrableInterface, SerializableImageBitmap, StructuredSerializedData, Transferrable as TransferrableInterface,
TransformStreamData,
}; };
use js::gc::RootedVec; use js::gc::RootedVec;
use js::glue::{ use js::glue::{
@ -42,6 +44,7 @@ use crate::dom::blob::Blob;
use crate::dom::dompoint::DOMPoint; use crate::dom::dompoint::DOMPoint;
use crate::dom::dompointreadonly::DOMPointReadOnly; use crate::dom::dompointreadonly::DOMPointReadOnly;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::imagebitmap::ImageBitmap;
use crate::dom::messageport::MessagePort; use crate::dom::messageport::MessagePort;
use crate::dom::readablestream::ReadableStream; use crate::dom::readablestream::ReadableStream;
use crate::dom::types::{DOMException, TransformStream}; use crate::dom::types::{DOMException, TransformStream};
@ -66,6 +69,7 @@ pub(super) enum StructuredCloneTags {
DomException = 0xFFFF8007, DomException = 0xFFFF8007,
WritableStream = 0xFFFF8008, WritableStream = 0xFFFF8008,
TransformStream = 0xFFFF8009, TransformStream = 0xFFFF8009,
ImageBitmap = 0xFFFF800A,
Max = 0xFFFFFFFF, Max = 0xFFFFFFFF,
} }
@ -76,6 +80,7 @@ impl From<SerializableInterface> for StructuredCloneTags {
SerializableInterface::DomPointReadOnly => StructuredCloneTags::DomPointReadOnly, SerializableInterface::DomPointReadOnly => StructuredCloneTags::DomPointReadOnly,
SerializableInterface::DomPoint => StructuredCloneTags::DomPoint, SerializableInterface::DomPoint => StructuredCloneTags::DomPoint,
SerializableInterface::DomException => StructuredCloneTags::DomException, SerializableInterface::DomException => StructuredCloneTags::DomException,
SerializableInterface::ImageBitmap => StructuredCloneTags::ImageBitmap,
} }
} }
} }
@ -83,6 +88,7 @@ impl From<SerializableInterface> for StructuredCloneTags {
impl From<TransferrableInterface> for StructuredCloneTags { impl From<TransferrableInterface> for StructuredCloneTags {
fn from(v: TransferrableInterface) -> Self { fn from(v: TransferrableInterface) -> Self {
match v { match v {
TransferrableInterface::ImageBitmap => StructuredCloneTags::ImageBitmap,
TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort, TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort,
TransferrableInterface::ReadableStream => StructuredCloneTags::ReadableStream, TransferrableInterface::ReadableStream => StructuredCloneTags::ReadableStream,
TransferrableInterface::WritableStream => StructuredCloneTags::WritableStream, TransferrableInterface::WritableStream => StructuredCloneTags::WritableStream,
@ -104,6 +110,7 @@ fn reader_for_type(
SerializableInterface::DomPointReadOnly => read_object::<DOMPointReadOnly>, SerializableInterface::DomPointReadOnly => read_object::<DOMPointReadOnly>,
SerializableInterface::DomPoint => read_object::<DOMPoint>, SerializableInterface::DomPoint => read_object::<DOMPoint>,
SerializableInterface::DomException => read_object::<DOMException>, SerializableInterface::DomException => read_object::<DOMException>,
SerializableInterface::ImageBitmap => read_object::<ImageBitmap>,
} }
} }
@ -237,6 +244,7 @@ fn serialize_for_type(val: SerializableInterface) -> SerializeOperation {
SerializableInterface::DomPointReadOnly => try_serialize::<DOMPointReadOnly>, SerializableInterface::DomPointReadOnly => try_serialize::<DOMPointReadOnly>,
SerializableInterface::DomPoint => try_serialize::<DOMPoint>, SerializableInterface::DomPoint => try_serialize::<DOMPoint>,
SerializableInterface::DomException => try_serialize::<DOMException>, SerializableInterface::DomException => try_serialize::<DOMException>,
SerializableInterface::ImageBitmap => try_serialize::<ImageBitmap>,
} }
} }
@ -264,6 +272,7 @@ fn receiver_for_type(
) -> fn(&GlobalScope, &mut StructuredDataReader<'_>, u64, RawMutableHandleObject) -> Result<(), ()> ) -> fn(&GlobalScope, &mut StructuredDataReader<'_>, u64, RawMutableHandleObject) -> Result<(), ()>
{ {
match val { match val {
TransferrableInterface::ImageBitmap => receive_object::<ImageBitmap>,
TransferrableInterface::MessagePort => receive_object::<MessagePort>, TransferrableInterface::MessagePort => receive_object::<MessagePort>,
TransferrableInterface::ReadableStream => receive_object::<ReadableStream>, TransferrableInterface::ReadableStream => receive_object::<ReadableStream>,
TransferrableInterface::WritableStream => receive_object::<WritableStream>, TransferrableInterface::WritableStream => receive_object::<WritableStream>,
@ -390,6 +399,7 @@ type TransferOperation = unsafe fn(
fn transfer_for_type(val: TransferrableInterface) -> TransferOperation { fn transfer_for_type(val: TransferrableInterface) -> TransferOperation {
match val { match val {
TransferrableInterface::ImageBitmap => try_transfer::<ImageBitmap>,
TransferrableInterface::MessagePort => try_transfer::<MessagePort>, TransferrableInterface::MessagePort => try_transfer::<MessagePort>,
TransferrableInterface::ReadableStream => try_transfer::<ReadableStream>, TransferrableInterface::ReadableStream => try_transfer::<ReadableStream>,
TransferrableInterface::WritableStream => try_transfer::<WritableStream>, TransferrableInterface::WritableStream => try_transfer::<WritableStream>,
@ -439,6 +449,7 @@ unsafe fn can_transfer_for_type(
root_from_object::<T>(*obj, cx).map(|o| Transferable::can_transfer(&*o)) root_from_object::<T>(*obj, cx).map(|o| Transferable::can_transfer(&*o))
} }
match transferable { match transferable {
TransferrableInterface::ImageBitmap => can_transfer::<ImageBitmap>(obj, cx),
TransferrableInterface::MessagePort => can_transfer::<MessagePort>(obj, cx), TransferrableInterface::MessagePort => can_transfer::<MessagePort>(obj, cx),
TransferrableInterface::ReadableStream => can_transfer::<ReadableStream>(obj, cx), TransferrableInterface::ReadableStream => can_transfer::<ReadableStream>(obj, cx),
TransferrableInterface::WritableStream => can_transfer::<WritableStream>(obj, cx), TransferrableInterface::WritableStream => can_transfer::<WritableStream>(obj, cx),
@ -517,6 +528,8 @@ pub(crate) struct StructuredDataReader<'a> {
/// used as part of the "transfer-receiving" steps of ports, /// used as part of the "transfer-receiving" steps of ports,
/// to produce the DOM ports stored in `message_ports` above. /// to produce the DOM ports stored in `message_ports` above.
pub(crate) port_impls: Option<HashMap<MessagePortId, MessagePortImpl>>, pub(crate) port_impls: Option<HashMap<MessagePortId, MessagePortImpl>>,
/// A map of transform stream implementations,
pub(crate) transform_streams_port_impls: Option<HashMap<MessagePortId, TransformStreamData>>,
/// A map of blob implementations, /// A map of blob implementations,
/// used as part of the "deserialize" steps of blobs, /// used as part of the "deserialize" steps of blobs,
/// to produce the DOM blobs stored in `blobs` above. /// to produce the DOM blobs stored in `blobs` above.
@ -525,6 +538,10 @@ pub(crate) struct StructuredDataReader<'a> {
pub(crate) points: Option<HashMap<DomPointId, DomPoint>>, pub(crate) points: Option<HashMap<DomPointId, DomPoint>>,
/// A map of serialized exceptions. /// A map of serialized exceptions.
pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>, pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>,
// A map of serialized image bitmaps.
pub(crate) image_bitmaps: Option<HashMap<ImageBitmapId, SerializableImageBitmap>>,
/// A map of transferred image bitmaps.
pub(crate) transferred_image_bitmaps: Option<HashMap<ImageBitmapId, SerializableImageBitmap>>,
} }
/// A data holder for transferred and serialized objects. /// A data holder for transferred and serialized objects.
@ -535,12 +552,18 @@ pub(crate) struct StructuredDataWriter {
pub(crate) errors: DOMErrorRecord, pub(crate) errors: DOMErrorRecord,
/// Transferred ports. /// Transferred ports.
pub(crate) ports: Option<HashMap<MessagePortId, MessagePortImpl>>, pub(crate) ports: Option<HashMap<MessagePortId, MessagePortImpl>>,
/// Transferred transform streams.
pub(crate) transform_streams_port: Option<HashMap<MessagePortId, TransformStreamData>>,
/// Serialized points. /// Serialized points.
pub(crate) points: Option<HashMap<DomPointId, DomPoint>>, pub(crate) points: Option<HashMap<DomPointId, DomPoint>>,
/// Serialized exceptions. /// Serialized exceptions.
pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>, pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>,
/// Serialized blobs. /// Serialized blobs.
pub(crate) blobs: Option<HashMap<BlobId, BlobImpl>>, pub(crate) blobs: Option<HashMap<BlobId, BlobImpl>>,
/// Serialized image bitmaps.
pub(crate) image_bitmaps: Option<HashMap<ImageBitmapId, SerializableImageBitmap>>,
/// Transferred image bitmaps.
pub(crate) transferred_image_bitmaps: Option<HashMap<ImageBitmapId, SerializableImageBitmap>>,
} }
/// Writes a structured clone. Returns a `DataClone` error if that fails. /// Writes a structured clone. Returns a `DataClone` error if that fails.
@ -591,9 +614,12 @@ pub(crate) fn write(
let data = StructuredSerializedData { let data = StructuredSerializedData {
serialized: data, serialized: data,
ports: sc_writer.ports.take(), ports: sc_writer.ports.take(),
transform_streams: sc_writer.transform_streams_port.take(),
points: sc_writer.points.take(), points: sc_writer.points.take(),
exceptions: sc_writer.exceptions.take(), exceptions: sc_writer.exceptions.take(),
blobs: sc_writer.blobs.take(), blobs: sc_writer.blobs.take(),
image_bitmaps: sc_writer.image_bitmaps.take(),
transferred_image_bitmaps: sc_writer.transferred_image_bitmaps.take(),
}; };
Ok(data) Ok(data)
@ -613,10 +639,13 @@ pub(crate) fn read(
let mut sc_reader = StructuredDataReader { let mut sc_reader = StructuredDataReader {
roots, roots,
port_impls: data.ports.take(), port_impls: data.ports.take(),
transform_streams_port_impls: data.transform_streams.take(),
blob_impls: data.blobs.take(), blob_impls: data.blobs.take(),
points: data.points.take(), points: data.points.take(),
exceptions: data.exceptions.take(), exceptions: data.exceptions.take(),
errors: DOMErrorRecord { message: None }, errors: DOMErrorRecord { message: None },
image_bitmaps: data.image_bitmaps.take(),
transferred_image_bitmaps: data.transferred_image_bitmaps.take(),
}; };
let sc_reader_ptr = &mut sc_reader as *mut _; let sc_reader_ptr = &mut sc_reader as *mut _;
unsafe { unsafe {

View file

@ -12,11 +12,10 @@ use dom_struct::dom_struct;
use encoding_rs::UTF_8; use encoding_rs::UTF_8;
use js::jsapi::JSObject; use js::jsapi::JSObject;
use js::rust::HandleObject; use js::rust::HandleObject;
use js::typedarray::Uint8; use js::typedarray::{ArrayBufferU8, Uint8};
use net_traits::filemanager_thread::RelativePos; use net_traits::filemanager_thread::RelativePos;
use uuid::Uuid; use uuid::Uuid;
use crate::body::{FetchedData, run_array_buffer_data_algorithm};
use crate::dom::bindings::buffer_source::create_buffer_source; use crate::dom::bindings::buffer_source::create_buffer_source;
use crate::dom::bindings::codegen::Bindings::BlobBinding; use crate::dom::bindings::codegen::Bindings::BlobBinding;
use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods; use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
@ -226,7 +225,7 @@ impl BlobMethods<crate::DomTypeHolder> for Blob {
Blob::new(&global, blob_impl, can_gc) Blob::new(&global, blob_impl, can_gc)
} }
// https://w3c.github.io/FileAPI/#text-method-algo /// <https://w3c.github.io/FileAPI/#text-method-algo>
fn Text(&self, can_gc: CanGc) -> Rc<Promise> { fn Text(&self, can_gc: CanGc) -> Rc<Promise> {
let global = self.global(); let global = self.global();
let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>(); let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
@ -250,35 +249,51 @@ impl BlobMethods<crate::DomTypeHolder> for Blob {
} }
// https://w3c.github.io/FileAPI/#arraybuffer-method-algo // https://w3c.github.io/FileAPI/#arraybuffer-method-algo
fn ArrayBuffer(&self, can_gc: CanGc) -> Rc<Promise> { fn ArrayBuffer(&self, in_realm: InRealm, can_gc: CanGc) -> Rc<Promise> {
let global = self.global();
let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
let p = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
let id = self.get_blob_url_id();
global.read_file_async(
id,
p.clone(),
Box::new(|promise, bytes| {
match bytes {
Ok(b) => {
let cx = GlobalScope::get_cx(); let cx = GlobalScope::get_cx();
let result = run_array_buffer_data_algorithm(cx, b, CanGc::note()); let global = GlobalScope::from_safe_context(cx, in_realm);
let promise = Promise::new_in_current_realm(in_realm, can_gc);
match result { // 1. Let stream be the result of calling get stream on this.
Ok(FetchedData::ArrayBuffer(a)) => { let stream = self.get_stream(can_gc);
promise.resolve_native(&a, CanGc::note())
// 2. Let reader be the result of getting a reader from stream.
// If that threw an exception, return a new promise rejected with that exception.
let reader = match stream.and_then(|s| s.acquire_default_reader(can_gc)) {
Ok(reader) => reader,
Err(error) => {
promise.reject_error(error, can_gc);
return promise;
}, },
Err(e) => promise.reject_error(e, CanGc::note()),
_ => panic!("Unexpected result from run_array_buffer_data_algorithm"),
}
},
Err(e) => promise.reject_error(e, CanGc::note()),
}; };
// 3. Let promise be the result of reading all bytes from stream with reader.
let success_promise = promise.clone();
let failure_promise = promise.clone();
reader.read_all_bytes(
cx,
&global,
Rc::new(move |bytes| {
rooted!(in(*cx) let mut js_object = ptr::null_mut::<JSObject>());
// 4. Return the result of transforming promise by a fulfillment handler that returns a new
// [ArrayBuffer]
let array_buffer = create_buffer_source::<ArrayBufferU8>(
cx,
bytes,
js_object.handle_mut(),
can_gc,
)
.expect("Converting input to ArrayBufferU8 should never fail");
success_promise.resolve_native(&array_buffer, can_gc);
}), }),
Rc::new(move |cx, value| {
failure_promise.reject(cx, value, can_gc);
}),
in_realm,
can_gc,
); );
p
promise
} }
/// <https://w3c.github.io/FileAPI/#dom-blob-bytes> /// <https://w3c.github.io/FileAPI/#dom-blob-bytes>

View file

@ -5,6 +5,7 @@
use canvas_traits::canvas::{Canvas2dMsg, CanvasId, CanvasMsg, FromScriptMsg}; use canvas_traits::canvas::{Canvas2dMsg, CanvasId, CanvasMsg, FromScriptMsg};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use euclid::default::Size2D; use euclid::default::Size2D;
use ipc_channel::ipc::IpcSender;
use profile_traits::ipc; use profile_traits::ipc;
use script_bindings::inheritance::Castable; use script_bindings::inheritance::Castable;
use servo_url::ServoUrl; use servo_url::ServoUrl;
@ -36,27 +37,50 @@ use crate::dom::path2d::Path2D;
use crate::dom::textmetrics::TextMetrics; use crate::dom::textmetrics::TextMetrics;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
#[derive(JSTraceable, MallocSizeOf)]
struct DroppableCanvasRenderingContext2D {
#[no_trace]
ipc_sender: IpcSender<CanvasMsg>,
#[no_trace]
canvas_id: CanvasId,
}
impl Drop for DroppableCanvasRenderingContext2D {
fn drop(&mut self) {
if let Err(err) = self.ipc_sender.send(CanvasMsg::Close(self.canvas_id)) {
warn!("Could not close canvas: {}", err)
}
}
}
// https://html.spec.whatwg.org/multipage/#canvasrenderingcontext2d // https://html.spec.whatwg.org/multipage/#canvasrenderingcontext2d
#[dom_struct] #[dom_struct]
pub(crate) struct CanvasRenderingContext2D { pub(crate) struct CanvasRenderingContext2D {
reflector_: Reflector, reflector_: Reflector,
canvas: HTMLCanvasElementOrOffscreenCanvas, canvas: HTMLCanvasElementOrOffscreenCanvas,
canvas_state: CanvasState, canvas_state: CanvasState,
droppable: DroppableCanvasRenderingContext2D,
} }
impl CanvasRenderingContext2D { impl CanvasRenderingContext2D {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_inherited( pub(crate) fn new_inherited(
global: &GlobalScope, global: &GlobalScope,
canvas: HTMLCanvasElementOrOffscreenCanvas, canvas: HTMLCanvasElementOrOffscreenCanvas,
size: Size2D<u32>, size: Size2D<u32>,
) -> CanvasRenderingContext2D { ) -> CanvasRenderingContext2D {
let canvas_state =
CanvasState::new(global, Size2D::new(size.width as u64, size.height as u64));
let ipc_sender = canvas_state.get_ipc_renderer().clone();
let canvas_id = canvas_state.get_canvas_id();
CanvasRenderingContext2D { CanvasRenderingContext2D {
reflector_: Reflector::new(), reflector_: Reflector::new(),
canvas, canvas,
canvas_state: CanvasState::new( canvas_state,
global, droppable: DroppableCanvasRenderingContext2D {
Size2D::new(size.width as u64, size.height as u64), ipc_sender,
), canvas_id,
},
} }
} }
@ -689,15 +713,3 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
.set_shadow_color(self.canvas.canvas(), value, can_gc) .set_shadow_color(self.canvas.canvas(), value, can_gc)
} }
} }
impl Drop for CanvasRenderingContext2D {
fn drop(&mut self) {
if let Err(err) = self
.canvas_state
.get_ipc_renderer()
.send(CanvasMsg::Close(self.canvas_state.get_canvas_id()))
{
warn!("Could not close canvas: {}", err)
}
}
}

View file

@ -7,6 +7,7 @@ use std::rc::Rc;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use js::rust::{HandleObject, MutableHandleValue}; use js::rust::{HandleObject, MutableHandleValue};
use net_traits::image_cache::Image;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::DataTransferBinding::DataTransferMethods; use crate::dom::bindings::codegen::Bindings::DataTransferBinding::DataTransferMethods;
@ -155,7 +156,10 @@ impl DataTransferMethods<crate::DomTypeHolder> for DataTransfer {
// Step 3 // Step 3
if let Some(image) = image.downcast::<HTMLImageElement>() { if let Some(image) = image.downcast::<HTMLImageElement>() {
data_store.set_bitmap(image.image_data(), x, y); match image.image_data().as_ref().and_then(Image::as_raster_image) {
Some(image) => data_store.set_bitmap(Some(image), x, y),
None => warn!("Vector images are not yet supported in setDragImage"),
}
} }
} }

View file

@ -1557,11 +1557,14 @@ impl Document {
return; return;
} }
// For a node within a text input UA shadow DOM, delegate the focus target into its shadow host.
// TODO: This focus delegation should be done with shadow DOM delegateFocus attribute.
let target_el = el.find_focusable_shadow_host_if_necessary();
self.begin_focus_transaction(); self.begin_focus_transaction();
// Try to focus `el`. If it's not focusable, focus the document // Try to focus `el`. If it's not focusable, focus the document instead.
// instead.
self.request_focus(None, FocusInitiator::Local, can_gc); self.request_focus(None, FocusInitiator::Local, can_gc);
self.request_focus(Some(&*el), FocusInitiator::Local, can_gc); self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
} }
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event( let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
@ -2375,6 +2378,9 @@ impl Document {
let mut cancel_state = event.get_cancel_state(); let mut cancel_state = event.get_cancel_state();
// https://w3c.github.io/uievents/#keys-cancelable-keys // https://w3c.github.io/uievents/#keys-cancelable-keys
// it MUST prevent the respective beforeinput and input
// (and keypress if supported) events from being generated
// TODO: keypress should be deprecated and superceded by beforeinput
if keyboard_event.state == KeyState::Down && if keyboard_event.state == KeyState::Down &&
is_character_value_key(&(keyboard_event.key)) && is_character_value_key(&(keyboard_event.key)) &&
!keyboard_event.is_composing && !keyboard_event.is_composing &&
@ -5060,6 +5066,35 @@ impl Document {
self.image_animation_manager.borrow_mut() self.image_animation_manager.borrow_mut()
} }
pub(crate) fn update_animating_images(&self) {
let mut image_animation_manager = self.image_animation_manager.borrow_mut();
if !image_animation_manager.image_animations_present() {
return;
}
image_animation_manager
.update_active_frames(&self.window, self.current_animation_timeline_value());
if !self.animations().animations_present() {
let next_scheduled_time =
image_animation_manager.next_schedule_time(self.current_animation_timeline_value());
// TODO: Once we have refresh signal from the compositor,
// we should get rid of timer for animated image update.
if let Some(next_scheduled_time) = next_scheduled_time {
self.schedule_image_animation_update(next_scheduled_time);
}
}
}
fn schedule_image_animation_update(&self, next_scheduled_time: f64) {
let callback = ImageAnimationUpdateCallback {
document: Trusted::new(self),
};
self.global().schedule_callback(
OneshotTimerCallback::ImageAnimationUpdate(callback),
Duration::from_secs_f64(next_scheduled_time),
);
}
/// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps> /// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps>
pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8]) { pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8]) {
// 1. If document's will declaratively refresh is true, then return. // 1. If document's will declaratively refresh is true, then return.
@ -6756,6 +6791,21 @@ impl FakeRequestAnimationFrameCallback {
} }
} }
/// This is a temporary workaround to update animated images,
/// we should get rid of this after we have refresh driver #3406
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct ImageAnimationUpdateCallback {
/// The document.
#[ignore_malloc_size_of = "non-owning"]
document: Trusted<Document>,
}
impl ImageAnimationUpdateCallback {
pub(crate) fn invoke(self, can_gc: CanGc) {
with_script_thread(|script_thread| script_thread.update_the_rendering(true, can_gc))
}
}
#[derive(JSTraceable, MallocSizeOf)] #[derive(JSTraceable, MallocSizeOf)]
pub(crate) enum AnimationFrameCallback { pub(crate) enum AnimationFrameCallback {
DevtoolsFramerateTick { DevtoolsFramerateTick {

View file

@ -13,7 +13,7 @@ use std::str::FromStr;
use std::{fmt, mem}; use std::{fmt, mem};
use content_security_policy as csp; use content_security_policy as csp;
use cssparser::match_ignore_ascii_case; use cssparser::{Parser as CssParser, ParserInput as CssParserInput, match_ignore_ascii_case};
use devtools_traits::AttrInfo; use devtools_traits::AttrInfo;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::InputMethodType; use embedder_traits::InputMethodType;
@ -36,6 +36,8 @@ use style::applicable_declarations::ApplicableDeclarationBlock;
use style::attr::{AttrValue, LengthOrPercentageOrAuto}; use style::attr::{AttrValue, LengthOrPercentageOrAuto};
use style::context::QuirksMode; use style::context::QuirksMode;
use style::invalidation::element::restyle_hints::RestyleHint; use style::invalidation::element::restyle_hints::RestyleHint;
use style::media_queries::MediaList;
use style::parser::ParserContext as CssParserContext;
use style::properties::longhands::{ use style::properties::longhands::{
self, background_image, border_spacing, font_family, font_size, self, background_image, border_spacing, font_family, font_size,
}; };
@ -50,13 +52,14 @@ use style::selector_parser::{
}; };
use style::shared_lock::{Locked, SharedRwLock}; use style::shared_lock::{Locked, SharedRwLock};
use style::stylesheets::layer_rule::LayerOrder; use style::stylesheets::layer_rule::LayerOrder;
use style::stylesheets::{CssRuleType, UrlExtraData}; use style::stylesheets::{CssRuleType, Origin as CssOrigin, UrlExtraData};
use style::values::computed::Overflow; use style::values::computed::Overflow;
use style::values::generics::NonNegative; use style::values::generics::NonNegative;
use style::values::generics::position::PreferredRatio; use style::values::generics::position::PreferredRatio;
use style::values::generics::ratio::Ratio; use style::values::generics::ratio::Ratio;
use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified}; use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified};
use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state}; use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state};
use style_traits::ParsingMode as CssParsingMode;
use stylo_atoms::Atom; use stylo_atoms::Atom;
use stylo_dom::ElementState; use stylo_dom::ElementState;
use xml5ever::serialize::TraversalScope::{ use xml5ever::serialize::TraversalScope::{
@ -66,7 +69,7 @@ use xml5ever::serialize::TraversalScope::{
use crate::conversions::Convert; use crate::conversions::Convert;
use crate::dom::activation::Activatable; use crate::dom::activation::Activatable;
use crate::dom::attr::{Attr, AttrHelpersForLayout, is_relevant_attribute}; use crate::dom::attr::{Attr, AttrHelpersForLayout, is_relevant_attribute};
use crate::dom::bindings::cell::{DomRefCell, Ref, RefMut, ref_filter_map}; use crate::dom::bindings::cell::{DomRefCell, Ref, RefMut};
use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::{ use crate::dom::bindings::codegen::Bindings::ElementBinding::{
@ -781,6 +784,33 @@ impl Element {
.registered_intersection_observers .registered_intersection_observers
.retain(|reg_obs| *reg_obs.observer != *observer) .retain(|reg_obs| *reg_obs.observer != *observer)
} }
/// <https://html.spec.whatwg.org/multipage/#matches-the-environment>
pub(crate) fn matches_environment(&self, media_query: &str) -> bool {
let document = self.owner_document();
let quirks_mode = document.quirks_mode();
let document_url_data = UrlExtraData(document.url().get_arc());
// FIXME(emilio): This should do the same that we do for other media
// lists regarding the rule type and such, though it doesn't really
// matter right now...
//
// Also, ParsingMode::all() is wrong, and should be DEFAULT.
let context = CssParserContext::new(
CssOrigin::Author,
&document_url_data,
Some(CssRuleType::Style),
CssParsingMode::all(),
quirks_mode,
/* namespaces = */ Default::default(),
None,
None,
);
let mut parser_input = CssParserInput::new(media_query);
let mut parser = CssParser::new(&mut parser_input);
let media_list = MediaList::parse(&context, &mut parser);
let result = media_list.evaluate(document.window().layout().device(), quirks_mode);
result
}
} }
/// <https://dom.spec.whatwg.org/#valid-shadow-host-name> /// <https://dom.spec.whatwg.org/#valid-shadow-host-name>
@ -1480,7 +1510,7 @@ impl Element {
// is "xmlns", and local name is prefix, or if prefix is null and it has an attribute // is "xmlns", and local name is prefix, or if prefix is null and it has an attribute
// whose namespace is the XMLNS namespace, namespace prefix is null, and local name is // whose namespace is the XMLNS namespace, namespace prefix is null, and local name is
// "xmlns", then return its value if it is not the empty string, and null otherwise." // "xmlns", then return its value if it is not the empty string, and null otherwise."
let attr = ref_filter_map(self.attrs(), |attrs| { let attr = Ref::filter_map(self.attrs(), |attrs| {
attrs.iter().find(|attr| { attrs.iter().find(|attr| {
if attr.namespace() != &ns!(xmlns) { if attr.namespace() != &ns!(xmlns) {
return false; return false;
@ -1493,7 +1523,8 @@ impl Element {
_ => false, _ => false,
} }
}) })
}); })
.ok();
if let Some(attr) = attr { if let Some(attr) = attr {
return (**attr.value()).into(); return (**attr.value()).into();
@ -1631,6 +1662,27 @@ impl Element {
) )
} }
/// Returns the focusable shadow host if this is a text control inner editor.
/// This is a workaround for the focus delegation of shadow DOM and should be
/// used only to delegate focusable inner editor of [HTMLInputElement] and
/// [HTMLTextAreaElement].
pub(crate) fn find_focusable_shadow_host_if_necessary(&self) -> Option<DomRoot<Element>> {
if self.is_focusable_area() {
Some(DomRoot::from_ref(self))
} else if self.upcast::<Node>().is_text_control_inner_editor() {
let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host());
assert!(
containing_shadow_host
.as_ref()
.is_some_and(|e| e.is_focusable_area()),
"Containing shadow host is not focusable"
);
containing_shadow_host
} else {
None
}
}
pub(crate) fn is_actually_disabled(&self) -> bool { pub(crate) fn is_actually_disabled(&self) -> bool {
let node = self.upcast::<Node>(); let node = self.upcast::<Node>();
match node.type_id() { match node.type_id() {

View file

@ -74,6 +74,7 @@ use super::bindings::codegen::Bindings::WebGPUBinding::GPUDeviceLostReason;
use super::bindings::error::Fallible; use super::bindings::error::Fallible;
use super::bindings::trace::{HashMapTracedValues, RootedTraceableBox}; use super::bindings::trace::{HashMapTracedValues, RootedTraceableBox};
use super::serviceworkerglobalscope::ServiceWorkerGlobalScope; use super::serviceworkerglobalscope::ServiceWorkerGlobalScope;
use super::transformstream::CrossRealmTransform;
use crate::dom::bindings::cell::{DomRefCell, RefMut}; use crate::dom::bindings::cell::{DomRefCell, RefMut};
use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::BroadcastChannelMethods; use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::BroadcastChannelMethods;
use crate::dom::bindings::codegen::Bindings::EventSourceBinding::EventSource_Binding::EventSourceMethods; use crate::dom::bindings::codegen::Bindings::EventSourceBinding::EventSource_Binding::EventSourceMethods;
@ -458,13 +459,9 @@ pub(crate) struct ManagedMessagePort {
/// Whether the port has been closed by script in this global, /// Whether the port has been closed by script in this global,
/// so it can be removed. /// so it can be removed.
explicitly_closed: bool, explicitly_closed: bool,
/// Note: it may seem strange to use a pair of options, versus for example an enum. /// The handler for `message` or `messageerror` used in the cross realm transform,
/// But it looks like tranform streams will require both of those in their transfer. /// if any was setup with this port.
/// This will be resolved when we reach that point of the implementation. cross_realm_transform: Option<CrossRealmTransform>,
/// <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
cross_realm_transform_readable: Option<CrossRealmTransformReadable>,
/// <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
cross_realm_transform_writable: Option<CrossRealmTransformWritable>,
} }
/// State representing whether this global is currently managing broadcast channels. /// State representing whether this global is currently managing broadcast channels.
@ -1345,7 +1342,9 @@ impl GlobalScope {
unreachable!("Cross realm transform readable must match a managed port"); unreachable!("Cross realm transform readable must match a managed port");
}; };
managed_port.cross_realm_transform_readable = Some(cross_realm_transform_readable.clone()); managed_port.cross_realm_transform = Some(CrossRealmTransform::Readable(
cross_realm_transform_readable.clone(),
));
} }
/// <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> /// <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
@ -1368,7 +1367,9 @@ impl GlobalScope {
unreachable!("Cross realm transform writable must match a managed port"); unreachable!("Cross realm transform writable must match a managed port");
}; };
managed_port.cross_realm_transform_writable = Some(cross_realm_transform_writable.clone()); managed_port.cross_realm_transform = Some(CrossRealmTransform::Writable(
cross_realm_transform_writable.clone(),
));
} }
/// Custom routing logic, followed by the task steps of /// Custom routing logic, followed by the task steps of
@ -1380,8 +1381,7 @@ impl GlobalScope {
can_gc: CanGc, can_gc: CanGc,
) { ) {
let cx = GlobalScope::get_cx(); let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut cross_realm_transform_readable = None); rooted!(in(*cx) let mut cross_realm_transform = None);
rooted!(in(*cx) let mut cross_realm_transform_writable = None);
let should_dispatch = if let MessagePortState::Managed(_id, message_ports) = let should_dispatch = if let MessagePortState::Managed(_id, message_ports) =
&mut *self.message_port_state.borrow_mut() &mut *self.message_port_state.borrow_mut()
@ -1399,10 +1399,7 @@ impl GlobalScope {
let to_dispatch = port_impl.handle_incoming(task).map(|to_dispatch| { let to_dispatch = port_impl.handle_incoming(task).map(|to_dispatch| {
(DomRoot::from_ref(&*managed_port.dom_port), to_dispatch) (DomRoot::from_ref(&*managed_port.dom_port), to_dispatch)
}); });
cross_realm_transform_readable cross_realm_transform.set(managed_port.cross_realm_transform.clone());
.set(managed_port.cross_realm_transform_readable.clone());
cross_realm_transform_writable
.set(managed_port.cross_realm_transform_writable.clone());
to_dispatch to_dispatch
} else { } else {
panic!("managed-port has no port-impl."); panic!("managed-port has no port-impl.");
@ -1429,10 +1426,6 @@ impl GlobalScope {
// Re-ordered because we need to pass it to `structuredclone::read`. // Re-ordered because we need to pass it to `structuredclone::read`.
rooted!(in(*cx) let mut message_clone = UndefinedValue()); rooted!(in(*cx) let mut message_clone = UndefinedValue());
// Note: if this port is used to transfer a stream, we handle the events in Rust.
let has_cross_realm_tansform = cross_realm_transform_readable.is_some() ||
cross_realm_transform_writable.is_some();
let realm = enter_realm(self); let realm = enter_realm(self);
let comp = InRealm::Entered(&realm); let comp = InRealm::Entered(&realm);
@ -1447,10 +1440,13 @@ impl GlobalScope {
// if any, maintaining their relative order. // if any, maintaining their relative order.
// Note: both done in `structuredclone::read`. // Note: both done in `structuredclone::read`.
if let Ok(ports) = structuredclone::read(self, data, message_clone.handle_mut()) { if let Ok(ports) = structuredclone::read(self, data, message_clone.handle_mut()) {
// Note: if this port is used to transfer a stream, we handle the events in Rust.
if let Some(transform) = cross_realm_transform.as_ref() {
match transform {
// Add a handler for ports message event with the following steps: // Add a handler for ports message event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable> // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
if let Some(transform) = cross_realm_transform_readable.as_ref() { CrossRealmTransform::Readable(readable) => {
transform.handle_message( readable.handle_message(
cx, cx,
self, self,
&dom_port, &dom_port,
@ -1458,15 +1454,14 @@ impl GlobalScope {
comp, comp,
can_gc, can_gc,
); );
} },
// Add a handler for ports message event with the following steps: // Add a handler for ports message event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
if let Some(transform) = cross_realm_transform_writable.as_ref() { CrossRealmTransform::Writable(writable) => {
transform.handle_message(cx, self, message_clone.handle(), comp, can_gc); writable.handle_message(cx, self, message_clone.handle(), comp, can_gc);
},
} }
} else {
if !has_cross_realm_tansform {
// Fire an event named message at messageEventTarget, // Fire an event named message at messageEventTarget,
// using MessageEvent, // using MessageEvent,
// with the data attribute initialized to messageClone // with the data attribute initialized to messageClone
@ -1481,20 +1476,20 @@ impl GlobalScope {
can_gc, can_gc,
); );
} }
} else { } else if let Some(transform) = cross_realm_transform.as_ref() {
match transform {
// Add a handler for ports messageerror event with the following steps: // Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable> // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
if let Some(transform) = cross_realm_transform_readable.as_ref() { CrossRealmTransform::Readable(readable) => {
transform.handle_error(cx, self, &dom_port, comp, can_gc); readable.handle_error(cx, self, &dom_port, comp, can_gc);
} },
// Add a handler for ports messageerror event with the following steps: // Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
if let Some(transform) = cross_realm_transform_writable.as_ref() { CrossRealmTransform::Writable(writable) => {
transform.handle_error(cx, self, &dom_port, comp, can_gc); writable.handle_error(cx, self, &dom_port, comp, can_gc);
},
} }
} else {
if !has_cross_realm_tansform {
// If this throws an exception, catch it, // If this throws an exception, catch it,
// fire an event named messageerror at messageEventTarget, // fire an event named messageerror at messageEventTarget,
// using MessageEvent, and then return. // using MessageEvent, and then return.
@ -1502,7 +1497,6 @@ impl GlobalScope {
} }
} }
} }
}
/// Check all ports that have been transfer-received in the previous task, /// Check all ports that have been transfer-received in the previous task,
/// and complete their transfer if they haven't been re-transferred. /// and complete their transfer if they haven't been re-transferred.
@ -1689,8 +1683,7 @@ impl GlobalScope {
dom_port: Dom::from_ref(dom_port), dom_port: Dom::from_ref(dom_port),
pending: true, pending: true,
explicitly_closed: false, explicitly_closed: false,
cross_realm_transform_readable: None, cross_realm_transform: None,
cross_realm_transform_writable: None,
}, },
); );
@ -1713,8 +1706,7 @@ impl GlobalScope {
dom_port: Dom::from_ref(dom_port), dom_port: Dom::from_ref(dom_port),
pending: false, pending: false,
explicitly_closed: false, explicitly_closed: false,
cross_realm_transform_readable: None, cross_realm_transform: None,
cross_realm_transform_writable: None,
}, },
); );
let _ = self.script_to_constellation_chan().send( let _ = self.script_to_constellation_chan().send(
@ -2990,13 +2982,13 @@ impl GlobalScope {
return p; return p;
} }
if let Some(snapshot) = canvas.get_image_data() { match canvas.get_image_data() {
let size = snapshot.size().cast(); Some(snapshot) => {
let image_bitmap = let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
ImageBitmap::new(self, size.width, size.height, can_gc).unwrap();
image_bitmap.set_bitmap_data(snapshot.to_vec());
image_bitmap.set_origin_clean(canvas.origin_is_clean()); image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc); p.resolve_native(&(image_bitmap), can_gc);
},
None => p.reject_error(Error::InvalidState, can_gc),
} }
p p
}, },
@ -3007,13 +2999,13 @@ impl GlobalScope {
return p; return p;
} }
if let Some(snapshot) = canvas.get_image_data() { match canvas.get_image_data() {
let size = snapshot.size().cast(); Some(snapshot) => {
let image_bitmap = let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
ImageBitmap::new(self, size.width, size.height, can_gc).unwrap();
image_bitmap.set_bitmap_data(snapshot.to_vec());
image_bitmap.set_origin_clean(canvas.origin_is_clean()); image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc); p.resolve_native(&(image_bitmap), can_gc);
},
None => p.reject_error(Error::InvalidState, can_gc),
} }
p p
}, },

View file

@ -5,14 +5,15 @@
use std::cell::Cell; use std::cell::Cell;
use std::str::{self, FromStr}; use std::str::{self, FromStr};
use data_url::mime::Mime as DataUrlMime;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue}; use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue};
use js::rust::HandleObject; use js::rust::HandleObject;
use net_traits::fetch::headers::{ use net_traits::fetch::headers::{
get_decode_and_split_header_value, get_value_from_header_list, is_forbidden_method, extract_mime_type, get_decode_and_split_header_value, get_value_from_header_list,
is_forbidden_method,
}; };
use net_traits::request::is_cors_safelisted_request_header; use net_traits::request::is_cors_safelisted_request_header;
use net_traits::trim_http_whitespace;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods}; use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods};
@ -33,7 +34,7 @@ pub(crate) struct Headers {
header_list: DomRefCell<HyperHeaders>, header_list: DomRefCell<HyperHeaders>,
} }
// https://fetch.spec.whatwg.org/#concept-headers-guard /// <https://fetch.spec.whatwg.org/#concept-headers-guard>
#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
pub(crate) enum Guard { pub(crate) enum Guard {
Immutable, Immutable,
@ -66,7 +67,7 @@ impl Headers {
} }
impl HeadersMethods<crate::DomTypeHolder> for Headers { impl HeadersMethods<crate::DomTypeHolder> for Headers {
// https://fetch.spec.whatwg.org/#dom-headers /// <https://fetch.spec.whatwg.org/#dom-headers>
fn Constructor( fn Constructor(
global: &GlobalScope, global: &GlobalScope,
proto: Option<HandleObject>, proto: Option<HandleObject>,
@ -78,47 +79,41 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
Ok(dom_headers_new) Ok(dom_headers_new)
} }
// https://fetch.spec.whatwg.org/#concept-headers-append /// <https://fetch.spec.whatwg.org/#concept-headers-append>
fn Append(&self, name: ByteString, value: ByteString) -> ErrorResult { fn Append(&self, name: ByteString, value: ByteString) -> ErrorResult {
// Step 1 // 1. Normalize value.
let value = normalize_value(value); let value = trim_http_whitespace(&value);
// Step 2 // 2. If validating (name, value) for headers returns false, then return.
// https://fetch.spec.whatwg.org/#headers-validate let Some((mut valid_name, valid_value)) =
let (mut valid_name, valid_value) = validate_name_and_value(name, value)?; self.validate_name_and_value(name, ByteString::new(value.into()))?
else {
return Ok(());
};
valid_name = valid_name.to_lowercase(); valid_name = valid_name.to_lowercase();
if self.guard.get() == Guard::Immutable { // 3. If headerss guard is "request-no-cors":
return Err(Error::Type("Guard is immutable".to_string()));
}
if self.guard.get() == Guard::Request &&
is_forbidden_request_header(&valid_name, &valid_value)
{
return Ok(());
}
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
return Ok(());
}
// Step 3
if self.guard.get() == Guard::RequestNoCors { if self.guard.get() == Guard::RequestNoCors {
// 3.1. Let temporaryValue be the result of getting name from headerss header list.
let tmp_value = if let Some(mut value) = let tmp_value = if let Some(mut value) =
get_value_from_header_list(&valid_name, &self.header_list.borrow()) get_value_from_header_list(&valid_name, &self.header_list.borrow())
{ {
// 3.3. Otherwise, set temporaryValue to temporaryValue, followed by 0x2C 0x20, followed by value.
value.extend(b", "); value.extend(b", ");
value.extend(valid_value.clone()); value.extend(valid_value.to_vec());
value value
} else { } else {
valid_value.clone() // 3.2. If temporaryValue is null, then set temporaryValue to value.
valid_value.to_vec()
}; };
// 3.4. If (name, temporaryValue) is not a no-CORS-safelisted request-header, then return.
if !is_cors_safelisted_request_header(&valid_name, &tmp_value) { if !is_cors_safelisted_request_header(&valid_name, &tmp_value) {
return Ok(()); return Ok(());
} }
} }
// Step 4 // 4. Append (name, value) to headerss header list.
match HeaderValue::from_bytes(&valid_value) { match HeaderValue::from_bytes(&valid_value) {
Ok(value) => { Ok(value) => {
self.header_list self.header_list
@ -134,7 +129,7 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
}, },
}; };
// Step 5 // 5. If headerss guard is "request-no-cors", then remove privileged no-CORS request-headers from headers.
if self.guard.get() == Guard::RequestNoCors { if self.guard.get() == Guard::RequestNoCors {
self.remove_privileged_no_cors_request_headers(); self.remove_privileged_no_cors_request_headers();
} }
@ -142,50 +137,53 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
Ok(()) Ok(())
} }
// https://fetch.spec.whatwg.org/#dom-headers-delete /// <https://fetch.spec.whatwg.org/#dom-headers-delete>
fn Delete(&self, name: ByteString) -> ErrorResult { fn Delete(&self, name: ByteString) -> ErrorResult {
// Step 1 // Step 1 If validating (name, ``) for this returns false, then return.
let (mut valid_name, valid_value) = validate_name_and_value(name, ByteString::new(vec![]))?; let name_and_value = self.validate_name_and_value(name, ByteString::new(vec![]))?;
let Some((mut valid_name, _valid_value)) = name_and_value else {
return Ok(());
};
valid_name = valid_name.to_lowercase(); valid_name = valid_name.to_lowercase();
// Step 2 // Step 2 If thiss guard is "request-no-cors", name is not a no-CORS-safelisted request-header name,
if self.guard.get() == Guard::Immutable { // and name is not a privileged no-CORS request-header name, then return.
return Err(Error::Type("Guard is immutable".to_string()));
}
// Step 3
if self.guard.get() == Guard::Request &&
is_forbidden_request_header(&valid_name, &valid_value)
{
return Ok(());
}
// Step 4
if self.guard.get() == Guard::RequestNoCors && if self.guard.get() == Guard::RequestNoCors &&
!is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec()) !is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec())
{ {
return Ok(()); return Ok(());
} }
// Step 5
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) { // 3. If thiss header list does not contain name, then return.
return Ok(()); // 4. Delete name from thiss header list.
self.header_list.borrow_mut().remove(valid_name);
// 5. If thiss guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
if self.guard.get() == Guard::RequestNoCors {
self.remove_privileged_no_cors_request_headers();
} }
// Step 6
self.header_list.borrow_mut().remove(&valid_name);
Ok(()) Ok(())
} }
// https://fetch.spec.whatwg.org/#dom-headers-get /// <https://fetch.spec.whatwg.org/#dom-headers-get>
fn Get(&self, name: ByteString) -> Fallible<Option<ByteString>> { fn Get(&self, name: ByteString) -> Fallible<Option<ByteString>> {
// Step 1 // 1. If name is not a header name, then throw a TypeError.
let valid_name = validate_name(name)?; let valid_name = validate_name(name)?;
// 2. Return the result of getting name from thiss header list.
Ok( Ok(
get_value_from_header_list(&valid_name, &self.header_list.borrow()) get_value_from_header_list(&valid_name, &self.header_list.borrow())
.map(ByteString::new), .map(ByteString::new),
) )
} }
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie /// <https://fetch.spec.whatwg.org/#dom-headers-getsetcookie>
fn GetSetCookie(&self) -> Vec<ByteString> { fn GetSetCookie(&self) -> Vec<ByteString> {
// 1. If thiss header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in thiss header list whose name is a
// byte-case-insensitive match for `Set-Cookie`, in order.
self.header_list self.header_list
.borrow() .borrow()
.get_all("set-cookie") .get_all("set-cookie")
@ -194,42 +192,36 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
.collect() .collect()
} }
// https://fetch.spec.whatwg.org/#dom-headers-has /// <https://fetch.spec.whatwg.org/#dom-headers-has>
fn Has(&self, name: ByteString) -> Fallible<bool> { fn Has(&self, name: ByteString) -> Fallible<bool> {
// Step 1 // 1. If name is not a header name, then throw a TypeError.
let valid_name = validate_name(name)?; let valid_name = validate_name(name)?;
// Step 2 // 2. Return true if thiss header list contains name; otherwise false.
Ok(self.header_list.borrow_mut().get(&valid_name).is_some()) Ok(self.header_list.borrow_mut().get(&valid_name).is_some())
} }
// https://fetch.spec.whatwg.org/#dom-headers-set /// <https://fetch.spec.whatwg.org/#dom-headers-set>
fn Set(&self, name: ByteString, value: ByteString) -> Fallible<()> { fn Set(&self, name: ByteString, value: ByteString) -> Fallible<()> {
// Step 1 // 1. Normalize value
let value = normalize_value(value); let value = trim_http_whitespace(&value);
// Step 2
let (mut valid_name, valid_value) = validate_name_and_value(name, value)?; // 2. If validating (name, value) for this returns false, then return.
let Some((mut valid_name, valid_value)) =
self.validate_name_and_value(name, ByteString::new(value.into()))?
else {
return Ok(());
};
valid_name = valid_name.to_lowercase(); valid_name = valid_name.to_lowercase();
// Step 3
if self.guard.get() == Guard::Immutable { // 3. If thiss guard is "request-no-cors" and (name, value) is not a
return Err(Error::Type("Guard is immutable".to_string())); // no-CORS-safelisted request-header, then return.
}
// Step 4
if self.guard.get() == Guard::Request &&
is_forbidden_request_header(&valid_name, &valid_value)
{
return Ok(());
}
// Step 5
if self.guard.get() == Guard::RequestNoCors && if self.guard.get() == Guard::RequestNoCors &&
!is_cors_safelisted_request_header(&valid_name, &valid_value) !is_cors_safelisted_request_header(&valid_name, &valid_value.to_vec())
{ {
return Ok(()); return Ok(());
} }
// Step 6
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) { // 4. Set (name, value) in thiss header list.
return Ok(());
}
// Step 7
// https://fetch.spec.whatwg.org/#concept-header-list-set // https://fetch.spec.whatwg.org/#concept-header-list-set
match HeaderValue::from_bytes(&valid_value) { match HeaderValue::from_bytes(&valid_value) {
Ok(value) => { Ok(value) => {
@ -245,6 +237,12 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
); );
}, },
}; };
// 5. If thiss guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
if self.guard.get() == Guard::RequestNoCors {
self.remove_privileged_no_cors_request_headers();
}
Ok(()) Ok(())
} }
} }
@ -260,7 +258,7 @@ impl Headers {
Ok(()) Ok(())
} }
// https://fetch.spec.whatwg.org/#concept-headers-fill /// <https://fetch.spec.whatwg.org/#concept-headers-fill>
pub(crate) fn fill(&self, filler: Option<HeadersInit>) -> ErrorResult { pub(crate) fn fill(&self, filler: Option<HeadersInit>) -> ErrorResult {
match filler { match filler {
Some(HeadersInit::ByteStringSequenceSequence(v)) => { Some(HeadersInit::ByteStringSequenceSequence(v)) => {
@ -316,12 +314,12 @@ impl Headers {
self.header_list.borrow_mut().clone() self.header_list.borrow_mut().clone()
} }
// https://fetch.spec.whatwg.org/#concept-header-extract-mime-type /// <https://fetch.spec.whatwg.org/#concept-header-extract-mime-type>
pub(crate) fn extract_mime_type(&self) -> Vec<u8> { pub(crate) fn extract_mime_type(&self) -> Vec<u8> {
extract_mime_type(&self.header_list.borrow()).unwrap_or_default() extract_mime_type(&self.header_list.borrow()).unwrap_or_default()
} }
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine /// <https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine>
pub(crate) fn sort_and_combine(&self) -> Vec<(String, Vec<u8>)> { pub(crate) fn sort_and_combine(&self) -> Vec<(String, Vec<u8>)> {
let borrowed_header_list = self.header_list.borrow(); let borrowed_header_list = self.header_list.borrow();
let mut header_vec = vec![]; let mut header_vec = vec![];
@ -341,11 +339,38 @@ impl Headers {
header_vec header_vec
} }
// https://fetch.spec.whatwg.org/#ref-for-privileged-no-cors-request-header-name /// <https://fetch.spec.whatwg.org/#ref-for-privileged-no-cors-request-header-name>
pub(crate) fn remove_privileged_no_cors_request_headers(&self) { pub(crate) fn remove_privileged_no_cors_request_headers(&self) {
// https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name // <https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name>
self.header_list.borrow_mut().remove("range"); self.header_list.borrow_mut().remove("range");
} }
/// <https://fetch.spec.whatwg.org/#headers-validate>
pub(crate) fn validate_name_and_value(
&self,
name: ByteString,
value: ByteString,
) -> Fallible<Option<(String, ByteString)>> {
// 1. If name is not a header name or value is not a header value, then throw a TypeError.
let valid_name = validate_name(name)?;
if !is_legal_header_value(&value) {
return Err(Error::Type("Header value is not valid".to_string()));
}
// 2. If headerss guard is "immutable", then throw a TypeError.
if self.guard.get() == Guard::Immutable {
return Err(Error::Type("Guard is immutable".to_string()));
}
// 3. If headerss guard is "request" and (name, value) is a forbidden request-header, then return false.
if self.guard.get() == Guard::Request && is_forbidden_request_header(&valid_name, &value) {
return Ok(None);
}
// 4. If headerss guard is "response" and name is a forbidden response-header name, then return false.
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
return Ok(None);
}
Ok(Some((valid_name, value)))
}
} }
impl Iterable for Headers { impl Iterable for Headers {
@ -391,6 +416,7 @@ pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
"keep-alive", "keep-alive",
"origin", "origin",
"referer", "referer",
"set-cookie",
"te", "te",
"trailer", "trailer",
"transfer-encoding", "transfer-encoding",
@ -448,26 +474,11 @@ pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
false false
} }
// https://fetch.spec.whatwg.org/#forbidden-response-header-name /// <https://fetch.spec.whatwg.org/#forbidden-response-header-name>
fn is_forbidden_response_header(name: &str) -> bool { fn is_forbidden_response_header(name: &str) -> bool {
matches!(name, "set-cookie" | "set-cookie2") // A forbidden response-header name is a header name that is a byte-case-insensitive match for one of
} let name = name.to_ascii_lowercase();
matches!(name.as_str(), "set-cookie" | "set-cookie2")
// There is some unresolved confusion over the definition of a name and a value.
//
// As of December 2019, WHATWG has no formal grammar production for value;
// https://fetch.spec.whatg.org/#concept-header-value just says not to have
// newlines, nulls, or leading/trailing whitespace. It even allows
// octets that aren't a valid UTF-8 encoding, and WPT tests reflect this.
// The HeaderValue class does not fully reflect this, so headers
// containing bytes with values 1..31 or 127 can't be created, failing
// WPT tests but probably not affecting anything important on the real Internet.
fn validate_name_and_value(name: ByteString, value: ByteString) -> Fallible<(String, Vec<u8>)> {
let valid_name = validate_name(name)?;
if !is_legal_header_value(&value) {
return Err(Error::Type("Header value is not valid".to_string()));
}
Ok((valid_name, value.into()))
} }
fn validate_name(name: ByteString) -> Fallible<String> { fn validate_name(name: ByteString) -> Fallible<String> {
@ -480,47 +491,20 @@ fn validate_name(name: ByteString) -> Fallible<String> {
} }
} }
// Removes trailing and leading HTTP whitespace bytes. /// <http://tools.ietf.org/html/rfc7230#section-3.2>
// https://fetch.spec.whatwg.org/#concept-header-value-normalize
pub fn normalize_value(value: ByteString) -> ByteString {
match (
index_of_first_non_whitespace(&value),
index_of_last_non_whitespace(&value),
) {
(Some(begin), Some(end)) => ByteString::new(value[begin..end + 1].to_owned()),
_ => ByteString::new(vec![]),
}
}
fn is_http_whitespace(byte: u8) -> bool {
byte == b'\t' || byte == b'\n' || byte == b'\r' || byte == b' '
}
fn index_of_first_non_whitespace(value: &ByteString) -> Option<usize> {
for (index, &byte) in value.iter().enumerate() {
if !is_http_whitespace(byte) {
return Some(index);
}
}
None
}
fn index_of_last_non_whitespace(value: &ByteString) -> Option<usize> {
for (index, &byte) in value.iter().enumerate().rev() {
if !is_http_whitespace(byte) {
return Some(index);
}
}
None
}
// http://tools.ietf.org/html/rfc7230#section-3.2
fn is_field_name(name: &ByteString) -> bool { fn is_field_name(name: &ByteString) -> bool {
is_token(name) is_token(name)
} }
// https://fetch.spec.whatg.org/#concept-header-value // As of December 2019, WHATWG has no formal grammar production for value;
fn is_legal_header_value(value: &ByteString) -> bool { // https://fetch.spec.whatg.org/#concept-header-value just says not to have
// newlines, nulls, or leading/trailing whitespace. It even allows
// octets that aren't a valid UTF-8 encoding, and WPT tests reflect this.
// The HeaderValue class does not fully reflect this, so headers
// containing bytes with values 1..31 or 127 can't be created, failing
// WPT tests but probably not affecting anything important on the real Internet.
/// <https://fetch.spec.whatg.org/#concept-header-value>
fn is_legal_header_value(value: &[u8]) -> bool {
let value_len = value.len(); let value_len = value.len();
if value_len == 0 { if value_len == 0 {
return true; return true;
@ -533,7 +517,7 @@ fn is_legal_header_value(value: &ByteString) -> bool {
b' ' | b'\t' => return false, b' ' | b'\t' => return false,
_ => {}, _ => {},
}; };
for &ch in &value[..] { for &ch in value {
match ch { match ch {
b'\0' | b'\n' | b'\r' => return false, b'\0' | b'\n' | b'\r' => return false,
_ => {}, _ => {},
@ -555,81 +539,12 @@ fn is_legal_header_value(value: &ByteString) -> bool {
// } // }
} }
// https://tools.ietf.org/html/rfc5234#appendix-B.1 /// <https://tools.ietf.org/html/rfc5234#appendix-B.1>
pub(crate) fn is_vchar(x: u8) -> bool { pub(crate) fn is_vchar(x: u8) -> bool {
matches!(x, 0x21..=0x7E) matches!(x, 0x21..=0x7E)
} }
// http://tools.ietf.org/html/rfc7230#section-3.2.6 /// <http://tools.ietf.org/html/rfc7230#section-3.2.6>
pub(crate) fn is_obs_text(x: u8) -> bool { pub(crate) fn is_obs_text(x: u8) -> bool {
matches!(x, 0x80..=0xFF) matches!(x, 0x80..=0xFF)
} }
// https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
// This function uses data_url::Mime to parse the MIME Type because
// mime::Mime does not provide a parser following the Fetch spec
// see https://github.com/hyperium/mime/issues/106
pub(crate) fn extract_mime_type(headers: &HyperHeaders) -> Option<Vec<u8>> {
let mut charset: Option<String> = None;
let mut essence: String = "".to_string();
let mut mime_type: Option<DataUrlMime> = None;
// Step 4
let headers_values = headers.get_all(http::header::CONTENT_TYPE).iter();
// Step 5
if headers_values.size_hint() == (0, Some(0)) {
return None;
}
// Step 6
for header_value in headers_values {
// Step 6.1
match DataUrlMime::from_str(header_value.to_str().unwrap_or("")) {
// Step 6.2
Err(_) => continue,
Ok(temp_mime) => {
let temp_essence = format!("{}/{}", temp_mime.type_, temp_mime.subtype);
// Step 6.2
if temp_essence == "*/*" {
continue;
}
let temp_charset = &temp_mime.get_parameter("charset");
// Step 6.3
mime_type = Some(DataUrlMime {
type_: temp_mime.type_.to_string(),
subtype: temp_mime.subtype.to_string(),
parameters: temp_mime.parameters.clone(),
});
// Step 6.4
if temp_essence != essence {
charset = temp_charset.map(|c| c.to_string());
temp_essence.clone_into(&mut essence);
} else {
// Step 6.5
if temp_charset.is_none() && charset.is_some() {
let DataUrlMime {
type_: t,
subtype: st,
parameters: p,
} = mime_type.unwrap();
let mut params = p;
params.push(("charset".to_string(), charset.clone().unwrap()));
mime_type = Some(DataUrlMime {
type_: t.to_string(),
subtype: st.to_string(),
parameters: params,
})
}
}
},
}
}
// Step 7, 8
mime_type.map(|m| format!("{}", m).into_bytes())
}

View file

@ -46,6 +46,8 @@ pub enum Area {
bottom_right: (f32, f32), bottom_right: (f32, f32),
}, },
Polygon { Polygon {
/// Stored as a flat array of coordinates
/// e.g. [x1, y1, x2, y2, x3, y3] for a triangle
points: Vec<f32>, points: Vec<f32>,
}, },
} }
@ -203,8 +205,28 @@ impl Area {
p.y >= top_left.1 p.y >= top_left.1
}, },
//TODO polygon hit_test Area::Polygon { ref points } => {
_ => false, // Ray-casting algorithm to determine if point is inside polygon
// https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
let mut inside = false;
debug_assert!(points.len() % 2 == 0);
let vertices = points.len() / 2;
for i in 0..vertices {
let next_i = if i + 1 == vertices { 0 } else { i + 1 };
let xi = points[2 * i];
let yi = points[2 * i + 1];
let xj = points[2 * next_i];
let yj = points[2 * next_i + 1];
if (yi > p.y) != (yj > p.y) && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi {
inside = !inside;
}
}
inside
},
} }
} }

View file

@ -16,7 +16,7 @@ use html5ever::{LocalName, Prefix, local_name, ns};
use image::codecs::jpeg::JpegEncoder; use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::PngEncoder; use image::codecs::png::PngEncoder;
use image::codecs::webp::WebPEncoder; use image::codecs::webp::WebPEncoder;
use image::{ColorType, ImageEncoder}; use image::{ColorType, ImageEncoder, ImageError};
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use ipc_channel::ipc::{self as ipcchan}; use ipc_channel::ipc::{self as ipcchan};
use js::error::throw_type_error; use js::error::throw_type_error;
@ -27,11 +27,12 @@ use servo_media::streams::registry::MediaStreamId;
use snapshot::Snapshot; use snapshot::Snapshot;
use style::attr::AttrValue; use style::attr::AttrValue;
use super::node::NodeDamage;
pub(crate) use crate::canvas_context::*; pub(crate) use crate::canvas_context::*;
use crate::conversions::Convert; use crate::conversions::Convert;
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::cell::{DomRefCell, Ref, ref_filter_map}; use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::{ use crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::{
BlobCallback, HTMLCanvasElementMethods, RenderingContext as RootedRenderingContext, BlobCallback, HTMLCanvasElementMethods, RenderingContext as RootedRenderingContext,
}; };
@ -225,7 +226,7 @@ impl LayoutHTMLCanvasElementHelpers for LayoutDom<'_, HTMLCanvasElement> {
impl HTMLCanvasElement { impl HTMLCanvasElement {
pub(crate) fn context(&self) -> Option<Ref<RenderingContext>> { pub(crate) fn context(&self) -> Option<Ref<RenderingContext>> {
ref_filter_map(self.context_mode.borrow(), |ctx| ctx.as_ref()) Ref::filter_map(self.context_mode.borrow(), |ctx| ctx.as_ref()).ok()
} }
fn get_or_init_2d_context(&self, can_gc: CanGc) -> Option<DomRoot<CanvasRenderingContext2D>> { fn get_or_init_2d_context(&self, can_gc: CanGc) -> Option<DomRoot<CanvasRenderingContext2D>> {
@ -361,7 +362,13 @@ impl HTMLCanvasElement {
Some(context) => context.get_image_data(), Some(context) => context.get_image_data(),
None => { None => {
let size = self.get_size(); let size = self.get_size();
if size.width == 0 || size.height == 0 { if size.is_empty() ||
pixels::compute_rgba8_byte_length_if_within_limit(
size.width as usize,
size.height as usize,
)
.is_none()
{
None None
} else { } else {
Some(Snapshot::cleared(size.cast())) Some(Snapshot::cleared(size.cast()))
@ -384,7 +391,7 @@ impl HTMLCanvasElement {
quality: Option<f64>, quality: Option<f64>,
snapshot: &Snapshot, snapshot: &Snapshot,
encoder: &mut W, encoder: &mut W,
) { ) -> Result<(), ImageError> {
// We can't use self.Width() or self.Height() here, since the size of the canvas // We can't use self.Width() or self.Height() here, since the size of the canvas
// may have changed since the snapshot was created. Truncating the dimensions to a // may have changed since the snapshot was created. Truncating the dimensions to a
// u32 can't panic, since the data comes from a canvas which is always smaller than // u32 can't panic, since the data comes from a canvas which is always smaller than
@ -397,9 +404,7 @@ impl HTMLCanvasElement {
EncodedImageType::Png => { EncodedImageType::Png => {
// FIXME(nox): https://github.com/image-rs/image-png/issues/86 // FIXME(nox): https://github.com/image-rs/image-png/issues/86
// FIXME(nox): https://github.com/image-rs/image-png/issues/87 // FIXME(nox): https://github.com/image-rs/image-png/issues/87
PngEncoder::new(encoder) PngEncoder::new(encoder).write_image(canvas_data, width, height, ColorType::Rgba8)
.write_image(canvas_data, width, height, ColorType::Rgba8)
.unwrap();
}, },
EncodedImageType::Jpeg => { EncodedImageType::Jpeg => {
let jpeg_encoder = if let Some(quality) = quality { let jpeg_encoder = if let Some(quality) = quality {
@ -417,16 +422,16 @@ impl HTMLCanvasElement {
JpegEncoder::new(encoder) JpegEncoder::new(encoder)
}; };
jpeg_encoder jpeg_encoder.write_image(canvas_data, width, height, ColorType::Rgba8)
.write_image(canvas_data, width, height, ColorType::Rgba8)
.unwrap();
}, },
EncodedImageType::Webp => { EncodedImageType::Webp => {
// No quality support because of https://github.com/image-rs/image/issues/1984 // No quality support because of https://github.com/image-rs/image/issues/1984
WebPEncoder::new_lossless(encoder) WebPEncoder::new_lossless(encoder).write_image(
.write_image(canvas_data, width, height, ColorType::Rgba8) canvas_data,
.unwrap(); width,
height,
ColorType::Rgba8,
)
}, },
} }
} }
@ -515,17 +520,22 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
mime_type: DOMString, mime_type: DOMString,
quality: HandleValue, quality: HandleValue,
) -> Fallible<USVString> { ) -> Fallible<USVString> {
// Step 1. // Step 1: If this canvas element's bitmap's origin-clean flag is set to false,
// then throw a "SecurityError" DOMException.
if !self.origin_is_clean() { if !self.origin_is_clean() {
return Err(Error::Security); return Err(Error::Security);
} }
// Step 2. // Step 2: If this canvas element's bitmap has no pixels (i.e. either its
// horizontal dimension or its vertical dimension is zero), then return the string
// "data:,". (This is the shortest data: URL; it represents the empty string in a
// text/plain resource.)
if self.Width() == 0 || self.Height() == 0 { if self.Width() == 0 || self.Height() == 0 {
return Ok(USVString("data:,".into())); return Ok(USVString("data:,".into()));
} }
// Step 3. // Step 3: Let file be a serialization of this canvas element's bitmap as a file,
// passing type and quality if given.
let Some(mut snapshot) = self.get_image_data() else { let Some(mut snapshot) = self.get_image_data() else {
return Ok(USVString("data:,".into())); return Ok(USVString("data:,".into()));
}; };
@ -550,12 +560,20 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
&base64::engine::general_purpose::STANDARD, &base64::engine::general_purpose::STANDARD,
); );
self.encode_for_mime_type( if self
.encode_for_mime_type(
&image_type, &image_type,
Self::maybe_quality(quality), Self::maybe_quality(quality),
&snapshot, &snapshot,
&mut encoder, &mut encoder,
); )
.is_err()
{
// Step 4. If file is null, then return "data:,".
return Ok(USVString("data:,".into()));
}
// Step 5. Return a data: URL representing file. [RFC2397]
encoder.into_inner(); encoder.into_inner();
Ok(USVString(url)) Ok(USVString(url))
} }
@ -603,26 +621,37 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
return error!("Expected blob callback, but found none!"); return error!("Expected blob callback, but found none!");
}; };
if let Some(mut snapshot) = result { let Some(mut snapshot) = result else {
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
return;
};
snapshot.transform( snapshot.transform(
snapshot::AlphaMode::Transparent{ premultiplied: false }, snapshot::AlphaMode::Transparent{ premultiplied: false },
snapshot::PixelFormat::RGBA snapshot::PixelFormat::RGBA
); );
// Step 4.1
// If result is non-null, then set result to a serialization of result as a file with // Step 4.1: If result is non-null, then set result to a serialization of
// type and quality if given. // result as a file with type and quality if given.
// Step 4.2: Queue an element task on the canvas blob serialization task
// source given the canvas element to run these steps:
let mut encoded: Vec<u8> = vec![]; let mut encoded: Vec<u8> = vec![];
let blob_impl;
this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded); let blob;
let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); let result = match this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded) {
// Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element Ok(..) => {
let blob = Blob::new(&this.global(), blob_impl, CanGc::note()); // Step 4.2.1: If result is non-null, then set result to a new Blob
// object, created in the relevant realm of this canvas element,
// Step 4.2.2 Invoke callback with « result » and "report". // representing result. [FILEAPI]
let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note()); blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
} else { blob = Blob::new(&this.global(), blob_impl, CanGc::note());
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note()); Some(&*blob)
} }
Err(..) => None,
};
// Step 4.2.2: Invoke callback with « result » and "report".
let _ = callback.Call__(result, ExceptionHandling::Report, CanGc::note());
})); }));
Ok(()) Ok(())
@ -687,8 +716,11 @@ impl VirtualMethods for HTMLCanvasElement {
.unwrap() .unwrap()
.attribute_mutated(attr, mutation, can_gc); .attribute_mutated(attr, mutation, can_gc);
match attr.local_name() { match attr.local_name() {
&local_name!("width") | &local_name!("height") => self.recreate_contexts_after_resize(), &local_name!("width") | &local_name!("height") => {
_ => (), self.recreate_contexts_after_resize();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
},
_ => {},
}; };
} }

View file

@ -16,6 +16,7 @@ use embedder_traits::ViewportDetails;
use html5ever::{LocalName, Prefix, local_name, ns}; use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject; use js::rust::HandleObject;
use net_traits::ReferrerPolicy; use net_traits::ReferrerPolicy;
use net_traits::request::Destination;
use profile_traits::ipc as ProfiledIpc; use profile_traits::ipc as ProfiledIpc;
use script_traits::{NewLayoutInfo, UpdatePipelineIdReason}; use script_traits::{NewLayoutInfo, UpdatePipelineIdReason};
use servo_url::ServoUrl; use servo_url::ServoUrl;
@ -222,6 +223,7 @@ impl HTMLIFrameElement {
old_pipeline_id, old_pipeline_id,
sandbox: sandboxed, sandbox: sandboxed,
viewport_details, viewport_details,
theme: window.theme(),
}; };
window window
.as_global_scope() .as_global_scope()
@ -237,6 +239,7 @@ impl HTMLIFrameElement {
opener: None, opener: None,
load_data, load_data,
viewport_details, viewport_details,
theme: window.theme(),
}; };
self.pipeline_id.set(Some(new_pipeline_id)); self.pipeline_id.set(Some(new_pipeline_id));
@ -249,6 +252,7 @@ impl HTMLIFrameElement {
old_pipeline_id, old_pipeline_id,
sandbox: sandboxed, sandbox: sandboxed,
viewport_details, viewport_details,
theme: window.theme(),
}; };
window window
.as_global_scope() .as_global_scope()
@ -282,6 +286,7 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()), Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_or_current_origin(), document.has_trustworthy_ancestor_or_current_origin(),
); );
load_data.destination = Destination::IFrame;
load_data.policy_container = Some(window.as_global_scope().policy_container()); load_data.policy_container = Some(window.as_global_scope().policy_container());
let element = self.upcast::<Element>(); let element = self.upcast::<Element>();
load_data.srcdoc = String::from(element.get_string_attribute(&local_name!("srcdoc"))); load_data.srcdoc = String::from(element.get_string_attribute(&local_name!("srcdoc")));
@ -375,6 +380,8 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()), Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_or_current_origin(), document.has_trustworthy_ancestor_or_current_origin(),
); );
load_data.destination = Destination::IFrame;
load_data.policy_container = Some(window.as_global_scope().policy_container());
let pipeline_id = self.pipeline_id(); let pipeline_id = self.pipeline_id();
// If the initial `about:blank` page is the current page, load with replacement enabled, // If the initial `about:blank` page is the current page, load with replacement enabled,
@ -382,10 +389,6 @@ impl HTMLIFrameElement {
let is_about_blank = let is_about_blank =
pipeline_id.is_some() && pipeline_id == self.about_blank_pipeline_id.get(); pipeline_id.is_some() && pipeline_id == self.about_blank_pipeline_id.get();
if is_about_blank {
load_data.policy_container = Some(window.as_global_scope().policy_container());
}
let history_handling = if is_about_blank { let history_handling = if is_about_blank {
NavigationHistoryBehavior::Replace NavigationHistoryBehavior::Replace
} else { } else {
@ -425,6 +428,7 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()), Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_or_current_origin(), document.has_trustworthy_ancestor_or_current_origin(),
); );
load_data.destination = Destination::IFrame;
load_data.policy_container = Some(window.as_global_scope().policy_container()); load_data.policy_container = Some(window.as_global_scope().policy_container());
let browsing_context_id = BrowsingContextId::new(); let browsing_context_id = BrowsingContextId::new();
let webview_id = window.window_proxy().webview_id(); let webview_id = window.window_proxy().webview_id();

View file

@ -20,8 +20,8 @@ use js::rust::HandleObject;
use mime::{self, Mime}; use mime::{self, Mime};
use net_traits::http_status::HttpStatus; use net_traits::http_status::HttpStatus;
use net_traits::image_cache::{ use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse, Image, ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable,
PendingImageId, UsePlaceholder, ImageResponse, PendingImageId, UsePlaceholder,
}; };
use net_traits::request::{Destination, Initiator, RequestId}; use net_traits::request::{Destination, Initiator, RequestId};
use net_traits::{ use net_traits::{
@ -29,14 +29,13 @@ use net_traits::{
ResourceFetchTiming, ResourceTimingType, ResourceFetchTiming, ResourceTimingType,
}; };
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
use pixels::{CorsStatus, Image, ImageMetadata}; use pixels::{CorsStatus, ImageMetadata};
use servo_url::ServoUrl; use servo_url::ServoUrl;
use servo_url::origin::MutableOrigin; use servo_url::origin::MutableOrigin;
use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_integer, parse_length}; use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_integer, parse_length};
use style::context::QuirksMode; use style::context::QuirksMode;
use style::media_queries::MediaList;
use style::parser::ParserContext; use style::parser::ParserContext;
use style::stylesheets::{CssRuleType, Origin, UrlExtraData}; use style::stylesheets::{CssRuleType, Origin};
use style::values::specified::AbsoluteLength; use style::values::specified::AbsoluteLength;
use style::values::specified::length::{Length, NoCalcLength}; use style::values::specified::length::{Length, NoCalcLength};
use style::values::specified::source_size_list::SourceSizeList; use style::values::specified::source_size_list::SourceSizeList;
@ -146,9 +145,8 @@ struct ImageRequest {
parsed_url: Option<ServoUrl>, parsed_url: Option<ServoUrl>,
source_url: Option<USVString>, source_url: Option<USVString>,
blocker: DomRefCell<Option<LoadBlocker>>, blocker: DomRefCell<Option<LoadBlocker>>,
#[conditional_malloc_size_of]
#[no_trace] #[no_trace]
image: Option<Arc<Image>>, image: Option<Image>,
#[no_trace] #[no_trace]
metadata: Option<ImageMetadata>, metadata: Option<ImageMetadata>,
#[no_trace] #[no_trace]
@ -177,7 +175,8 @@ impl HTMLImageElement {
pub(crate) fn is_usable(&self) -> Fallible<bool> { pub(crate) fn is_usable(&self) -> Fallible<bool> {
// If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad. // If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad.
if let Some(image) = &self.current_request.borrow().image { if let Some(image) = &self.current_request.borrow().image {
if image.width == 0 || image.height == 0 { let intrinsic_size = image.metadata();
if intrinsic_size.width == 0 || intrinsic_size.height == 0 {
return Ok(false); return Ok(false);
} }
} }
@ -191,7 +190,7 @@ impl HTMLImageElement {
} }
} }
pub(crate) fn image_data(&self) -> Option<Arc<Image>> { pub(crate) fn image_data(&self) -> Option<Image> {
self.current_request.borrow().image.clone() self.current_request.borrow().image.clone()
} }
} }
@ -341,10 +340,12 @@ impl HTMLImageElement {
is_placeholder, is_placeholder,
}) => { }) => {
if is_placeholder { if is_placeholder {
if let Some(raster_image) = image.as_raster_image() {
self.process_image_response( self.process_image_response(
ImageResponse::PlaceholderLoaded(image, url), ImageResponse::PlaceholderLoaded(raster_image, url),
can_gc, can_gc,
) )
}
} else { } else {
self.process_image_response(ImageResponse::Loaded(image, url), can_gc) self.process_image_response(ImageResponse::Loaded(image, url), can_gc)
} }
@ -403,7 +404,7 @@ impl HTMLImageElement {
window window
.image_cache() .image_cache()
.add_listener(ImageResponder::new(sender, window.pipeline_id(), id)); .add_listener(ImageLoadListener::new(sender, window.pipeline_id(), id));
} }
fn fetch_request(&self, img_url: &ServoUrl, id: PendingImageId) { fn fetch_request(&self, img_url: &ServoUrl, id: PendingImageId) {
@ -448,11 +449,8 @@ impl HTMLImageElement {
} }
// Steps common to when an image has been loaded. // Steps common to when an image has been loaded.
fn handle_loaded_image(&self, image: Arc<Image>, url: ServoUrl, can_gc: CanGc) { fn handle_loaded_image(&self, image: Image, url: ServoUrl, can_gc: CanGc) {
self.current_request.borrow_mut().metadata = Some(ImageMetadata { self.current_request.borrow_mut().metadata = Some(image.metadata());
height: image.height,
width: image.width,
});
self.current_request.borrow_mut().final_url = Some(url); self.current_request.borrow_mut().final_url = Some(url);
self.current_request.borrow_mut().image = Some(image); self.current_request.borrow_mut().image = Some(image);
self.current_request.borrow_mut().state = State::CompletelyAvailable; self.current_request.borrow_mut().state = State::CompletelyAvailable;
@ -471,7 +469,7 @@ impl HTMLImageElement {
(true, false) (true, false)
}, },
(ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Current) => { (ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Current) => {
self.handle_loaded_image(image, url, can_gc); self.handle_loaded_image(Image::Raster(image), url, can_gc);
(false, true) (false, true)
}, },
(ImageResponse::Loaded(image, url), ImageRequestPhase::Pending) => { (ImageResponse::Loaded(image, url), ImageRequestPhase::Pending) => {
@ -483,7 +481,7 @@ impl HTMLImageElement {
(ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Pending) => { (ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Pending) => {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
self.image_request.set(ImageRequestPhase::Current); self.image_request.set(ImageRequestPhase::Current);
self.handle_loaded_image(image, url, can_gc); self.handle_loaded_image(Image::Raster(image), url, can_gc);
(false, true) (false, true)
}, },
(ImageResponse::MetadataLoaded(meta), ImageRequestPhase::Current) => { (ImageResponse::MetadataLoaded(meta), ImageRequestPhase::Current) => {
@ -536,11 +534,15 @@ impl HTMLImageElement {
can_gc: CanGc, can_gc: CanGc,
) { ) {
match image { match image {
ImageResponse::Loaded(image, url) | ImageResponse::PlaceholderLoaded(image, url) => { ImageResponse::Loaded(image, url) => {
self.pending_request.borrow_mut().metadata = Some(ImageMetadata { self.pending_request.borrow_mut().metadata = Some(image.metadata());
height: image.height, self.pending_request.borrow_mut().final_url = Some(url);
width: image.width, self.pending_request.borrow_mut().image = Some(image);
}); self.finish_reacting_to_environment_change(src, generation, selected_pixel_density);
},
ImageResponse::PlaceholderLoaded(image, url) => {
let image = Image::Raster(image);
self.pending_request.borrow_mut().metadata = Some(image.metadata());
self.pending_request.borrow_mut().final_url = Some(url); self.pending_request.borrow_mut().final_url = Some(url);
self.pending_request.borrow_mut().image = Some(image); self.pending_request.borrow_mut().image = Some(image);
self.finish_reacting_to_environment_change(src, generation, selected_pixel_density); self.finish_reacting_to_environment_change(src, generation, selected_pixel_density);
@ -675,7 +677,7 @@ impl HTMLImageElement {
// Step 4.6 // Step 4.6
if let Some(x) = element.get_attribute(&ns!(), &local_name!("media")) { if let Some(x) = element.get_attribute(&ns!(), &local_name!("media")) {
if !self.matches_environment(x.value().to_string()) { if !elem.matches_environment(&x.value()) {
continue; continue;
} }
} }
@ -719,33 +721,6 @@ impl HTMLImageElement {
result result
} }
/// <https://html.spec.whatwg.org/multipage/#matches-the-environment>
fn matches_environment(&self, media_query: String) -> bool {
let document = self.owner_document();
let quirks_mode = document.quirks_mode();
let document_url_data = UrlExtraData(document.url().get_arc());
// FIXME(emilio): This should do the same that we do for other media
// lists regarding the rule type and such, though it doesn't really
// matter right now...
//
// Also, ParsingMode::all() is wrong, and should be DEFAULT.
let context = ParserContext::new(
Origin::Author,
&document_url_data,
Some(CssRuleType::Style),
ParsingMode::all(),
quirks_mode,
/* namespaces = */ Default::default(),
None,
None,
);
let mut parserInput = ParserInput::new(&media_query);
let mut parser = Parser::new(&mut parserInput);
let media_list = MediaList::parse(&context, &mut parser);
let result = media_list.evaluate(document.window().layout().device(), quirks_mode);
result
}
/// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities> /// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities>
fn normalise_source_densities(&self, source_set: &mut SourceSet, width: Option<Length>) { fn normalise_source_densities(&self, source_set: &mut SourceSet, width: Option<Length>) {
// Step 1 // Step 1
@ -1020,10 +995,7 @@ impl HTMLImageElement {
// set on this element. // set on this element.
self.generation.set(self.generation.get() + 1); self.generation.set(self.generation.get() + 1);
// Step 6.3 // Step 6.3
let metadata = ImageMetadata { let metadata = image.metadata();
height: image.height,
width: image.width,
};
// Step 6.3.2 abort requests // Step 6.3.2 abort requests
self.abort_request( self.abort_request(
State::CompletelyAvailable, State::CompletelyAvailable,
@ -1033,7 +1005,7 @@ impl HTMLImageElement {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
let mut current_request = self.current_request.borrow_mut(); let mut current_request = self.current_request.borrow_mut();
current_request.final_url = Some(img_url.clone()); current_request.final_url = Some(img_url.clone());
current_request.image = Some(image.clone()); current_request.image = Some(image);
current_request.metadata = Some(metadata); current_request.metadata = Some(metadata);
// Step 6.3.6 // Step 6.3.6
current_request.current_pixel_density = pixel_density; current_request.current_pixel_density = pixel_density;
@ -1360,7 +1332,7 @@ impl HTMLImageElement {
pub(crate) fn same_origin(&self, origin: &MutableOrigin) -> bool { pub(crate) fn same_origin(&self, origin: &MutableOrigin) -> bool {
if let Some(ref image) = self.current_request.borrow().image { if let Some(ref image) = self.current_request.borrow().image {
return image.cors_status == CorsStatus::Safe; return image.cors_status() == CorsStatus::Safe;
} }
self.current_request self.current_request
@ -1432,7 +1404,7 @@ impl MicrotaskRunnable for ImageElementMicrotask {
pub(crate) trait LayoutHTMLImageElementHelpers { pub(crate) trait LayoutHTMLImageElementHelpers {
fn image_url(self) -> Option<ServoUrl>; fn image_url(self) -> Option<ServoUrl>;
fn image_density(self) -> Option<f64>; fn image_density(self) -> Option<f64>;
fn image_data(self) -> (Option<Arc<Image>>, Option<ImageMetadata>); fn image_data(self) -> (Option<Image>, Option<ImageMetadata>);
fn get_width(self) -> LengthOrPercentageOrAuto; fn get_width(self) -> LengthOrPercentageOrAuto;
fn get_height(self) -> LengthOrPercentageOrAuto; fn get_height(self) -> LengthOrPercentageOrAuto;
} }
@ -1449,12 +1421,9 @@ impl LayoutHTMLImageElementHelpers for LayoutDom<'_, HTMLImageElement> {
self.current_request().parsed_url.clone() self.current_request().parsed_url.clone()
} }
fn image_data(self) -> (Option<Arc<Image>>, Option<ImageMetadata>) { fn image_data(self) -> (Option<Image>, Option<ImageMetadata>) {
let current_request = self.current_request(); let current_request = self.current_request();
( (current_request.image.clone(), current_request.metadata)
current_request.image.clone(),
current_request.metadata.clone(),
)
} }
fn image_density(self) -> Option<f64> { fn image_density(self) -> Option<f64> {

View file

@ -12,9 +12,13 @@ use std::str::FromStr;
use std::{f64, ptr}; use std::{f64, ptr};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::{FilterPattern, InputMethodType}; use embedder_traits::{
EmbedderMsg, FilterPattern, FormControl as EmbedderFormControl, InputMethodType, RgbColor,
};
use encoding_rs::Encoding; use encoding_rs::Encoding;
use euclid::{Point2D, Rect, Size2D};
use html5ever::{LocalName, Prefix, local_name, ns}; use html5ever::{LocalName, Prefix, local_name, ns};
use ipc_channel::ipc;
use js::jsapi::{ use js::jsapi::{
ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject, ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject,
NewUCRegExpObject, ObjectIsDate, RegExpFlag_UnicodeSets, RegExpFlags, NewUCRegExpObject, ObjectIsDate, RegExpFlag_UnicodeSets, RegExpFlags,
@ -25,7 +29,9 @@ use js::rust::{HandleObject, MutableHandleObject};
use net_traits::blob_url_store::get_blob_origin; use net_traits::blob_url_store::get_blob_origin;
use net_traits::filemanager_thread::FileManagerThreadMsg; use net_traits::filemanager_thread::FileManagerThreadMsg;
use net_traits::{CoreResourceMsg, IpcSend}; use net_traits::{CoreResourceMsg, IpcSend};
use profile_traits::ipc; use script_bindings::codegen::GenericBindings::ShadowRootBinding::{
ShadowRootMode, SlotAssignmentMode,
};
use style::attr::AttrValue; use style::attr::AttrValue;
use style::str::{split_commas, str_join}; use style::str::{split_commas, str_join};
use stylo_atoms::Atom; use stylo_atoms::Atom;
@ -33,12 +39,12 @@ use stylo_dom::ElementState;
use time::{Month, OffsetDateTime, Time}; use time::{Month, OffsetDateTime, Time};
use unicode_bidi::{BidiClass, bidi_class}; use unicode_bidi::{BidiClass, bidi_class};
use url::Url; use url::Url;
use webrender_api::units::DeviceIntRect;
use super::bindings::str::{FromInputValueString, ToInputValueString};
use crate::clipboard_provider::EmbedderClipboardProvider; use crate::clipboard_provider::EmbedderClipboardProvider;
use crate::dom::activation::Activatable; use crate::dom::activation::Activatable;
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods; use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods; use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods;
@ -48,30 +54,33 @@ use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, N
use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::error::{Error, ErrorResult};
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::str::{DOMString, FromInputValueString, ToInputValueString, USVString};
use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::clipboardevent::ClipboardEvent;
use crate::dom::compositionevent::CompositionEvent; use crate::dom::compositionevent::CompositionEvent;
use crate::dom::document::Document; use crate::dom::document::Document;
use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; use crate::dom::element::{AttributeMutation, Element, ElementCreator, LayoutElementHelpers};
use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::file::File; use crate::dom::file::File;
use crate::dom::filelist::{FileList, LayoutFileListHelpers}; use crate::dom::filelist::{FileList, LayoutFileListHelpers};
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::htmldatalistelement::HTMLDataListElement; use crate::dom::htmldatalistelement::HTMLDataListElement;
use crate::dom::htmldivelement::HTMLDivElement;
use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlelement::HTMLElement;
use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; use crate::dom::htmlfieldsetelement::HTMLFieldSetElement;
use crate::dom::htmlformelement::{ use crate::dom::htmlformelement::{
FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom, FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom,
SubmittedFrom, SubmittedFrom,
}; };
use crate::dom::htmlstyleelement::HTMLStyleElement;
use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::keyboardevent::KeyboardEvent;
use crate::dom::mouseevent::MouseEvent; use crate::dom::mouseevent::MouseEvent;
use crate::dom::node::{ use crate::dom::node::{
BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext, BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext,
}; };
use crate::dom::nodelist::NodeList; use crate::dom::nodelist::NodeList;
use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot};
use crate::dom::textcontrol::{TextControlElement, TextControlSelection}; use crate::dom::textcontrol::{TextControlElement, TextControlSelection};
use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
use crate::dom::validitystate::{ValidationFlags, ValidityState}; use crate::dom::validitystate::{ValidationFlags, ValidityState};
@ -92,6 +101,96 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
const PASSWORD_REPLACEMENT_CHAR: char = '●'; const PASSWORD_REPLACEMENT_CHAR: char = '●';
const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen"; const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Contains reference to text control inner editor and placeholder container element in the UA
/// shadow tree for `<input type=text>`. The following is the structure of the shadow tree.
///
/// ```
/// <input type="text">
/// #shadow-root
/// <div id="inner-container">
/// <div id="input-editor"></div>
/// <div id="input-placeholder"></div>
/// </div>
/// </input>
/// ```
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
// But, this could be slower in performance and does have some discrepancies. For example,
// they would try to vertically align <input> text baseline with the baseline of other
// TextNode within an inline flow. Another example is the horizontal scroll.
struct InputTypeTextShadowTree {
text_container: Dom<HTMLDivElement>,
placeholder_container: Dom<HTMLDivElement>,
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Contains references to the elements in the shadow tree for `<input type=range>`.
///
/// The shadow tree consists of a single div with the currently selected color as
/// the background.
struct InputTypeColorShadowTree {
color_value: Dom<HTMLDivElement>,
}
// FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support.
const TEXT_TREE_STYLE: &str = "
#input-editor::selection {
background: rgba(176, 214, 255, 1.0);
color: black;
}
:host:not(:placeholder-shown) #input-placeholder {
visibility: hidden !important
}
#input-editor {
overflow-wrap: normal;
pointer-events: auto;
}
#input-container {
position: relative;
height: 100%;
pointer-events: none;
display: flex;
}
#input-editor, #input-placeholder {
white-space: pre;
margin-block: auto !important;
inset-block: 0 !important;
block-size: fit-content !important;
}
#input-placeholder {
overflow: hidden !important;
position: absolute !important;
color: grey;
pointer-events: none !important;
}
";
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[non_exhaustive]
enum ShadowTree {
Text(InputTypeTextShadowTree),
Color(InputTypeColorShadowTree),
// TODO: Add shadow trees for other input types (range etc) here
}
const COLOR_TREE_STYLE: &str = "
#color-value {
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid gray;
border-radius: 2px;
}
";
/// <https://html.spec.whatwg.org/multipage/#attr-input-type> /// <https://html.spec.whatwg.org/multipage/#attr-input-type>
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)] #[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
@ -172,7 +271,6 @@ impl InputType {
fn is_textual(&self) -> bool { fn is_textual(&self) -> bool {
matches!( matches!(
*self, *self,
InputType::Color |
InputType::Date | InputType::Date |
InputType::DatetimeLocal | InputType::DatetimeLocal |
InputType::Email | InputType::Email |
@ -277,9 +375,16 @@ impl From<&Atom> for InputType {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum ValueMode { enum ValueMode {
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-value>
Value, Value,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default>
Default, Default,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default-on>
DefaultOn, DefaultOn,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-filename>
Filename, Filename,
} }
@ -314,6 +419,7 @@ pub(crate) struct HTMLInputElement {
form_owner: MutNullableDom<HTMLFormElement>, form_owner: MutNullableDom<HTMLFormElement>,
labels_node_list: MutNullableDom<NodeList>, labels_node_list: MutNullableDom<NodeList>,
validity_state: MutNullableDom<ValidityState>, validity_state: MutNullableDom<ValidityState>,
shadow_tree: DomRefCell<Option<ShadowTree>>,
} }
#[derive(JSTraceable)] #[derive(JSTraceable)]
@ -372,6 +478,7 @@ impl HTMLInputElement {
form_owner: Default::default(), form_owner: Default::default(),
labels_node_list: MutNullableDom::new(None), labels_node_list: MutNullableDom::new(None),
validity_state: Default::default(), validity_state: Default::default(),
shadow_tree: Default::default(),
} }
} }
@ -475,6 +582,7 @@ impl HTMLInputElement {
let mut value = textinput.single_line_content().clone(); let mut value = textinput.single_line_content().clone();
self.sanitize_value(&mut value); self.sanitize_value(&mut value);
textinput.set_content(value); textinput.set_content(value);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
} }
fn does_minmaxlength_apply(&self) -> bool { fn does_minmaxlength_apply(&self) -> bool {
@ -803,7 +911,7 @@ impl HTMLInputElement {
.map(DomRoot::from_ref) .map(DomRoot::from_ref)
} }
// https://html.spec.whatwg.org/multipage/#suffering-from-being-missing /// <https://html.spec.whatwg.org/multipage/#suffering-from-being-missing>
fn suffers_from_being_missing(&self, value: &DOMString) -> bool { fn suffers_from_being_missing(&self, value: &DOMString) -> bool {
match self.input_type() { match self.input_type() {
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing // https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing
@ -958,9 +1066,9 @@ impl HTMLInputElement {
failed_flags failed_flags
} }
// https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow /// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow>
// https://html.spec.whatwg.org/multipage/#suffering-from-an-overflow /// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-overflow>
// https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch /// * <https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch>
fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags { fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags {
if value.is_empty() || !self.does_value_as_number_apply() { if value.is_empty() || !self.does_value_as_number_apply() {
return ValidationFlags::empty(); return ValidationFlags::empty();
@ -1014,9 +1122,221 @@ impl HTMLInputElement {
failed_flags failed_flags
} }
/// Return a reference to the ShadowRoot that this element is a host of,
/// or create one if none exists.
fn shadow_root(&self, can_gc: CanGc) -> DomRoot<ShadowRoot> {
self.upcast::<Element>().shadow_root().unwrap_or_else(|| {
self.upcast::<Element>()
.attach_shadow(
IsUserAgentWidget::Yes,
ShadowRootMode::Closed,
false,
false,
true,
SlotAssignmentMode::Manual,
can_gc,
)
.expect("Attaching UA shadow root failed")
})
}
fn create_text_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let shadow_root = self.shadow_root(can_gc);
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
let inner_container =
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
inner_container
.upcast::<Element>()
.SetId(DOMString::from("input-container"), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(inner_container.upcast::<Node>(), can_gc)
.unwrap();
let placeholder_container =
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
placeholder_container
.upcast::<Element>()
.SetId(DOMString::from("input-placeholder"), can_gc);
inner_container
.upcast::<Node>()
.AppendChild(placeholder_container.upcast::<Node>(), can_gc)
.unwrap();
let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
text_container
.upcast::<Element>()
.SetId(DOMString::from("input-editor"), can_gc);
text_container
.upcast::<Node>()
.set_text_control_inner_editor();
inner_container
.upcast::<Node>()
.AppendChild(text_container.upcast::<Node>(), can_gc)
.unwrap();
let style = HTMLStyleElement::new(
local_name!("style"),
None,
&document,
None,
ElementCreator::ScriptCreated,
can_gc,
);
// TODO(stevennovaryo): Either use UA stylesheet with internal pseudo element or preemptively parse
// the stylesheet to reduce the costly operation and avoid CSP related error.
style
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(TEXT_TREE_STYLE)), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(style.upcast::<Node>(), can_gc)
.unwrap();
let _ = self
.shadow_tree
.borrow_mut()
.insert(ShadowTree::Text(InputTypeTextShadowTree {
text_container: text_container.as_traced(),
placeholder_container: placeholder_container.as_traced(),
}));
}
fn text_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeTextShadowTree> {
let has_text_shadow_tree = self
.shadow_tree
.borrow()
.as_ref()
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Text(_)));
if !has_text_shadow_tree {
self.create_text_shadow_tree(can_gc);
}
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
match shadow_tree {
ShadowTree::Text(text_tree) => Some(text_tree),
_ => None,
}
})
.ok()
.expect("UA shadow tree was not created")
}
fn create_color_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let shadow_root = self.shadow_root(can_gc);
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
let color_value = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
color_value
.upcast::<Element>()
.SetId(DOMString::from("color-value"), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(color_value.upcast::<Node>(), can_gc)
.unwrap();
let style = HTMLStyleElement::new(
local_name!("style"),
None,
&document,
None,
ElementCreator::ScriptCreated,
can_gc,
);
style
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(COLOR_TREE_STYLE)), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(style.upcast::<Node>(), can_gc)
.unwrap();
let _ = self
.shadow_tree
.borrow_mut()
.insert(ShadowTree::Color(InputTypeColorShadowTree {
color_value: color_value.as_traced(),
}));
}
/// Get a handle to the shadow tree for this input, assuming it's [InputType] is `Color`.
///
/// If the input is not currently a shadow host, a new shadow tree will be created.
///
/// If the input is a shadow host for a different kind of shadow tree then the old
/// tree will be removed and a new one will be created.
fn color_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeColorShadowTree> {
let has_color_shadow_tree = self
.shadow_tree
.borrow()
.as_ref()
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Color(_)));
if !has_color_shadow_tree {
self.create_color_shadow_tree(can_gc);
}
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
match shadow_tree {
ShadowTree::Color(color_tree) => Some(color_tree),
_ => None,
}
})
.ok()
.expect("UA shadow tree was not created")
}
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
match self.input_type() {
InputType::Text => {
let text_shadow_tree = self.text_shadow_tree(can_gc);
let value = self.Value();
// 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?
let value_text = match value.is_empty() {
false => value,
true => "\u{200B}".into(),
};
text_shadow_tree
.text_container
.upcast::<Node>()
.SetTextContent(Some(value_text), can_gc);
},
InputType::Color => {
let color_shadow_tree = self.color_shadow_tree(can_gc);
let mut value = self.Value();
if value.str().is_valid_simple_color_string() {
value.make_ascii_lowercase();
} else {
value = DOMString::from("#000000");
}
let style = format!("background-color: {value}");
color_shadow_tree
.color_value
.upcast::<Element>()
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
},
_ => {},
}
}
} }
pub(crate) trait LayoutHTMLInputElementHelpers<'dom> { pub(crate) trait LayoutHTMLInputElementHelpers<'dom> {
/// Return a string that represents the contents of the element for layout.
fn value_for_layout(self) -> Cow<'dom, str>; fn value_for_layout(self) -> Cow<'dom, str>;
fn size_for_layout(self) -> u32; fn size_for_layout(self) -> u32;
fn selection_for_layout(self) -> Option<Range<usize>>; fn selection_for_layout(self) -> Option<Range<usize>>;
@ -1075,16 +1395,15 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
Some(filelist) => { Some(filelist) => {
let length = filelist.len(); let length = filelist.len();
if length == 0 { if length == 0 {
return DEFAULT_FILE_INPUT_VALUE.into(); DEFAULT_FILE_INPUT_VALUE.into()
} } else if length == 1 {
if length == 1 {
match filelist.file_for_layout(0) { match filelist.file_for_layout(0) {
Some(file) => return file.name().to_string().into(), Some(file) => file.name().to_string().into(),
None => return DEFAULT_FILE_INPUT_VALUE.into(), None => DEFAULT_FILE_INPUT_VALUE.into(),
} }
} } else {
format!("{} files", length).into() format!("{} files", length).into()
}
}, },
None => DEFAULT_FILE_INPUT_VALUE.into(), None => DEFAULT_FILE_INPUT_VALUE.into(),
} }
@ -1103,6 +1422,9 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
self.placeholder().into() self.placeholder().into()
} }
}, },
InputType::Color => {
unreachable!("Input type color is explicitly not rendered as text");
},
_ => { _ => {
let text = self.get_raw_textinput_value(); let text = self.get_raw_textinput_value();
if !text.is_empty() { if !text.is_empty() {
@ -1178,11 +1500,11 @@ impl TextControlElement for HTMLInputElement {
InputType::Week | InputType::Week |
InputType::Time | InputType::Time |
InputType::DatetimeLocal | InputType::DatetimeLocal |
InputType::Number | InputType::Number => true,
InputType::Color => true,
InputType::Button | InputType::Button |
InputType::Checkbox | InputType::Checkbox |
InputType::Color |
InputType::File | InputType::File |
InputType::Hidden | InputType::Hidden |
InputType::Image | InputType::Image |
@ -1257,7 +1579,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
// https://html.spec.whatwg.org/multipage/#dom-input-checked // https://html.spec.whatwg.org/multipage/#dom-input-checked
fn SetChecked(&self, checked: bool) { fn SetChecked(&self, checked: bool) {
self.update_checked_state(checked, true); self.update_checked_state(checked, true);
update_related_validity_states(self, CanGc::note()) self.value_changed(CanGc::note());
} }
// https://html.spec.whatwg.org/multipage/#dom-input-readonly // https://html.spec.whatwg.org/multipage/#dom-input-readonly
@ -1317,6 +1639,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult { fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult {
match self.value_mode() { match self.value_mode() {
ValueMode::Value => { ValueMode::Value => {
{
// Step 3. // Step 3.
self.value_dirty.set(true); self.value_dirty.set(true);
@ -1333,6 +1656,12 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
// Step 5. // Step 5.
textinput.clear_selection_to_limit(Direction::Forward); textinput.clear_selection_to_limit(Direction::Forward);
} }
}
// Additionaly, update the placeholder shown state. This is
// normally being done in the attributed mutated. And, being
// done in another scope to prevent borrow checker issues.
self.update_placeholder_shown_state();
}, },
ValueMode::Default | ValueMode::DefaultOn => { ValueMode::Default | ValueMode::DefaultOn => {
self.upcast::<Element>() self.upcast::<Element>()
@ -1349,7 +1678,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
}, },
} }
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
Ok(()) Ok(())
} }
@ -1724,18 +2053,6 @@ fn perform_radio_group_validation(elem: &HTMLInputElement, group: Option<&Atom>,
} }
} }
fn update_related_validity_states(elem: &HTMLInputElement, can_gc: CanGc) {
match elem.input_type() {
InputType::Radio => {
perform_radio_group_validation(elem, elem.radio_group_name().as_ref(), can_gc)
},
_ => {
elem.validity_state()
.perform_validation_and_update(ValidationFlags::all(), can_gc);
},
}
}
// https://html.spec.whatwg.org/multipage/#radio-button-group // https://html.spec.whatwg.org/multipage/#radio-button-group
fn in_same_group( fn in_same_group(
other: &HTMLInputElement, other: &HTMLInputElement,
@ -1905,7 +2222,7 @@ impl HTMLInputElement {
InputType::Radio | InputType::Checkbox => { InputType::Radio | InputType::Checkbox => {
self.update_checked_state(self.DefaultChecked(), false); self.update_checked_state(self.DefaultChecked(), false);
self.checked_changed.set(false); self.checked_changed.set(false);
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
}, },
InputType::Image => (), InputType::Image => (),
_ => (), _ => (),
@ -1927,6 +2244,19 @@ impl HTMLInputElement {
el.set_placeholder_shown_state(has_placeholder && !has_value); el.set_placeholder_shown_state(has_placeholder && !has_value);
} }
// Update the placeholder text in the text shadow tree.
// To increase the performance, we would only do this when it is necessary.
fn update_text_shadow_tree_placeholder(&self, can_gc: CanGc) {
if self.input_type() != InputType::Text {
return;
}
self.text_shadow_tree(can_gc)
.placeholder_container
.upcast::<Node>()
.SetTextContent(Some(self.placeholder.borrow().clone()), can_gc);
}
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file) // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file)
// Select files by invoking UI or by passed in argument // Select files by invoking UI or by passed in argument
fn select_files(&self, opt_test_paths: Option<Vec<DOMString>>, can_gc: CanGc) { fn select_files(&self, opt_test_paths: Option<Vec<DOMString>>, can_gc: CanGc) {
@ -1949,7 +2279,8 @@ impl HTMLInputElement {
.collect() .collect()
}); });
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) let (chan, recv) =
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel"); .expect("Error initializing channel");
let msg = let msg =
FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths); FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths);
@ -1977,7 +2308,8 @@ impl HTMLInputElement {
None => None, None => None,
}; };
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) let (chan, recv) =
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel"); .expect("Error initializing channel");
let msg = let msg =
FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path); FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path);
@ -2348,6 +2680,70 @@ impl HTMLInputElement {
}, },
}) })
} }
fn update_related_validity_states(&self, can_gc: CanGc) {
match self.input_type() {
InputType::Radio => {
perform_radio_group_validation(self, self.radio_group_name().as_ref(), can_gc)
},
_ => {
self.validity_state()
.perform_validation_and_update(ValidationFlags::all(), can_gc);
},
}
}
fn value_changed(&self, can_gc: CanGc) {
self.update_related_validity_states(can_gc);
self.update_shadow_tree_if_needed(can_gc);
}
/// <https://html.spec.whatwg.org/multipage/#show-the-picker,-if-applicable>
fn show_the_picker_if_applicable(&self, can_gc: CanGc) {
// FIXME: Implement most of this algorithm
// Step 2. If element is not mutable, then return.
if !self.is_mutable() {
return;
}
// Step 6. Otherwise, the user agent should show the relevant user interface for selecting a value for element,
// in the way it normally would when the user interacts with the control.
if self.input_type() == InputType::Color {
let (ipc_sender, ipc_receiver) =
ipc::channel::<Option<RgbColor>>().expect("Failed to create IPC channel!");
let document = self.owner_document();
let rect = self.upcast::<Node>().bounding_content_box_or_zero(can_gc);
let rect = Rect::new(
Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
);
let current_value = self.Value();
let current_color = RgbColor {
red: u8::from_str_radix(&current_value[1..3], 16).unwrap(),
green: u8::from_str_radix(&current_value[3..5], 16).unwrap(),
blue: u8::from_str_radix(&current_value[5..7], 16).unwrap(),
};
document.send_to_embedder(EmbedderMsg::ShowFormControl(
document.webview_id(),
DeviceIntRect::from_untyped(&rect.to_box2d()),
EmbedderFormControl::ColorPicker(current_color, ipc_sender),
));
let Ok(response) = ipc_receiver.recv() else {
log::error!("Failed to receive response");
return;
};
if let Some(selected_color) = response {
let formatted_color = format!(
"#{:0>2x}{:0>2x}{:0>2x}",
selected_color.red, selected_color.green, selected_color.blue
);
let _ = self.SetValue(formatted_color.into(), can_gc);
}
}
}
} }
impl VirtualMethods for HTMLInputElement { impl VirtualMethods for HTMLInputElement {
@ -2359,6 +2755,7 @@ impl VirtualMethods for HTMLInputElement {
self.super_type() self.super_type()
.unwrap() .unwrap()
.attribute_mutated(attr, mutation, can_gc); .attribute_mutated(attr, mutation, can_gc);
match *attr.local_name() { match *attr.local_name() {
local_name!("disabled") => { local_name!("disabled") => {
let disabled_state = match mutation { let disabled_state = match mutation {
@ -2466,6 +2863,7 @@ impl VirtualMethods for HTMLInputElement {
let mut value = textinput.single_line_content().clone(); let mut value = textinput.single_line_content().clone();
self.sanitize_value(&mut value); self.sanitize_value(&mut value);
textinput.set_content(value); textinput.set_content(value);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
// Steps 7-9 // Steps 7-9
if !previously_selectable && self.selection_api_applies() { if !previously_selectable && self.selection_api_applies() {
@ -2484,8 +2882,11 @@ impl VirtualMethods for HTMLInputElement {
}, },
} }
self.update_text_shadow_tree_placeholder(can_gc);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
}, },
// FIXME(stevennovaryo): This is only reachable by Default and DefaultOn value mode. While others
// are being handled in [Self::SetValue]. Should we merge this two together?
local_name!("value") if !self.value_dirty.get() => { local_name!("value") if !self.value_dirty.get() => {
let value = mutation.new_value(attr).map(|value| (**value).to_owned()); let value = mutation.new_value(attr).map(|value| (**value).to_owned());
let mut value = value.map_or(DOMString::new(), DOMString::from); let mut value = value.map_or(DOMString::new(), DOMString::from);
@ -2493,6 +2894,8 @@ impl VirtualMethods for HTMLInputElement {
self.sanitize_value(&mut value); self.sanitize_value(&mut value);
self.textinput.borrow_mut().set_content(value); self.textinput.borrow_mut().set_content(value);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}, },
local_name!("name") if self.input_type() == InputType::Radio => { local_name!("name") if self.input_type() == InputType::Radio => {
self.radio_group_updated( self.radio_group_updated(
@ -2532,6 +2935,7 @@ impl VirtualMethods for HTMLInputElement {
.extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r')); .extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r'));
} }
} }
self.update_text_shadow_tree_placeholder(can_gc);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
}, },
local_name!("readonly") => { local_name!("readonly") => {
@ -2553,7 +2957,7 @@ impl VirtualMethods for HTMLInputElement {
_ => {}, _ => {},
} }
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
} }
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
@ -2584,7 +2988,8 @@ impl VirtualMethods for HTMLInputElement {
if self.input_type() == InputType::Radio { if self.input_type() == InputType::Radio {
self.radio_group_updated(self.radio_group_name().as_ref()); self.radio_group_updated(self.radio_group_name().as_ref());
} }
update_related_validity_states(self, can_gc);
self.value_changed(can_gc);
} }
fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
@ -2676,6 +3081,17 @@ impl VirtualMethods for HTMLInputElement {
self.implicit_submission(can_gc); self.implicit_submission(can_gc);
}, },
DispatchInput => { DispatchInput => {
if event.IsTrusted() {
self.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
self.value_dirty.set(true); self.value_dirty.set(true);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
@ -2692,17 +3108,9 @@ impl VirtualMethods for HTMLInputElement {
!event.DefaultPrevented() && !event.DefaultPrevented() &&
self.input_type().is_textual_or_password() self.input_type().is_textual_or_password()
{ {
if event.IsTrusted() { // keypress should be deprecated and replaced by beforeinput.
self.owner_global() // keypress was supposed to fire "blur" and "focus" events
.task_manager() // but already done in `document.rs`
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
} else if (event.type_() == atom!("compositionstart") || } else if (event.type_() == atom!("compositionstart") ||
event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionupdate") ||
event.type_() == atom!("compositionend")) && event.type_() == atom!("compositionend")) &&
@ -2730,7 +3138,7 @@ impl VirtualMethods for HTMLInputElement {
} }
} }
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
} }
// https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext // https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext
@ -2752,7 +3160,7 @@ impl VirtualMethods for HTMLInputElement {
elem.textinput elem.textinput
.borrow_mut() .borrow_mut()
.set_content(self.textinput.borrow().get_content()); .set_content(self.textinput.borrow().get_content());
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
} }
} }
@ -2861,7 +3269,8 @@ impl Activatable for HTMLInputElement {
}, },
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):input-activation-behavior // https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):input-activation-behavior
// https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):input-activation-behavior // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):input-activation-behavior
InputType::Checkbox | InputType::Radio => true, // https://html.spec.whatwg.org/multipage/#color-state-(type=color):input-activation-behavior
InputType::Checkbox | InputType::Radio | InputType::Color => true,
_ => false, _ => false,
} }
} }
@ -2907,7 +3316,7 @@ impl Activatable for HTMLInputElement {
}; };
if activation_state.is_some() { if activation_state.is_some() {
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
} }
activation_state activation_state
@ -2966,7 +3375,7 @@ impl Activatable for HTMLInputElement {
_ => (), _ => (),
} }
update_related_validity_states(self, can_gc); self.value_changed(can_gc);
} }
/// <https://html.spec.whatwg.org/multipage/#input-activation-behavior> /// <https://html.spec.whatwg.org/multipage/#input-activation-behavior>
@ -3028,6 +3437,10 @@ impl Activatable for HTMLInputElement {
}, },
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior
InputType::File => self.select_files(None, can_gc), InputType::File => self.select_files(None, can_gc),
// https://html.spec.whatwg.org/multipage/#color-state-(type=color):input-activation-behavior
InputType::Color => {
self.show_the_picker_if_applicable(can_gc);
},
_ => (), _ => (),
} }
} }

View file

@ -5,6 +5,7 @@
use std::borrow::{Borrow, ToOwned}; use std::borrow::{Borrow, ToOwned};
use std::cell::Cell; use std::cell::Cell;
use std::default::Default; use std::default::Default;
use std::str::FromStr;
use base::id::WebViewId; use base::id::WebViewId;
use content_security_policy as csp; use content_security_policy as csp;
@ -12,6 +13,8 @@ use dom_struct::dom_struct;
use embedder_traits::EmbedderMsg; use embedder_traits::EmbedderMsg;
use html5ever::{LocalName, Prefix, local_name, ns}; use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject; use js::rust::HandleObject;
use mime::Mime;
use net_traits::mime_classifier::{MediaType, MimeClassifier};
use net_traits::policy_container::PolicyContainer; use net_traits::policy_container::PolicyContainer;
use net_traits::request::{ use net_traits::request::{
CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder, CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder,
@ -22,7 +25,7 @@ use net_traits::{
ResourceTimingType, ResourceTimingType,
}; };
use servo_arc::Arc; use servo_arc::Arc;
use servo_url::ServoUrl; use servo_url::{ImmutableOrigin, ServoUrl};
use style::attr::AttrValue; use style::attr::AttrValue;
use style::stylesheets::Stylesheet; use style::stylesheets::Stylesheet;
use stylo_atoms::Atom; use stylo_atoms::Atom;
@ -78,6 +81,7 @@ struct LinkProcessingOptions {
policy_container: PolicyContainer, policy_container: PolicyContainer,
source_set: Option<()>, source_set: Option<()>,
base_url: ServoUrl, base_url: ServoUrl,
origin: ImmutableOrigin,
insecure_requests_policy: InsecureRequestsPolicy, insecure_requests_policy: InsecureRequestsPolicy,
has_trustworthy_ancestor_origin: bool, has_trustworthy_ancestor_origin: bool,
// Some fields that we don't need yet are missing // Some fields that we don't need yet are missing
@ -113,6 +117,10 @@ pub(crate) struct HTMLLinkElement {
request_generation_id: Cell<RequestGenerationId>, request_generation_id: Cell<RequestGenerationId>,
/// <https://html.spec.whatwg.org/multipage/#explicitly-enabled> /// <https://html.spec.whatwg.org/multipage/#explicitly-enabled>
is_explicitly_enabled: Cell<bool>, is_explicitly_enabled: Cell<bool>,
/// Whether the previous type matched with the destination
previous_type_matched: Cell<bool>,
/// Whether the previous media environment matched with the media query
previous_media_environment_matched: Cell<bool>,
} }
impl HTMLLinkElement { impl HTMLLinkElement {
@ -133,6 +141,8 @@ impl HTMLLinkElement {
any_failed_load: Cell::new(false), any_failed_load: Cell::new(false),
request_generation_id: Cell::new(RequestGenerationId(0)), request_generation_id: Cell::new(RequestGenerationId(0)),
is_explicitly_enabled: Cell::new(false), is_explicitly_enabled: Cell::new(false),
previous_type_matched: Cell::new(true),
previous_media_environment_matched: Cell::new(true),
} }
} }
@ -236,7 +246,7 @@ impl VirtualMethods for HTMLLinkElement {
return; return;
} }
if !self.upcast::<Node>().is_connected() || is_removal { if !self.upcast::<Node>().is_connected() {
return; return;
} }
match *local_name { match *local_name {
@ -245,6 +255,12 @@ impl VirtualMethods for HTMLLinkElement {
.set(LinkRelations::for_element(self.upcast())); .set(LinkRelations::for_element(self.upcast()));
}, },
local_name!("href") => { local_name!("href") => {
if is_removal {
return;
}
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
// When the href attribute of the link element of an external resource link
// that is already browsing-context connected is changed.
if self.relations.get().contains(LinkRelations::STYLESHEET) { if self.relations.get().contains(LinkRelations::STYLESHEET) {
self.handle_stylesheet_url(&attr.value()); self.handle_stylesheet_url(&attr.value());
} }
@ -254,9 +270,19 @@ impl VirtualMethods for HTMLLinkElement {
self.handle_favicon_url(&attr.value(), &sizes); self.handle_favicon_url(&attr.value(), &sizes);
} }
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
// When the href attribute of the link element of an external resource link
// that is already browsing-context connected is changed.
if self.relations.get().contains(LinkRelations::PREFETCH) { if self.relations.get().contains(LinkRelations::PREFETCH) {
self.fetch_and_process_prefetch_link(&attr.value()); self.fetch_and_process_prefetch_link(&attr.value());
} }
// https://html.spec.whatwg.org/multipage/#link-type-preload
// When the href attribute of the link element of an external resource link
// that is already browsing-context connected is changed.
if self.relations.get().contains(LinkRelations::PRELOAD) {
self.handle_preload_url();
}
}, },
local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => { local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => {
if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) { if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) {
@ -264,9 +290,73 @@ impl VirtualMethods for HTMLLinkElement {
} }
}, },
local_name!("crossorigin") => { local_name!("crossorigin") => {
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
// When the crossorigin attribute of the link element of an external resource link
// that is already browsing-context connected is set, changed, or removed.
if self.relations.get().contains(LinkRelations::PREFETCH) { if self.relations.get().contains(LinkRelations::PREFETCH) {
self.fetch_and_process_prefetch_link(&attr.value()); self.fetch_and_process_prefetch_link(&attr.value());
} }
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
// When the crossorigin attribute of the link element of an external resource link
// that is already browsing-context connected is set, changed, or removed.
if self.relations.get().contains(LinkRelations::STYLESHEET) {
self.handle_stylesheet_url(&attr.value());
}
},
local_name!("as") => {
// https://html.spec.whatwg.org/multipage/#link-type-preload
// When the as attribute of the link element of an external resource link
// that is already browsing-context connected is changed.
if self.relations.get().contains(LinkRelations::PRELOAD) {
if let AttributeMutation::Set(Some(_)) = mutation {
self.handle_preload_url();
}
}
},
local_name!("type") => {
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
// When the type attribute of the link element of an external resource link that
// is already browsing-context connected is set or changed to a value that does
// not or no longer matches the Content-Type metadata of the previous obtained
// external resource, if any.
//
// TODO: Match Content-Type metadata to check if it needs to be updated
if self.relations.get().contains(LinkRelations::STYLESHEET) {
self.handle_stylesheet_url(&attr.value());
}
// https://html.spec.whatwg.org/multipage/#link-type-preload
// When the type attribute of the link element of an external resource link that
// is already browsing-context connected, but was previously not obtained due to
// the type attribute specifying an unsupported type for the request destination,
// is set, removed, or changed.
if self.relations.get().contains(LinkRelations::PRELOAD) &&
!self.previous_type_matched.get()
{
self.handle_preload_url();
}
},
local_name!("media") => {
// https://html.spec.whatwg.org/multipage/#link-type-preload
// When the media attribute of the link element of an external resource link that
// is already browsing-context connected, but was previously not obtained due to
// the media attribute not matching the environment, is changed or removed.
if self.relations.get().contains(LinkRelations::PRELOAD) &&
!self.previous_media_environment_matched.get()
{
match mutation {
AttributeMutation::Removed | AttributeMutation::Set(Some(_)) => {
self.handle_preload_url()
},
_ => {},
};
}
let matches_media_environment =
self.upcast::<Element>().matches_environment(&attr.value());
self.previous_media_environment_matched
.set(matches_media_environment);
}, },
_ => {}, _ => {},
} }
@ -307,6 +397,10 @@ impl VirtualMethods for HTMLLinkElement {
if relations.contains(LinkRelations::PREFETCH) { if relations.contains(LinkRelations::PREFETCH) {
self.fetch_and_process_prefetch_link(&href); self.fetch_and_process_prefetch_link(&href);
} }
if relations.contains(LinkRelations::PRELOAD) {
self.handle_preload_url();
}
} }
} }
} }
@ -325,6 +419,14 @@ impl VirtualMethods for HTMLLinkElement {
} }
impl HTMLLinkElement { impl HTMLLinkElement {
fn compute_destination_for_attribute(&self) -> Destination {
let element = self.upcast::<Element>();
element
.get_attribute(&ns!(), &local_name!("as"))
.map(|attr| translate_a_preload_destination(&attr.value()))
.unwrap_or(Destination::None)
}
/// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element> /// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element>
fn processing_options(&self) -> LinkProcessingOptions { fn processing_options(&self) -> LinkProcessingOptions {
let element = self.upcast::<Element>(); let element = self.upcast::<Element>();
@ -333,10 +435,7 @@ impl HTMLLinkElement {
let document = self.upcast::<Node>().owner_doc(); let document = self.upcast::<Node>().owner_doc();
// Step 2. Let options be a new link processing options // Step 2. Let options be a new link processing options
let destination = element let destination = self.compute_destination_for_attribute();
.get_attribute(&ns!(), &local_name!("as"))
.map(|attr| translate_a_preload_destination(&attr.value()))
.unwrap_or(Destination::None);
let mut options = LinkProcessingOptions { let mut options = LinkProcessingOptions {
href: String::new(), href: String::new(),
@ -348,6 +447,7 @@ impl HTMLLinkElement {
referrer_policy: referrer_policy_for_element(element), referrer_policy: referrer_policy_for_element(element),
policy_container: document.policy_container().to_owned(), policy_container: document.policy_container().to_owned(),
source_set: None, // FIXME source_set: None, // FIXME
origin: document.borrow().origin().immutable().to_owned(),
base_url: document.borrow().base_url(), base_url: document.borrow().base_url(),
insecure_requests_policy: document.insecure_requests_policy(), insecure_requests_policy: document.insecure_requests_policy(),
has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(), has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(),
@ -446,6 +546,10 @@ impl HTMLLinkElement {
None => "", None => "",
}; };
if !element.matches_environment(mq_str) {
return;
}
let media = MediaList::parse_media_list(mq_str, document.window()); let media = MediaList::parse_media_list(mq_str, document.window());
let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity")); let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity"));
@ -458,8 +562,6 @@ impl HTMLLinkElement {
self.request_generation_id self.request_generation_id
.set(self.request_generation_id.get().increment()); .set(self.request_generation_id.get().increment());
// TODO: #8085 - Don't load external stylesheets if the node's mq
// doesn't match.
let loader = StylesheetLoader::for_element(self.upcast()); let loader = StylesheetLoader::for_element(self.upcast());
loader.load( loader.load(
StylesheetContextSource::LinkElement { media: Some(media) }, StylesheetContextSource::LinkElement { media: Some(media) },
@ -494,6 +596,133 @@ impl HTMLLinkElement {
Err(e) => debug!("Parsing url {} failed: {}", href, e), Err(e) => debug!("Parsing url {} failed: {}", href, e),
} }
} }
/// <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
fn handle_preload_url(&self) {
// Step 1. Update the source set for el.
// TODO
// Step 2. Let options be the result of creating link options from el.
let options = self.processing_options();
// Step 3. Preload options, with the following steps given a response response:
// Step 3.1 If response is a network error, fire an event named error at el.
// Otherwise, fire an event named load at el.
self.preload(options);
}
/// <https://html.spec.whatwg.org/multipage/#preload>
fn preload(&self, options: LinkProcessingOptions) {
// Step 1. If options's type doesn't match options's destination, then return.
let type_matches_destination: bool =
HTMLLinkElement::type_matches_destination(&options.link_type, options.destination);
self.previous_type_matched.set(type_matches_destination);
if !type_matches_destination {
return;
}
// Step 2. If options's destination is "image" and options's source set is not null,
// then set options's href to the result of selecting an image source from options's source set.
// TODO
// Step 3. Let request be the result of creating a link request given options.
let url = options.base_url.clone();
let Some(request) = options.create_link_request(self.owner_window().webview_id()) else {
// Step 4. If request is null, then return.
return;
};
let document = self.upcast::<Node>().owner_doc();
// Step 5. Let unsafeEndTime be 0.
// TODO
// Step 6. Let entry be a new preload entry whose integrity metadata is options's integrity.
// TODO
// Step 7. Let key be the result of creating a preload key given request.
// TODO
// Step 8. If options's document is "pending", then set request's initiator type to "early hint".
// TODO
// Step 9. Let controller be null.
// Step 10. Let reportTiming given a Document document be to report timing for controller
// given document's relevant global object.
// Step 11. Set controller to the result of fetching request, with processResponseConsumeBody
// set to the following steps given a response response and null, failure, or a byte sequence bodyBytes:
let fetch_context = PreloadContext {
url,
link: Trusted::new(self),
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
};
document.fetch_background(request.clone(), fetch_context);
}
/// <https://html.spec.whatwg.org/multipage/#match-preload-type>
fn type_matches_destination(mime_type: &str, destination: Option<Destination>) -> bool {
// Step 1. If type is an empty string, then return true.
if mime_type.is_empty() {
return true;
}
// Step 2. If destination is "fetch", then return true.
//
// Fetch is handled as an empty string destination in the spec:
// https://fetch.spec.whatwg.org/#concept-potential-destination-translate
let Some(destination) = destination else {
return false;
};
if destination == Destination::None {
return true;
}
// Step 3. Let mimeTypeRecord be the result of parsing type.
let Ok(mime_type_record) = Mime::from_str(mime_type) else {
// Step 4. If mimeTypeRecord is failure, then return false.
return false;
};
// Step 5. If mimeTypeRecord is not supported by the user agent, then return false.
//
// We currently don't check if we actually support the mime type. Only if we can classify
// it according to the spec.
let Some(mime_type) = MimeClassifier::get_media_type(&mime_type_record) else {
return false;
};
// Step 6. If any of the following are true:
if
// destination is "audio" or "video", and mimeTypeRecord is an audio or video MIME type;
((destination == Destination::Audio || destination == Destination::Video) &&
mime_type == MediaType::AudioVideo)
// destination is a script-like destination and mimeTypeRecord is a JavaScript MIME type;
|| (destination.is_script_like() && mime_type == MediaType::JavaScript)
// destination is "image" and mimeTypeRecord is an image MIME type;
|| (destination == Destination::Image && mime_type == MediaType::Image)
// destination is "font" and mimeTypeRecord is a font MIME type;
|| (destination == Destination::Font && mime_type == MediaType::Font)
// destination is "json" and mimeTypeRecord is a JSON MIME type;
|| (destination == Destination::Json && mime_type == MediaType::Json)
// destination is "style" and mimeTypeRecord's essence is text/css; or
|| (destination == Destination::Style && mime_type_record == mime::TEXT_CSS)
// destination is "track" and mimeTypeRecord's essence is text/vtt,
|| (destination == Destination::Track && mime_type_record.essence_str() == "text/vtt")
{
// then return true.
return true;
}
// Step 7. Return false.
false
}
fn fire_event_after_response(&self, response: Result<ResourceFetchTiming, NetworkError>) {
if response.is_err() {
self.upcast::<EventTarget>()
.fire_event(atom!("error"), CanGc::note());
} else {
// TODO(35035): Figure out why we need to queue a task for the load event. Otherwise
// the performance timing data hasn't been saved yet, which fails several preload
// WPT tests that assume that performance timing information is available when
// the load event is fired.
let this = Trusted::new(self);
self.owner_global()
.task_manager()
.performance_timeline_task_source()
.queue(task!(preload_load_event: move || {
let this = this.root();
this
.upcast::<EventTarget>()
.fire_event(atom!("load"), CanGc::note());
}));
}
}
} }
impl StylesheetOwner for HTMLLinkElement { impl StylesheetOwner for HTMLLinkElement {
@ -552,6 +781,21 @@ impl HTMLLinkElementMethods<crate::DomTypeHolder> for HTMLLinkElement {
.set_tokenlist_attribute(&local_name!("rel"), rel, can_gc); .set_tokenlist_attribute(&local_name!("rel"), rel, can_gc);
} }
// https://html.spec.whatwg.org/multipage/#dom-link-as
make_enumerated_getter!(
As,
"as",
"fetch" | "audio" | "audioworklet" | "document" | "embed" | "font" | "frame"
| "iframe" | "image" | "json" | "manifest" | "object" | "paintworklet"
| "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track"
| "video" | "webidentity" | "worker" | "xslt",
missing => "",
invalid => ""
);
// https://html.spec.whatwg.org/multipage/#dom-link-as
make_setter!(SetAs, "as");
// https://html.spec.whatwg.org/multipage/#dom-link-media // https://html.spec.whatwg.org/multipage/#dom-link-media
make_getter!(Media, "media"); make_getter!(Media, "media");
@ -689,6 +933,8 @@ impl LinkProcessingOptions {
self.has_trustworthy_ancestor_origin, self.has_trustworthy_ancestor_origin,
self.policy_container, self.policy_container,
) )
.initiator(Initiator::Link)
.origin(self.origin)
.integrity_metadata(self.integrity) .integrity_metadata(self.integrity)
.cryptographic_nonce_metadata(self.cryptographic_nonce_metadata) .cryptographic_nonce_metadata(self.cryptographic_nonce_metadata)
.referrer_policy(self.referrer_policy); .referrer_policy(self.referrer_policy);
@ -795,3 +1041,77 @@ impl PreInvoke for PrefetchContext {
true true
} }
} }
struct PreloadContext {
/// The `<link>` element that caused this preload operation
link: Trusted<HTMLLinkElement>,
resource_timing: ResourceFetchTiming,
/// The url being preloaded
url: ServoUrl,
}
impl FetchResponseListener for PreloadContext {
fn process_request_body(&mut self, _: RequestId) {}
fn process_request_eof(&mut self, _: RequestId) {}
fn process_response(
&mut self,
_: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>,
) {
_ = fetch_metadata;
}
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
_ = chunk;
}
/// Step 3.1 of <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
fn process_response_eof(
&mut self,
_: RequestId,
response: Result<ResourceFetchTiming, NetworkError>,
) {
self.link.root().fire_event_after_response(response);
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
submit_timing(self, CanGc::note())
}
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) {
let global = &self.resource_timing_global();
global.report_csp_violations(violations, None);
}
}
impl ResourceTimingListener for PreloadContext {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(
InitiatorType::LocalName(self.url.clone().into_string()),
self.url.clone(),
)
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.link.root().upcast::<Node>().owner_doc().global()
}
}
impl PreInvoke for PreloadContext {
fn should_invoke(&self) -> bool {
// Preload requests are never aborted.
true
}
}

View file

@ -24,10 +24,10 @@ use js::jsapi::JSAutoRealm;
use media::{GLPlayerMsg, GLPlayerMsgForward, WindowGLContext}; use media::{GLPlayerMsg, GLPlayerMsgForward, WindowGLContext};
use net_traits::request::{Destination, RequestId}; use net_traits::request::{Destination, RequestId};
use net_traits::{ use net_traits::{
FetchMetadata, FetchResponseListener, Metadata, NetworkError, ResourceFetchTiming, FetchMetadata, FetchResponseListener, FilteredMetadata, Metadata, NetworkError,
ResourceTimingType, ResourceFetchTiming, ResourceTimingType,
}; };
use pixels::Image; use pixels::RasterImage;
use script_bindings::codegen::GenericBindings::TimeRangesBinding::TimeRangesMethods; use script_bindings::codegen::GenericBindings::TimeRangesBinding::TimeRangesMethods;
use script_bindings::codegen::InheritTypes::{ use script_bindings::codegen::InheritTypes::{
ElementTypeId, HTMLElementTypeId, HTMLMediaElementTypeId, NodeTypeId, ElementTypeId, HTMLElementTypeId, HTMLMediaElementTypeId, NodeTypeId,
@ -186,12 +186,12 @@ impl MediaFrameRenderer {
} }
} }
fn render_poster_frame(&mut self, image: Arc<Image>) { fn render_poster_frame(&mut self, image: Arc<RasterImage>) {
if let Some(image_key) = image.id { if let Some(image_key) = image.id {
self.current_frame = Some(MediaFrame { self.current_frame = Some(MediaFrame {
image_key, image_key,
width: image.width as i32, width: image.metadata.width as i32,
height: image.height as i32, height: image.metadata.height as i32,
}); });
self.show_poster = true; self.show_poster = true;
} }
@ -1358,18 +1358,17 @@ impl HTMLMediaElement {
} }
/// <https://html.spec.whatwg.org/multipage/#poster-frame> /// <https://html.spec.whatwg.org/multipage/#poster-frame>
pub(crate) fn process_poster_image_loaded(&self, image: Arc<Image>) { pub(crate) fn process_poster_image_loaded(&self, image: Arc<RasterImage>) {
if !self.show_poster.get() { if !self.show_poster.get() {
return; return;
} }
// Step 6. // Step 6.
self.handle_resize(Some(image.width), Some(image.height)); self.handle_resize(Some(image.metadata.width), Some(image.metadata.height));
self.video_renderer self.video_renderer
.lock() .lock()
.unwrap() .unwrap()
.render_poster_frame(image); .render_poster_frame(image);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
if pref!(media_testing_enabled) { if pref!(media_testing_enabled) {
self.owner_global() self.owner_global()
@ -1618,7 +1617,6 @@ impl HTMLMediaElement {
// TODO: 6. Abort the overall resource selection algorithm. // TODO: 6. Abort the overall resource selection algorithm.
}, },
PlayerEvent::VideoFrameUpdated => { PlayerEvent::VideoFrameUpdated => {
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
// Check if the frame was resized // Check if the frame was resized
if let Some(frame) = self.video_renderer.lock().unwrap().current_frame { if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
self.handle_resize(Some(frame.width as u32), Some(frame.height as u32)); self.handle_resize(Some(frame.width as u32), Some(frame.height as u32));
@ -2017,12 +2015,12 @@ impl HTMLMediaElement {
pub(crate) fn clear_current_frame_data(&self) { pub(crate) fn clear_current_frame_data(&self) {
self.handle_resize(None, None); self.handle_resize(None, None);
self.video_renderer.lock().unwrap().current_frame = None; self.video_renderer.lock().unwrap().current_frame = None;
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
} }
fn handle_resize(&self, width: Option<u32>, height: Option<u32>) { fn handle_resize(&self, width: Option<u32>, height: Option<u32>) {
if let Some(video_elem) = self.downcast::<HTMLVideoElement>() { if let Some(video_elem) = self.downcast::<HTMLVideoElement>() {
video_elem.resize(width, height); video_elem.resize(width, height);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
} }
} }
@ -2071,6 +2069,28 @@ impl HTMLMediaElement {
} }
} }
} }
/// <https://html.spec.whatwg.org/multipage/#concept-media-load-resource>
pub(crate) fn origin_is_clean(&self) -> bool {
// Step 5.local (media provider object).
if self.src_object.borrow().is_some() {
// The resource described by the current media resource, if any,
// contains the media data. It is CORS-same-origin.
return true;
}
// Step 5.remote (URL record).
if self.resource_url.borrow().is_some() {
// Update the media data with the contents
// of response's unsafe response obtained in this fashion.
// Response can be CORS-same-origin or CORS-cross-origin;
if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() {
return current_fetch_context.origin_is_clean();
}
}
true
}
} }
// XXX Placeholder for [https://github.com/servo/servo/issues/22293] // XXX Placeholder for [https://github.com/servo/servo/issues/22293]
@ -2656,6 +2676,8 @@ pub(crate) struct HTMLMediaElementFetchContext {
cancel_reason: Option<CancelReason>, cancel_reason: Option<CancelReason>,
/// Indicates whether the fetched stream is seekable. /// Indicates whether the fetched stream is seekable.
is_seekable: bool, is_seekable: bool,
/// Indicates whether the fetched stream is origin clean.
origin_clean: bool,
/// Fetch canceller. Allows cancelling the current fetch request by /// Fetch canceller. Allows cancelling the current fetch request by
/// manually calling its .cancel() method or automatically on Drop. /// manually calling its .cancel() method or automatically on Drop.
fetch_canceller: FetchCanceller, fetch_canceller: FetchCanceller,
@ -2666,6 +2688,7 @@ impl HTMLMediaElementFetchContext {
HTMLMediaElementFetchContext { HTMLMediaElementFetchContext {
cancel_reason: None, cancel_reason: None,
is_seekable: false, is_seekable: false,
origin_clean: true,
fetch_canceller: FetchCanceller::new(request_id), fetch_canceller: FetchCanceller::new(request_id),
} }
} }
@ -2678,6 +2701,14 @@ impl HTMLMediaElementFetchContext {
self.is_seekable = seekable; self.is_seekable = seekable;
} }
pub(crate) fn origin_is_clean(&self) -> bool {
self.origin_clean
}
fn set_origin_unclean(&mut self) {
self.origin_clean = false;
}
fn cancel(&mut self, reason: CancelReason) { fn cancel(&mut self, reason: CancelReason) {
if self.cancel_reason.is_some() { if self.cancel_reason.is_some() {
return; return;
@ -2732,6 +2763,16 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
return; return;
} }
if let Ok(FetchMetadata::Filtered {
filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_),
..
}) = metadata
{
if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
current_fetch_context.set_origin_unclean();
}
}
self.metadata = metadata.ok().map(|m| match m { self.metadata = metadata.ok().map(|m| match m {
FetchMetadata::Unfiltered(m) => m, FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_, FetchMetadata::Filtered { unsafe_, .. } => unsafe_,

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