Merge branch 'main' into outreachy-intern

This commit is contained in:
Aniebiet Afia 2025-05-29 12:47:11 +01:00 committed by GitHub
commit 0a91273a97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2099 changed files with 49743 additions and 390343 deletions

18
.flake8
View file

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

1
.github/CODEOWNERS vendored
View file

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

View file

@ -36,6 +36,7 @@ env:
RUST_BACKTRACE: 1
SHELL: /bin/bash
CARGO_INCREMENTAL: 0
BENCHER_PROJECT: ${{ vars.BENCHER_PROJECT || 'servo' }}
jobs:
build:
@ -168,11 +169,17 @@ jobs:
- name: Build for aarch64 HarmonyOS
run: |
./mach build --locked --target aarch64-unknown-linux-ohos --profile=${{ inputs.profile }} --flavor=harmonyos --no-default-features --features tracing,tracing-hitrace
- name: Upload supprt/hitrace-bencher/runs.json
uses: actions/upload-artifact@v4
with:
name: runs.json
path: support/hitrace-bencher/runs.json
overwrite: true
- uses: actions/upload-artifact@v4
with:
# Upload the **unsigned** artifact - We don't have the signing materials in pull request workflows
path: target/openharmony/aarch64-unknown-linux-ohos/${{ inputs.profile }}/entry/build/harmonyos/outputs/default/servoshell-default-unsigned.hap
name: servoshell-hos-${{ inputs.profile }}.hap
path: target/openharmony/aarch64-unknown-linux-ohos/${{ inputs.profile }}/entry/build/harmonyos/outputs/default/servoshell-default-unsigned.hap
test-harmonyos-aarch64:
@ -238,3 +245,19 @@ jobs:
[[ $servo_pid =~ ^[0-9]+$ ]] || { echo "It looks like servo crashed!" ; exit 1; }
# If the grep fails, then the trace output for the "page loaded" prompt is missing
grep 'org\.servo\.servo-.* tracing_mark_write.*PageLoadEndedPrompt' test_output/servo.ftrace
- name: Getting runs file
uses: actions/download-artifact@v4
with:
# Name of the artifact to download.
# If unspecified, all artifacts for the run are downloaded.
name: runs.json
- name: "Run benchmark"
run: hitrace-bench -r runs.json
- name: Getting bencher
uses: bencherdev/bencher@main
- name: Getting model name
run: |
echo "MODEL_NAME=$(hdc bugreport | head -n 20 | grep MarketName | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}' -)" >> $GITHUB_ENV
- name: Uploading to bencher.dev
run: |
bencher run --adapter json --file bench.json --project '${{ env.BENCHER_PROJECT }}' --token '${{ secrets.BENCHER_API_TOKEN }}' --github-actions '${{ secrets.GITHUB_TOKEN }}' --testbed="$MODEL_NAME"

View file

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

616
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ publish = false
rust-version = "1.85.0"
[workspace.dependencies]
accountable-refcell = "0.2.0"
accountable-refcell = "0.2.2"
aes = "0.8.4"
aes-gcm = "0.10.3"
aes-kw = { version = "0.2.1", features = ["alloc"] }
@ -70,7 +70,7 @@ gstreamer-sys = "0.23"
gstreamer-video = "0.23"
harfbuzz-sys = "0.6.1"
headers = "0.4"
hitrace = "0.1.4"
hitrace = "0.1.5"
html5ever = "0.31"
http = "1.3"
http-body-util = "0.1"
@ -113,6 +113,7 @@ rand_isaac = "0.3"
raw-window-handle = "0.6"
rayon = "1"
regex = "1.11"
resvg = "0.45.0"
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }
rustls-pemfile = "2.0"
rustls-pki-types = "1.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
64-bit macOS, 64-bit Linux, 64-bit Windows, 64-bit OpenHarmony, and Android.
Servo welcomes contribution from everyone. Check out [The Servo Book](https://book.servo.org) to get started, or go to [servo.org](https://servo.org/) for news and guides.
Servo welcomes contribution from everyone. Check out:
- The [Servo Book](https://book.servo.org) for documentation
- [servo.org](https://servo.org/) for news and guides
Coordination of Servo development happens:
- Here in the Github Issues
- On the [Servo Zulip](https://servo.zulipchat.com/)
- In video calls advertised in the [Servo Project](https://github.com/servo/project/issues) repo.
## Getting started

View file

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

View file

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

View file

@ -34,7 +34,6 @@ log = { workspace = true }
net = { path = "../net" }
pixels = { path = "../pixels" }
profile_traits = { workspace = true }
script_traits = { workspace = true }
servo_allocator = { path = "../allocator" }
servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" }

View file

@ -7,7 +7,6 @@ use std::collections::HashMap;
use std::env;
use std::fs::create_dir_all;
use std::iter::once;
use std::mem::take;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@ -33,7 +32,7 @@ use fnv::FnvHashMap;
use ipc_channel::ipc::{self, IpcSharedMemory};
use libc::c_void;
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::time::{self as profile_time, ProfilerCategory};
use profile_traits::{path, time_profile};
@ -55,7 +54,7 @@ use webrender_api::{
use crate::InitialCompositorState;
use crate::webview_manager::WebViewManager;
use crate::webview_renderer::{UnknownWebView, WebViewRenderer};
use crate::webview_renderer::{PinchZoomResult, UnknownWebView, WebViewRenderer};
#[derive(Debug, PartialEq)]
enum UnableToComposite {
@ -621,29 +620,37 @@ impl IOCompositor {
}
},
CompositorMsg::WebDriverMouseButtonEvent(webview_id, action, button, x, y) => {
CompositorMsg::WebDriverMouseButtonEvent(
webview_id,
action,
button,
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::MouseButton(MouseButtonEvent {
point,
action,
button,
}));
webview_renderer.dispatch_input_event(
InputEvent::MouseButton(MouseButtonEvent::new(action, button, point))
.with_webdriver_message_id(Some(message_id)),
);
},
CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y) => {
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 { point }));
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) => {
@ -1424,7 +1431,7 @@ impl IOCompositor {
&mut self,
webview_id: WebViewId,
page_rect: Option<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, UnableToComposite> {
) -> Result<Option<RasterImage>, UnableToComposite> {
self.render_inner()?;
let size = self.rendering_context.size2d().to_i32();
@ -1451,16 +1458,19 @@ impl IOCompositor {
Ok(self
.rendering_context
.read_to_image(rect)
.map(|image| Image {
width: image.width(),
height: image.height(),
.map(|image| RasterImage {
metadata: ImageMetadata {
width: image.width(),
height: image.height(),
},
format: PixelFormat::RGBA8,
frames: vec![ImageFrame {
delay: None,
bytes: ipc::IpcSharedMemory::from_bytes(&image),
byte_range: 0..image.len(),
width: image.width(),
height: image.height(),
}],
bytes: ipc::IpcSharedMemory::from_bytes(&image),
id: None,
cors_status: CorsStatus::Safe,
}))
@ -1668,11 +1678,39 @@ impl IOCompositor {
if let Err(err) = self.rendering_context.make_current() {
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() {
webview_renderer.process_pending_scroll_events(self);
let mut need_zoom = false;
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);
}
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.webview_renderers = webview_renderers;
self.global.borrow().shutdown_state() != ShutdownState::FinishedShuttingDown
}

View file

@ -20,15 +20,11 @@ use fnv::FnvHashSet;
use log::{debug, warn};
use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor};
use webrender::Transaction;
use webrender_api::units::{
DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D,
};
use webrender_api::{
ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation,
};
use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation};
use crate::IOCompositor;
use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
@ -55,6 +51,19 @@ enum ScrollZoomEvent {
Scroll(ScrollEvent),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ScrollResult {
pub pipeline_id: PipelineId,
pub external_scroll_id: ExternalScrollId,
pub offset: LayoutVector2D,
}
#[derive(PartialEq)]
pub(crate) enum PinchZoomResult {
DidPinchZoom,
DidNotPinchZoom,
}
/// A renderer for a libservo `WebView`. This is essentially the [`ServoRenderer`]'s interface to a
/// libservo `WebView`, but the code here cannot depend on libservo in order to prevent circular
/// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself.
@ -678,17 +687,17 @@ impl WebViewRenderer {
/// <http://w3c.github.io/touch-events/#mouse-events>
fn simulate_mouse_click(&mut self, point: DevicePoint) {
let button = MouseButton::Left;
self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point)));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Down,
button,
action: MouseButtonAction::Down,
point,
}));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent {
)));
self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Up,
button,
action: MouseButtonAction::Up,
point,
}));
)));
}
pub(crate) fn notify_scroll_event(
@ -737,9 +746,17 @@ impl WebViewRenderer {
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() {
return;
return (PinchZoomResult::DidNotPinchZoom, None);
}
// 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| {
self.scroll_node_at_device_point(
combined_event.cursor.to_f32(),
combined_event.scroll_location,
)
});
if !zoom_changed && scroll_result.is_none() {
return;
if let Some(scroll_result) = scroll_result {
self.send_scroll_positions_to_layout_for_pipeline(scroll_result.pipeline_id);
}
let mut transaction = Transaction::new();
if zoom_changed {
compositor.send_root_pipeline_display_list_in_transaction(&mut transaction);
}
let pinch_zoom_result = match self
.set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification)
{
true => PinchZoomResult::DidPinchZoom,
false => PinchZoomResult::DidNotPinchZoom,
};
if let Some((pipeline_id, external_id, offset)) = scroll_result {
let offset = LayoutVector2D::new(-offset.x, -offset.y);
transaction.set_scroll_offsets(
external_id,
vec![SampledScrollOffset {
offset,
generation: 0,
}],
);
self.send_scroll_positions_to_layout_for_pipeline(pipeline_id);
}
compositor.generate_frame(&mut transaction, RenderReasons::APZ);
self.global.borrow_mut().send_transaction(transaction);
(pinch_zoom_result, scroll_result)
}
/// Perform a hit test at the given [`DevicePoint`] and apply the [`ScrollLocation`]
@ -831,7 +835,7 @@ impl WebViewRenderer {
&mut self,
cursor: DevicePoint,
scroll_location: ScrollLocation,
) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> {
) -> Option<ScrollResult> {
let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => {
let device_pixels_per_page = self.device_pixels_per_page_pixel();
@ -871,8 +875,12 @@ impl WebViewRenderer {
let scroll_result = pipeline_details
.scroll_tree
.scroll_node_or_ancestor(scroll_tree_node, scroll_location);
if let Some((external_id, offset)) = scroll_result {
return Some((*pipeline_id, external_id, offset));
if let Some((external_scroll_id, offset)) = scroll_result {
return Some(ScrollResult {
pipeline_id: *pipeline_id,
external_scroll_id,
offset,
});
}
}
}

View file

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

View file

@ -99,7 +99,6 @@ pub struct Preferences {
pub dom_serviceworker_timeout_seconds: i64,
pub dom_servo_helpers_enabled: bool,
pub dom_servoparser_async_html_tokenizer_enabled: bool,
pub dom_shadowdom_enabled: bool,
pub dom_svg_enabled: bool,
pub dom_testable_crash_enabled: bool,
pub dom_testbinding_enabled: bool,
@ -117,10 +116,6 @@ pub struct Preferences {
// https://testutils.spec.whatwg.org#availability
pub dom_testutils_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,
/// Enable WebGL2 APIs.
pub dom_webgl2_enabled: bool,
@ -277,7 +272,6 @@ impl Preferences {
dom_serviceworker_timeout_seconds: 60,
dom_servo_helpers_enabled: false,
dom_servoparser_async_html_tokenizer_enabled: false,
dom_shadowdom_enabled: true,
dom_svg_enabled: false,
dom_testable_crash_enabled: false,
dom_testbinding_enabled: false,
@ -294,7 +288,6 @@ impl Preferences {
dom_testperf_enabled: false,
dom_testutils_enabled: false,
dom_trusted_types_enabled: false,
dom_urlpattern_enabled: false,
dom_webgl2_enabled: false,
dom_webgpu_enabled: false,
dom_webgpu_wgpu_backend: String::new(),

View file

@ -132,7 +132,7 @@ use embedder_traits::{
FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError,
JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState,
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
WebDriverLoadStatus,
WebDriverCommandResponse, WebDriverLoadStatus,
};
use euclid::Size2D;
use euclid::default::Size2D as UntypedSize2D;
@ -172,6 +172,7 @@ use crate::browsingcontext::{
AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator,
NewBrowsingContextInfo,
};
use crate::constellation_webview::ConstellationWebView;
use crate::event_loop::EventLoop;
use crate::pipeline::{InitialPipelineState, Pipeline};
use crate::process_manager::ProcessManager;
@ -229,18 +230,6 @@ struct WebrenderWGPU {
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.
///
/// <https://html.spec.whatwg.org/multipage/#browsing-context-group>
@ -324,7 +313,7 @@ pub struct Constellation<STF, SWF> {
compositor_proxy: CompositorProxy,
/// Bookkeeping data for all webviews in the constellation.
webviews: WebViewManager<WebView>,
webviews: WebViewManager<ConstellationWebView>,
/// Channels for the constellation to send messages to the public
/// resource-related threads. There are two groups of resource threads: one
@ -532,6 +521,8 @@ pub struct InitialConstellationState {
struct WebDriverData {
load_channel: Option<(PipelineId, IpcSender<WebDriverLoadStatus>)>,
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 {
@ -539,6 +530,7 @@ impl WebDriverData {
WebDriverData {
load_channel: None,
resize_channel: None,
input_command_response_sender: None,
}
}
}
@ -892,6 +884,16 @@ where
if self.shutting_down {
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!(
"{}: Creating new pipeline in {}",
pipeline_id, browsing_context_id
@ -970,6 +972,7 @@ where
time_profiler_chan: self.time_profiler_chan.clone(),
mem_profiler_chan: self.mem_profiler_chan.clone(),
viewport_details: initial_viewport_details,
theme,
event_loop,
load_data,
prev_throttled: throttled,
@ -1433,8 +1436,8 @@ where
size_type,
);
},
EmbedderToConstellationMessage::ThemeChange(theme) => {
self.handle_theme_change(theme);
EmbedderToConstellationMessage::ThemeChange(webview_id, theme) => {
self.handle_theme_change(webview_id, theme);
},
EmbedderToConstellationMessage::TickAnimation(webview_ids) => {
self.handle_tick_animation(webview_ids)
@ -1867,6 +1870,18 @@ where
ScriptToConstellationMessage::FinishJavaScriptEvaluation(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");
}
},
}
}
@ -3127,13 +3142,8 @@ where
// Register this new top-level browsing context id as a webview and set
// its focused browsing context to be itself.
self.webviews.add(
webview_id,
WebView {
focused_browsing_context_id: browsing_context_id,
session_history: JointSessionHistory::new(),
},
);
self.webviews
.add(webview_id, ConstellationWebView::new(browsing_context_id));
// https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group
let mut new_bc_group: BrowsingContextGroup = Default::default();
@ -3539,10 +3549,7 @@ where
self.pipelines.insert(new_pipeline_id, pipeline);
self.webviews.add(
new_webview_id,
WebView {
focused_browsing_context_id: new_browsing_context_id,
session_history: JointSessionHistory::new(),
},
ConstellationWebView::new(new_browsing_context_id),
);
// https://html.spec.whatwg.org/multipage/#bcg-append
@ -4836,7 +4843,11 @@ where
mouse_button,
x,
y,
msg_id,
response_sender,
) => {
self.webdriver.input_command_response_sender = Some(response_sender);
self.compositor_proxy
.send(CompositorMsg::WebDriverMouseButtonEvent(
webview_id,
@ -4844,11 +4855,16 @@ where
mouse_button,
x,
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
.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) => {
self.compositor_proxy
@ -5599,18 +5615,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(
feature = "tracing",
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() {
let msg = ScriptThreadMessage::ThemeChange(pipeline.id, theme);
if let Err(err) = pipeline.event_loop.send(msg) {
if pipeline.webview_id != webview_id {
continue;
}
if let Err(error) = pipeline
.event_loop
.send(ScriptThreadMessage::ThemeChange(pipeline.id, theme))
{
warn!(
"{}: Failed to send theme change event to pipeline ({:?}).",
pipeline.id, err
"{}: Failed to send theme change event to pipeline ({error:?}).",
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 constellation;
mod constellation_webview;
mod event_loop;
mod logging;
mod pipeline;

View file

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

View file

@ -177,6 +177,7 @@ mod from_script {
Self::TitleChanged(..) => target!("TitleChanged"),
Self::IFrameSizes(..) => target!("IFrameSizes"),
Self::ReportMemory(..) => target!("ReportMemory"),
Self::WebDriverInputComplete(..) => target!("WebDriverInputComplete"),
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
}
}
@ -239,7 +240,7 @@ mod from_script {
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
Self::ShowNotification(..) => target_variant!("ShowNotification"),
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
Self::ShowFormControl(..) => target_variant!("ShowFormControl"),
Self::FinishJavaScriptEvaluation(..) => {
target_variant!("FinishJavaScriptEvaluation")
},

View file

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

View file

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

View file

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

View file

@ -21,7 +21,6 @@ app_units = { workspace = true }
atomic_refcell = { workspace = true }
base = { workspace = true }
bitflags = { workspace = true }
canvas_traits = { workspace = true }
compositing_traits = { workspace = true }
constellation_traits = { 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(
self.context,
self.propagated_data,
true, /* has_first_formatted_line */
false, /* is_single_line_text_box */
self.info.style.writing_mode.to_bidi_level(),

View file

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

View file

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

View file

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

View file

@ -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;
fn to_webrender(&self) -> Self::Type {
match *self {
ComputedTextDecorationStyle::Solid => LineStyle::Solid,
ComputedTextDecorationStyle::Solid | ComputedTextDecorationStyle::Double => {
LineStyle::Solid
},
ComputedTextDecorationStyle::Dotted => LineStyle::Dotted,
ComputedTextDecorationStyle::Dashed => LineStyle::Dashed,
ComputedTextDecorationStyle::Wavy => LineStyle::Wavy,
_ => LineStyle::Solid,
ComputedTextDecorationStyle::MozNone => {
unreachable!("Should never try to draw a moz-none text decoration")
},
}
}
}

View file

@ -8,18 +8,23 @@ use std::sync::Arc;
use app_units::{AU_PER_PX, Au};
use base::WebRenderEpochToU16;
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 euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit};
use euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit, Vector2D};
use fonts::GlyphStore;
use gradient::WebRenderGradient;
use net_traits::image_cache::Image as CachedImage;
use range::Range as ServoRange;
use servo_arc::Arc as ServoArc;
use servo_config::opts::DebugOptions;
use servo_geometry::MaxRect;
use style::Zero;
use style::color::{AbsoluteColor, ColorSpace};
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::properties::ComputedValues;
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::ui::CursorKind;
use style_traits::CSSPixel;
use webrender_api::units::{DevicePixel, LayoutPixel, LayoutRect, LayoutSize};
use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize};
use webrender_api::{
self as wr, BorderDetails, BoxShadowClipMode, ClipChainId, CommonItemProperties,
ImageRendering, NinePatchBorder, NinePatchBorderSource, SpatialId, units,
self as wr, BorderDetails, BoxShadowClipMode, BuiltDisplayList, ClipChainId, ClipMode,
CommonItemProperties, ComplexClipRegion, ImageRendering, NinePatchBorder,
NinePatchBorderSource, PropertyBinding, SpatialId, SpatialTreeItemKey, units,
};
use wr::units::LayoutVector2D;
@ -55,7 +61,7 @@ use crate::replaced::NaturalSizes;
use crate::style_ext::{BorderStyleColor, ComputedValuesExt};
mod background;
mod clip_path;
mod clip;
mod conversions;
mod gradient;
mod stacking_context;
@ -63,79 +69,11 @@ mod stacking_context;
use background::BackgroundPainter;
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.
type ItemTag = (u64, u16);
type HitInfo = Option<ItemTag>;
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> {
/// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This
/// 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.
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
/// [stacking_context::StackingContextContent::Fragment] as an argument to display
/// list building functions.
current_clip_chain_id: ClipChainId,
current_clip_id: ClipId,
/// A [LayoutContext] used to get information about the device pixel ratio
/// and get handles to WebRender images.
pub context: &'a LayoutContext<'a>,
/// The [DisplayList] used to collect display list items and metadata.
pub display_list: &'a mut DisplayList,
/// The [`wr::DisplayListBuilder`] for this Servo [`DisplayListBuilder`].
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.
///
@ -171,6 +112,10 @@ pub(crate) struct DisplayListBuilder<'a> {
/// element inherits the `<body>`'s background to paint the page canvas background.
/// See <https://drafts.csswg.org/css-backgrounds/#body-background>.
paint_body_background: bool,
/// A mapping from [`ClipId`] To WebRender [`ClipChainId`] used when building this WebRender
/// display list.
clip_map: Vec<ClipChainId>,
}
struct InspectorHighlight {
@ -207,45 +152,224 @@ impl InspectorHighlight {
}
}
impl DisplayList {
pub fn build(
&mut self,
impl DisplayListBuilder<'_> {
pub(crate) fn build(
context: &LayoutContext,
stacking_context_tree: &mut StackingContextTree,
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")]
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 {
current_scroll_node_id: self.compositor_info.root_reference_frame_id,
current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id,
current_clip_chain_id: ClipChainId::INVALID,
current_scroll_node_id: compositor_info.root_reference_frame_id,
current_reference_frame_scroll_node_id: compositor_info.root_reference_frame_id,
current_clip_id: ClipId::INVALID,
context,
display_list: self,
webrender_display_list_builder: &mut webrender_display_list_builder,
compositor_info,
inspector_highlight: context
.highlighted_dom_node
.map(InspectorHighlight::for_node),
paint_body_background: true,
clip_map: Default::default(),
};
fragment_tree.build_display_list(&mut builder, root_stacking_context);
if let Some(highlight) = builder
.inspector_highlight
.take()
.and_then(|highlight| highlight.state)
{
builder.paint_dom_inspector_highlight(highlight);
builder.add_all_spatial_nodes();
for clip in stacking_context_tree.clip_store.0.iter() {
builder.add_clip_to_display_list(clip);
}
}
}
impl DisplayListBuilder<'_> {
// 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
}
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) {
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(
@ -258,8 +382,8 @@ impl DisplayListBuilder<'_> {
// for fragments that paint their entire border rectangle.
wr::CommonItemProperties {
clip_rect,
spatial_id: self.current_scroll_node_id.spatial_id,
clip_chain_id: self.current_clip_chain_id,
spatial_id: self.spatial_id(self.current_scroll_node_id),
clip_chain_id: self.clip_chain_id(self.current_clip_id),
flags: style.get_webrender_primitive_flags(),
}
}
@ -277,19 +401,24 @@ impl DisplayListBuilder<'_> {
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,
Some(cursor(inherited_ui.cursor.keyword, auto_cursor)),
self.current_scroll_node_id,
);
Some((
hit_test_index as u64,
self.display_list.compositor_info.epoch.as_u16(),
))
Some((hit_test_index as u64, self.compositor_info.epoch.as_u16()))
}
/// 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 {
r: 0.23,
g: 0.7,
@ -327,8 +456,7 @@ impl DisplayListBuilder<'_> {
flags: wr::PrimitiveFlags::default(),
};
self.display_list
.wr
self.wr()
.push_rect(&properties, content_box, CONTENT_BOX_HIGHLIGHT_COLOR);
// Highlight margin, border and padding
@ -441,13 +569,16 @@ impl Fragment {
section: StackingContextSection,
is_hit_test_for_scrollable_overflow: 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 self.tag() == Some(inspector_highlight.tag) {
inspector_highlight.register_fragment_of_highlighted_dom_node(
self,
builder.current_scroll_node_id.spatial_id,
builder.current_clip_chain_id,
spatial_id,
clip_chain_id,
containing_block,
);
}
@ -550,9 +681,12 @@ impl Fragment {
.get_inherited_box()
.visibility
{
Visibility::Visible => {
self.build_display_list_for_text_fragment(text, builder, containing_block)
},
Visibility::Visible => self.build_display_list_for_text_fragment(
text,
builder,
containing_block,
text_decorations,
),
Visibility::Hidden => (),
Visibility::Collapse => (),
}
@ -574,8 +708,8 @@ impl Fragment {
None => return,
};
let clip_chain_id = builder.current_clip_chain_id;
let spatial_id = builder.current_scroll_node_id.spatial_id;
let clip_chain_id = builder.clip_chain_id(builder.current_clip_id);
let spatial_id = builder.spatial_id(builder.current_scroll_node_id);
builder.wr().push_hit_test(
rect.to_webrender(),
clip_chain_id,
@ -590,6 +724,7 @@ impl Fragment {
fragment: &TextFragment,
builder: &mut DisplayListBuilder,
containing_block: &PhysicalRect<Au>,
text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) {
// NB: The order of painting text components (CSS Text Decoration Module Level 3) is:
// 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 mut baseline_origin = rect.origin;
baseline_origin.y += fragment.font_metrics.ascent;
let glyphs = glyphs(
&fragment.glyphs,
baseline_origin,
@ -641,23 +777,36 @@ impl Fragment {
);
}
if fragment
.text_decoration_line
.contains(TextDecorationLine::UNDERLINE)
{
let mut rect = rect;
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));
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color);
for text_decoration in text_decorations.iter() {
if text_decoration.line.contains(TextDecorationLine::UNDERLINE) {
let mut rect = rect;
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));
self.build_display_list_for_text_decoration(
&parent_style,
builder,
&rect,
text_decoration,
TextDecorationLine::UNDERLINE,
);
}
}
if fragment
.text_decoration_line
.contains(TextDecorationLine::OVERLINE)
{
let mut rect = rect;
rect.size.height = Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx));
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color);
for text_decoration in text_decorations.iter() {
if text_decoration.line.contains(TextDecorationLine::OVERLINE) {
let mut rect = rect;
rect.size.height =
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
@ -734,14 +883,23 @@ impl Fragment {
None,
);
if fragment
.text_decoration_line
.contains(TextDecorationLine::LINE_THROUGH)
{
let mut rect = rect;
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));
self.build_display_list_for_text_decoration(&parent_style, builder, &rect, &color);
for text_decoration in text_decorations.iter() {
if text_decoration
.line
.contains(TextDecorationLine::LINE_THROUGH)
{
let mut rect = rect;
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));
self.build_display_list_for_text_decoration(
&parent_style,
builder,
&rect,
text_decoration,
TextDecorationLine::LINE_THROUGH,
);
}
}
if !shadows.0.is_empty() {
@ -754,26 +912,47 @@ impl Fragment {
parent_style: &ServoArc<ComputedValues>,
builder: &mut DisplayListBuilder,
rect: &PhysicalRect<Au>,
color: &AbsoluteColor,
text_decoration: &FragmentTextDecoration,
line: TextDecorationLine,
) {
let rect = rect.to_webrender();
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 {
if text_decoration.style == ComputedTextDecorationStyle::MozNone {
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,
wavy_line_thickness,
line_thickness,
wr::LineOrientation::Horizontal,
&rgba(text_decoration_color),
text_decoration_style.to_webrender(),
&rgba(text_decoration.color),
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);
}
let maybe_clip = create_clip_chain(
self.border_radius,
self.border_rect,
builder,
force_clip_creation,
);
let maybe_clip =
builder.maybe_create_clip(self.border_radius, self.border_rect, force_clip_creation);
*self.border_edge_clip_chain_id.borrow_mut() = 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 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;
maybe_clip
}
@ -918,7 +1093,7 @@ impl<'a> BuilderForBoxFragment<'a> {
(self.fragment.border + self.fragment.padding).to_webrender(),
);
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;
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
let dppx = 1.0;
let intrinsic = NaturalSizes::from_width_and_height(
image_info.size.width as f32 / dppx,
image_info.size.height as f32 / dppx,
);
let Some(image_key) = image_info.key else {
let intrinsic =
NaturalSizes::from_width_and_height(size.width / dppx, size.height / dppx);
let layer = background::layout_layer(self, painter, builder, index, intrinsic);
let image_wr_key = match image {
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;
};
if let Some(layer) =
background::layout_layer(self, painter, builder, index, intrinsic)
{
if let Some(layer) = layer {
if layer.repeat {
builder.wr().push_repeating_image(
&layer.common,
@ -1289,13 +1485,17 @@ impl<'a> BuilderForBoxFragment<'a> {
.resolve_image(node, &border.border_image_source)
{
Err(_) => return false,
Ok(ResolvedImage::Image(image_info)) => {
let Some(key) = image_info.key else {
Ok(ResolvedImage::Image { image, size }) => {
let Some(image) = image.as_raster_image() else {
return false;
};
width = image_info.size.width as f32;
height = image_info.size.height as f32;
let Some(key) = image.id else {
return false;
};
width = size.width;
height = size.height;
NinePatchBorderSource::Image(key, ImageRendering::Auto)
},
Ok(ResolvedImage::Gradient(gradient)) => {
@ -1553,38 +1753,6 @@ fn offset_radii(mut radii: wr::BorderRadius, offset: f32) -> wr::BorderRadius {
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.
fn resolve_border_image_outset(
outset: BorderImageOutset,

File diff suppressed because it is too large Load diff

View file

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

View file

@ -201,17 +201,7 @@ fn traverse_children_of<'dom>(
) {
traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler);
let is_text_input_element = matches!(
parent_element.type_id(),
LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
);
let is_textarea_element = matches!(
parent_element.type_id(),
LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement)
);
if is_text_input_element || is_textarea_element {
if parent_element.is_text_input() {
let info = NodeAndStyleInfo::new(
parent_element,
parent_element.style(context.shared_context()),
@ -229,9 +219,7 @@ fn traverse_children_of<'dom>(
} else {
handler.handle_text(&info, node_text_content);
}
}
if !is_text_input_element && !is_textarea_element {
} else {
for child in iter_child_nodes(parent_element) {
if child.is_text_node() {
let info = NodeAndStyleInfo::new(child, child.style(context.shared_context()));

View file

@ -32,7 +32,7 @@ use crate::formatting_contexts::{Baselines, IndependentFormattingContextContents
use crate::fragment_tree::{
BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
};
use crate::geom::{AuOrAuto, LogicalRect, LogicalSides, LogicalVec2, Size, Sizes};
use crate::geom::{AuOrAuto, LazySize, LogicalRect, LogicalSides, LogicalVec2, Size, Sizes};
use crate::layout_box_base::CacheableLayoutResult;
use crate::positioned::{
AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement,
@ -417,9 +417,8 @@ struct DesiredFlexFractionAndGrowOrShrinkFactor {
#[derive(Default)]
struct FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size: Au,
content_min_main_size: Au,
content_max_main_size: Option<Au>,
pbm_auto_is_zero: FlexRelativeVec2<Au>,
outer_min_main_size: Au,
outer_max_main_size: Option<Au>,
min_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor,
max_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor,
min_content_main_size_for_multiline_container: Au,
@ -583,9 +582,8 @@ impl FlexContainer {
for FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size,
content_min_main_size,
content_max_main_size,
pbm_auto_is_zero,
outer_min_main_size,
outer_max_main_size,
min_flex_factors,
max_flex_factors,
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
// > factor, if shrinking) and the chosen flex fraction, then clamp that result by the max 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
// > afore-calculated sizes of all items within a single line.
container_max_content_size += (*outer_flex_base_size +
Au::from_f32_px(
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
// > identically to the max-content main size, except that the flex items
@ -621,7 +616,7 @@ impl FlexContainer {
Au::from_f32_px(
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 {
container_min_content_size
.max_assign(*min_content_main_size_for_multiline_container);
@ -655,6 +650,7 @@ impl FlexContainer {
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
depends_on_block_constraints: bool,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
let depends_on_block_constraints =
depends_on_block_constraints || self.config.flex_direction == FlexDirection::Column;
@ -676,14 +672,13 @@ impl FlexContainer {
// https://drafts.csswg.org/css-flexbox/#algo-main-container
let container_main_size = match self.config.flex_axis {
FlexAxis::Row => containing_block.size.inline,
FlexAxis::Column => match containing_block.size.block {
SizeConstraint::Definite(size) => size,
SizeConstraint::MinMax(min, max) => self
.main_content_sizes(layout_context, &containing_block.into(), || &flex_context)
FlexAxis::Column => lazy_block_size.resolve(|| {
let mut containing_block = IndefiniteContainingBlock::from(containing_block);
containing_block.size.block = None;
self.main_content_sizes(layout_context, &containing_block, || &flex_context)
.sizes
.max_content
.clamp_between_extremums(min, max),
},
}),
};
// Actual length may be less, but we guess that usually not by a lot
@ -766,30 +761,23 @@ impl FlexContainer {
.map(|layout| layout.line_size.cross)
.sum::<Au>() +
cross_gap * (line_count as i32 - 1);
let content_block_size = match self.config.flex_axis {
FlexAxis::Row => content_cross_size,
FlexAxis::Column => container_main_size,
};
// https://drafts.csswg.org/css-flexbox/#algo-cross-container
let container_cross_size = match flex_context.container_inner_size_constraint.cross {
SizeConstraint::Definite(cross_size) => cross_size,
SizeConstraint::MinMax(min, max) => {
content_cross_size.clamp_between_extremums(min, max)
},
let container_cross_size = match self.config.flex_axis {
FlexAxis::Row => lazy_block_size.resolve(|| content_cross_size),
FlexAxis::Column => containing_block.size.inline,
};
let container_size = FlexRelativeVec2 {
main: container_main_size,
cross: container_cross_size,
};
let content_block_size = flex_context
.config
.flex_axis
.vec2_to_flow_relative(container_size)
.block;
let mut remaining_free_cross_space =
match flex_context.container_inner_size_constraint.cross {
SizeConstraint::Definite(cross_size) => cross_size - content_cross_size,
_ => Au::zero(),
};
let mut remaining_free_cross_space = container_cross_size - content_cross_size;
// Implement fallback alignment.
//
@ -1936,6 +1924,29 @@ impl FlexItem<'_> {
}
}
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
// 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.
let stretch_size = containing_block
.size
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
LazySize::new(
&self.content_cross_sizes,
Direction::Block,
Size::FitContent,
Au::zero,
stretch_size,
self.is_table(),
)
};
let layout = non_replaced.layout(
flex_context.layout_context,
&mut positioning_context,
@ -1945,6 +1956,7 @@ impl FlexItem<'_> {
flex_axis == FlexAxis::Column ||
self.cross_size_stretches_to_line ||
self.depends_on_block_constraints,
&lazy_block_size,
);
let CacheableLayoutResult {
fragments,
@ -1962,22 +1974,7 @@ impl FlexItem<'_> {
});
let hypothetical_cross_size = if cross_axis_is_item_block_axis {
// 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.
// To be discussed in https://github.com/w3c/csswg-drafts/issues/11784.
let stretch_size = containing_block
.size
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
self.content_cross_sizes.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
stretch_size,
|| content_block_size.into(),
self.is_table(),
)
lazy_block_size.resolve(|| content_block_size)
} else {
inline_size
};
@ -2456,6 +2453,8 @@ impl FlexItemBox {
};
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(
content_contribution_sizes.max_content,
flex_base_size,
@ -2481,20 +2480,19 @@ impl FlexItemBox {
content_contribution_sizes.min_content;
let style_position = &self.style().get_position();
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() {
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
.clamp_between_extremums(content_min_main_size, content_max_main_size);
.clamp_between_extremums(outer_min_main_size, outer_max_main_size);
FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size,
content_min_main_size,
content_max_main_size,
pbm_auto_is_zero,
outer_min_main_size,
outer_max_main_size,
min_flex_factors,
max_flex_factors,
min_content_main_size_for_multiline_container,
@ -2687,6 +2685,7 @@ impl FlexItemBox {
flex_context.containing_block,
&self.independent_formatting_context.base,
false, /* depends_on_block_constraints */
&LazySize::intrinsic(),
)
.content_block_size
};

View file

@ -4,6 +4,7 @@
use geom::{FlexAxis, MainStartCrossStart};
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc as ServoArc;
use style::context::SharedStyleContext;
use style::logical_geometry::WritingMode;
@ -104,8 +105,7 @@ impl FlexContainer {
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
) -> Self {
let mut builder =
ModernContainerBuilder::new(context, info, propagated_data.union(&info.style));
let mut builder = ModernContainerBuilder::new(context, info, propagated_data);
contents.traverse(context, info, &mut builder);
let items = builder.finish();
@ -154,16 +154,17 @@ impl FlexLevelBox {
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &ServoArc<ComputedValues>,
) {
match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box
.independent_formatting_context
.repair_style(context, new_style),
.repair_style(context, node, new_style),
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut()
.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,
info,
block_level_boxes: Vec::new(),
propagated_data: propagated_data.union(&info.style),
propagated_data,
have_already_seen_first_line_for_text_indent: false,
anonymous_box_info: None,
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> {
self.inline_formatting_context_builder.take()?.finish(
self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent,
self.info.is_single_line_text_input(),
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.
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>> =
self.anonymous_table_content.drain(..).collect();
let last_text = match contents.last() {
@ -298,7 +287,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
};
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 {
self.ensure_inline_formatting_context_builder()
@ -315,7 +304,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
info: table_info,
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::AnonymousTable { table_block },
propagated_data,
propagated_data: self.propagated_data,
});
}
@ -464,7 +453,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
contents,
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 {
// If this inline element is an atomic, handle it and return.
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(
IndependentFormattingContext::construct(
context,
info,
display_inside,
contents,
// Text decorations are not propagated to atomic inline-level descendants.
propagaged_data,
propagated_data,
),
);
box_slot.set(LayoutBox::InlineLevel(vec![atomic]));
@ -550,7 +538,6 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
.and_then(|builder| {
builder.split_around_block_and_finish(
self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent,
self.info.style.writing_mode.to_bidi_level(),
)
@ -631,7 +618,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
info: info.clone(),
box_slot,
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(),
box_slot,
kind,
propagated_data: self.propagated_data.without_text_decorations(),
propagated_data: self.propagated_data,
});
}
@ -754,7 +741,7 @@ impl BlockLevelJob<'_> {
context,
info,
contents,
self.propagated_data.without_text_decorations(),
self.propagated_data,
false, /* is_list_item */
);
ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker {

View file

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

View file

@ -16,7 +16,6 @@ use super::{
InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
SharedInlineStyles,
};
use crate::PropagatedBoxTreeData;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
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
/// currently processing. Normally `display: contents` elements don't affect the structure of
/// 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
/// construction.
@ -344,7 +343,6 @@ impl InlineFormattingContextBuilder {
pub(crate) fn split_around_block_and_finish(
&mut self,
layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool,
default_bidi_level: Level,
) -> Option<InlineFormattingContext> {
@ -386,7 +384,6 @@ impl InlineFormattingContextBuilder {
inline_builder_from_before_split.finish(
layout_context,
propagated_data,
has_first_formatted_line,
/* is_single_line_text_input = */ false,
default_bidi_level,
@ -397,7 +394,6 @@ impl InlineFormattingContextBuilder {
pub(crate) fn finish(
self,
layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool,
is_single_line_text_input: bool,
default_bidi_level: Level,
@ -410,7 +406,6 @@ impl InlineFormattingContextBuilder {
Some(InlineFormattingContext::new_with_builder(
self,
layout_context,
propagated_data,
has_first_formatted_line,
is_single_line_text_input,
default_bidi_level,

View file

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

View file

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

View file

@ -91,6 +91,7 @@ use line_breaker::LineBreaker;
use malloc_size_of_derive::MallocSizeOf;
use range::Range;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode};
use servo_arc::Arc;
use style::Zero;
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::font::LineHeight;
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 text_run::{
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::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult};
use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin};
use crate::{ConstraintSpace, ContainingBlock, PropagatedBoxTreeData, SharedStyle};
use crate::{ConstraintSpace, ContainingBlock, SharedStyle};
// From gfxFontConstants.h in Firefox.
static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20;
@ -158,7 +159,9 @@ pub(crate) struct InlineFormattingContext {
/// context in order to avoid duplicating this information.
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:
/// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>.
@ -237,12 +240,14 @@ impl InlineItem {
InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => positioned_box
.borrow_mut()
.context
.repair_style(context, new_style),
.repair_style(context, node, new_style),
InlineItem::OutOfFlowFloatBox(float_box) => float_box
.borrow_mut()
.contents
.repair_style(context, new_style),
InlineItem::Atomic(atomic, ..) => atomic.borrow_mut().repair_style(context, new_style),
.repair_style(context, node, 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.
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
/// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`]
/// depends on the line-height quirk described in
@ -1454,7 +1453,6 @@ impl InlineFormattingContextLayout<'_> {
inline_styles: text_run.inline_styles.clone(),
font_metrics,
font_key: ifc_font_info.key,
text_decoration_line: self.current_inline_container_state().text_decoration_line,
bidi_level,
selection_range,
},
@ -1648,7 +1646,6 @@ impl InlineFormattingContext {
pub(super) fn new_with_builder(
builder: InlineFormattingContextBuilder,
layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool,
is_single_line_text_input: bool,
starting_bidi_level: Level,
@ -1699,7 +1696,11 @@ impl InlineFormattingContext {
inline_items: builder.inline_items,
inline_boxes: builder.inline_boxes,
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,
contains_floats: builder.contains_floats,
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(
&self,
layout_context: &LayoutContext,
@ -1764,7 +1770,6 @@ impl InlineFormattingContext {
style.to_arc(),
inline_container_state_flags,
None, /* parent_container */
self.text_decoration_line,
default_font_metrics.as_ref(),
),
inline_box_state_stack: Vec::new(),
@ -1862,10 +1867,8 @@ impl InlineContainerState {
style: Arc<ComputedValues>,
flags: InlineContainerStateFlags,
parent_container: Option<&InlineContainerState>,
parent_text_decoration_line: TextDecorationLine,
font_metrics: Option<&FontMetrics>,
) -> 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 line_height = line_height(
&style,
@ -1902,7 +1905,6 @@ impl InlineContainerState {
style,
flags,
has_content: RefCell::new(false),
text_decoration_line,
nested_strut_block_sizes: nested_block_sizes,
strut_block_sizes,
baseline_offset,

View file

@ -36,8 +36,8 @@ use crate::fragment_tree::{
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags,
};
use crate::geom::{
AuOrAuto, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint, PhysicalRect,
PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock,
AuOrAuto, LazySize, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint,
PhysicalRect, PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock,
};
use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength};
@ -78,6 +78,15 @@ impl BlockContainer {
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)]
@ -106,20 +115,21 @@ impl BlockLevelBox {
match self {
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
.borrow_mut()
.context
.repair_style(context, new_style),
.repair_style(context, node, new_style),
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) => {
outside_marker.repair_style(context, node, new_style)
},
BlockLevelBox::SameFormattingContextBlock { base, .. } => {
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
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> {
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.
@ -1217,6 +1231,15 @@ impl IndependentNonReplacedContents {
ignore_block_margins_for_stretch,
);
let lazy_block_size = LazySize::new(
&block_sizes,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
layout_style.is_table(),
);
let layout = self.layout(
layout_context,
positioning_context,
@ -1224,19 +1247,13 @@ impl IndependentNonReplacedContents {
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = layout
.content_inline_size_for_table
.unwrap_or(containing_block_for_children.size.inline);
let block_size = block_sizes.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
|| layout.content_block_size.into(),
layout_style.is_table(),
);
let block_size = lazy_block_size.resolve(|| layout.content_block_size);
let ResolvedMargins {
margin,
@ -1369,16 +1386,14 @@ impl IndependentNonReplacedContents {
)
};
let compute_block_size = |layout: &CacheableLayoutResult| {
content_box_sizes.block.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
|| layout.content_block_size.into(),
is_table,
)
};
let lazy_block_size = LazySize::new(
&content_box_sizes.block,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
is_table,
);
// The final inline size can depend on the available space, which depends on where
// we are placing the box, since floats reduce the available space.
@ -1407,10 +1422,11 @@ impl IndependentNonReplacedContents {
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
content_size = LogicalVec2 {
block: compute_block_size(&layout),
block: lazy_block_size.resolve(|| layout.content_block_size),
inline: layout.content_inline_size_for_table.unwrap_or(inline_size),
};
@ -1472,6 +1488,7 @@ impl IndependentNonReplacedContents {
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = if let Some(inline_size) = layout.content_inline_size_for_table {
@ -1485,7 +1502,7 @@ impl IndependentNonReplacedContents {
proposed_inline_size
};
content_size = LogicalVec2 {
block: compute_block_size(&layout),
block: lazy_block_size.resolve(|| layout.content_block_size),
inline: inline_size,
};
@ -2419,6 +2436,15 @@ impl IndependentFormattingContext {
"Mixed horizontal and vertical writing modes are not supported yet"
);
let lazy_block_size = LazySize::new(
&content_box_sizes_and_pbm.content_box_sizes.block,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
is_table,
);
let independent_layout = non_replaced.layout(
layout_context,
child_positioning_context,
@ -2426,18 +2452,12 @@ impl IndependentFormattingContext {
containing_block,
&self.base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = independent_layout
.content_inline_size_for_table
.unwrap_or(inline_size);
let block_size = content_box_sizes_and_pbm.content_box_sizes.block.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
|| independent_layout.content_block_size.into(),
is_table,
);
let block_size = lazy_block_size.resolve(|| independent_layout.content_block_size);
let content_size = LogicalVec2 {
block: block_size,

View file

@ -5,6 +5,8 @@
use app_units::Au;
use atomic_refcell::AtomicRef;
use compositing_traits::display_list::AxesScrollSensitivity;
use euclid::Rect;
use euclid::default::Size2D as UntypedSize2D;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{
@ -27,7 +29,7 @@ use crate::flow::inline::InlineItem;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentTree;
use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
use crate::geom::{LogicalVec2, PhysicalRect, PhysicalSize};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContents;
use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
@ -314,7 +316,7 @@ fn construct_for_root_element(
let contents = ReplacedContents::for_element(root_element, context)
.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() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
@ -348,7 +350,7 @@ impl BoxTree {
pub fn layout(
&self,
layout_context: &LayoutContext,
viewport: euclid::Size2D<f32, CSSPixel>,
viewport: UntypedSize2D<Au>,
) -> FragmentTree {
let style = layout_context
.style_context
@ -358,13 +360,8 @@ impl BoxTree {
// FIXME: use the documents mode:
// https://drafts.csswg.org/css-writing-modes/#principal-flow
let physical_containing_block = PhysicalRect::new(
PhysicalPoint::zero(),
PhysicalSize::new(
Au::from_f32_px(viewport.width),
Au::from_f32_px(viewport.height),
),
);
let physical_containing_block: Rect<Au, CSSPixel> =
PhysicalSize::from_untyped(viewport).into();
let initial_containing_block = DefiniteContainingBlock {
size: LogicalVec2 {
inline: physical_containing_block.size.width,

View file

@ -4,7 +4,7 @@
use app_units::Au;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutElement;
use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_arc::Arc;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
@ -15,6 +15,7 @@ use crate::dom_traversal::{Contents, NodeAndStyleInfo};
use crate::flexbox::FlexContainer;
use crate::flow::BlockFormattingContext;
use crate::fragment_tree::{BaseFragmentInfo, FragmentFlags};
use crate::geom::LazySize;
use crate::layout_box_base::{
CacheableLayoutResult, CacheableLayoutResultAndInputs, LayoutBoxBase,
};
@ -222,12 +223,13 @@ impl IndependentFormattingContext {
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
self.base.repair_style(new_style);
match &mut self.contents {
IndependentFormattingContextContents::NonReplaced(content) => {
content.repair_style(context, new_style);
content.repair_style(context, node, new_style);
},
IndependentFormattingContextContents::Replaced(..) => {},
}
@ -242,6 +244,7 @@ impl IndependentNonReplacedContents {
containing_block_for_children: &ContainingBlock,
containing_block: &ContainingBlock,
depends_on_block_constraints: bool,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
match self {
IndependentNonReplacedContents::Flow(bfc) => bfc.layout(
@ -255,6 +258,7 @@ impl IndependentNonReplacedContents {
positioning_context,
containing_block_for_children,
depends_on_block_constraints,
lazy_block_size,
),
IndependentNonReplacedContents::Grid(fc) => fc.layout(
layout_context,
@ -281,6 +285,7 @@ impl IndependentNonReplacedContents {
level = "trace",
)
)]
#[allow(clippy::too_many_arguments)]
pub fn layout(
&self,
layout_context: &LayoutContext,
@ -289,6 +294,7 @@ impl IndependentNonReplacedContents {
containing_block: &ContainingBlock,
base: &LayoutBoxBase,
depends_on_block_constraints: bool,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
if let Some(cache) = base.cached_layout_result.borrow().as_ref() {
let cache = &**cache;
@ -317,6 +323,7 @@ impl IndependentNonReplacedContents {
containing_block_for_children,
containing_block,
depends_on_block_constraints,
lazy_block_size,
);
*base.cached_layout_result.borrow_mut() = Some(Box::new(CacheableLayoutResultAndInputs {
@ -350,9 +357,16 @@ impl IndependentNonReplacedContents {
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 {
IndependentNonReplacedContents::Flow(..) => {},
IndependentNonReplacedContents::Flow(block_formatting_context) => {
block_formatting_context.repair_style(node, new_style);
},
IndependentNonReplacedContents::Flex(flex_container) => {
flex_container.repair_style(new_style)
},

View file

@ -92,7 +92,7 @@ pub(crate) struct BoxFragment {
pub scrollable_overflow_from_children: PhysicalRect<Au>,
/// 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.
pub(crate) resolved_sticky_insets: AtomicRefCell<Option<PhysicalSides<AuOrAuto>>>,

View file

@ -14,7 +14,6 @@ use range::Range as ServoRange;
use servo_arc::Arc as ServoArc;
use style::Zero;
use style::properties::ComputedValues;
use style::values::specified::text::TextDecorationLine;
use webrender_api::{FontInstanceKey, ImageKey};
use super::{
@ -72,9 +71,6 @@ pub(crate) struct TextFragment {
#[conditional_malloc_size_of]
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.
pub justification_adjustment: Au,
pub selection_range: Option<ServoRange<ByteIndex>>,

View file

@ -14,7 +14,6 @@ use webrender_api::units;
use super::{BoxFragment, ContainingBlockManager, Fragment};
use crate::ArcRefCell;
use crate::context::LayoutContext;
use crate::display_list::StackingContext;
use crate::geom::PhysicalRect;
#[derive(MallocSizeOf)]
@ -91,16 +90,6 @@ impl FragmentTree {
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) {
let mut print_tree = PrintTree::new("Fragment Tree".to_string());
for fragment in &self.root_fragments {

View file

@ -2,7 +2,7 @@
* 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::LazyCell;
use std::cell::{LazyCell, OnceCell};
use std::convert::From;
use std::fmt;
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
@ -1102,3 +1102,87 @@ impl Sizes {
)
}
}
struct LazySizeData<'a> {
sizes: &'a Sizes,
axis: Direction,
automatic_size: Size<Au>,
get_automatic_minimum_size: fn() -> Au,
stretch_size: Option<Au>,
is_table: bool,
}
/// Represents a size that can't be fully resolved until the intrinsic size
/// is known. This is useful in the block axis, since the intrinsic size
/// depends on layout, but the other inputs are known beforehand.
pub(crate) struct LazySize<'a> {
result: OnceCell<Au>,
data: Option<LazySizeData<'a>>,
}
impl<'a> LazySize<'a> {
pub(crate) fn new(
sizes: &'a Sizes,
axis: Direction,
automatic_size: Size<Au>,
get_automatic_minimum_size: fn() -> Au,
stretch_size: Option<Au>,
is_table: bool,
) -> Self {
Self {
result: OnceCell::new(),
data: Some(LazySizeData {
sizes,
axis,
automatic_size,
get_automatic_minimum_size,
stretch_size,
is_table,
}),
}
}
/// Creates a [`LazySize`] that will resolve to the intrinsic size.
/// Should be equivalent to [`LazySize::new()`] with default parameters,
/// but avoiding the trouble of getting a reference to a [`Sizes::default()`]
/// which lives long enough.
///
/// TODO: It's not clear what this should do if/when [`LazySize::resolve()`]
/// is changed to accept a [`ContentSizes`] as the intrinsic size.
pub(crate) fn intrinsic() -> Self {
Self {
result: OnceCell::new(),
data: None,
}
}
/// Resolves the [`LazySize`] into [`Au`], caching the result.
/// The argument is a callback that computes the intrinsic size lazily.
///
/// TODO: The intrinsic size should probably be a [`ContentSizes`] instead of [`Au`].
pub(crate) fn resolve(&self, get_content_size: impl FnOnce() -> Au) -> Au {
*self.result.get_or_init(|| {
let Some(ref data) = self.data else {
return get_content_size();
};
data.sizes.resolve(
data.axis,
data.automatic_size,
data.get_automatic_minimum_size,
data.stretch_size,
|| get_content_size().into(),
data.is_table,
)
})
}
}
impl From<Au> for LazySize<'_> {
/// Creates a [`LazySize`] that will resolve to the given [`Au`],
/// ignoring the intrinsic size.
fn from(value: Au) -> Self {
let result = OnceCell::new();
result.set(value).unwrap();
LazySize { result, data: None }
}
}

View file

@ -15,8 +15,8 @@ use base::Epoch;
use base::id::{PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::ScrollState;
use embedder_traits::{UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D, Vector2D};
use fnv::FnvHashMap;
use fonts::{FontContext, FontContextWebFontMethods};
@ -76,8 +76,8 @@ use url::Url;
use webrender_api::units::{DevicePixel, DevicePoint, LayoutPixel, LayoutPoint, LayoutSize};
use webrender_api::{ExternalScrollId, HitTestFlags};
use crate::context::LayoutContext;
use crate::display_list::{DisplayList, WebRenderImageInfo};
use crate::context::{CachedImageOrError, LayoutContext};
use crate::display_list::{DisplayListBuilder, StackingContextTree};
use crate::query::{
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,
@ -144,17 +144,19 @@ pub struct LayoutThread {
/// The fragment tree.
fragment_tree: RefCell<Option<Arc<FragmentTree>>>,
/// The [`StackingContextTree`] cached from previous layouts.
stacking_context_tree: RefCell<Option<StackingContextTree>>,
/// A counter for epoch messages
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: 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.
registered_painters: RegisteredPaintersImpl,
@ -504,8 +506,7 @@ impl LayoutThread {
Scale::new(config.viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(config.font_context.clone())),
ComputedValues::initial_values_with_font_override(font),
// TODO: obtain preferred color scheme from embedder
PrefersColorScheme::Light,
config.theme.into(),
);
LayoutThread {
@ -521,16 +522,13 @@ impl LayoutThread {
first_reflow: Cell::new(true),
box_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: 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,
scroll_offsets: Default::default(),
stylist: Stylist::new(device, QuirksMode::NoQuirks),
webrender_image_cache: Default::default(),
resolved_images_cache: Default::default(),
debug: opts::get().debug.clone(),
}
}
@ -607,7 +605,8 @@ impl LayoutThread {
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() {
data.hint.insert(RestyleHint::recascade_subtree());
}
@ -639,8 +638,9 @@ impl LayoutThread {
),
image_cache: self.image_cache.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_rasterization_images: Mutex::default(),
node_image_animation_map: Arc::new(RwLock::new(std::mem::take(
&mut reflow_request.node_to_image_animation_map,
))),
@ -649,25 +649,32 @@ impl LayoutThread {
highlighted_dom_node: reflow_request.highlighted_dom_node,
};
self.restyle_and_build_trees(
let damage = self.restyle_and_build_trees(
&reflow_request,
root_element,
rayon_pool,
&mut layout_context,
viewport_changed,
);
self.build_display_list(&reflow_request, &mut layout_context);
self.first_reflow.set(false);
self.build_stacking_context_tree(&reflow_request, damage);
self.build_display_list(&reflow_request, &mut layout_context);
self.first_reflow.set(false);
if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal {
self.update_scroll_node_state(&scroll_state);
}
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 node_to_image_animation_map =
std::mem::take(&mut *layout_context.node_image_animation_map.write());
Some(ReflowResult {
pending_images,
pending_rasterization_images,
iframe_sizes,
node_to_image_animation_map,
})
@ -676,12 +683,12 @@ impl LayoutThread {
fn update_device_if_necessary(
&mut self,
reflow_request: &ReflowRequest,
viewport_changed: bool,
guards: &StylesheetGuards,
) -> bool {
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);
if !viewport_size_changed && !theme_changed {
if !viewport_changed && !theme_changed {
return false;
}
self.update_device(
@ -689,7 +696,7 @@ impl LayoutThread {
reflow_request.theme,
guards,
);
(viewport_size_changed && had_used_viewport_units) || theme_changed
(viewport_changed && had_used_viewport_units) || theme_changed
}
fn prepare_stylist_for_reflow<'dom>(
@ -742,7 +749,8 @@ impl LayoutThread {
root_element: ServoLayoutElement<'_>,
rayon_pool: Option<&ThreadPool>,
layout_context: &mut LayoutContext<'_>,
) {
viewport_changed: bool,
) -> RestyleDamage {
let dirty_root = unsafe {
ServoLayoutNode::new(&reflow_request.dirty_root.unwrap())
.as_element()
@ -758,7 +766,7 @@ impl LayoutThread {
if !token.should_traverse() {
layout_context.style_context.stylist.rule_tree().maybe_gc();
return;
return RestyleDamage::empty();
}
let dirty_root: ServoLayoutNode =
@ -766,9 +774,9 @@ impl LayoutThread {
let root_node = root_element.as_node();
let damage = compute_damage_and_repair_style(layout_context.shared_context(), root_node);
if damage == RestyleDamage::REPAINT {
if !viewport_changed && !damage.contains(RestyleDamage::REBUILD_BOX) {
layout_context.style_context.stylist.rule_tree().maybe_gc();
return;
return damage;
}
let mut box_tree = self.box_tree.borrow_mut();
@ -787,10 +795,7 @@ impl LayoutThread {
build_box_tree()
};
let viewport_size = Size2D::new(
self.viewport_size.width.to_f32_px(),
self.viewport_size.height.to_f32_px(),
);
let viewport_size = self.stylist.device().au_viewport_size();
let run_layout = || {
box_tree
.as_ref()
@ -803,8 +808,15 @@ impl LayoutThread {
run_layout()
});
if self.debug.dump_flow_tree {
fragment_tree.print();
}
*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 {
println!(
"{:?}",
@ -822,6 +834,42 @@ impl LayoutThread {
// GC the rule tree if some heuristics are met.
layout_context.style_context.stylist.rule_tree().maybe_gc();
damage
}
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(),
);
// 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,
fragment_tree.scrollable_overflow(),
self.id.into(),
fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
&self.debug,
));
}
fn build_display_list(
@ -829,68 +877,40 @@ impl LayoutThread {
reflow_request: &ReflowRequest,
layout_context: &mut LayoutContext<'_>,
) {
if !reflow_request.reflow_goal.needs_display() {
return;
}
let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
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;
}
};
let mut epoch = self.epoch.get();
epoch.next();
self.epoch.set(epoch);
stacking_context_tree.compositor_info.epoch = epoch.into();
let viewport_size = LayoutSize::from_untyped(Size2D::new(
self.viewport_size.width.to_f32_px(),
self.viewport_size.height.to_f32_px(),
));
let mut display_list = DisplayList::new(
viewport_size,
fragment_tree.scrollable_overflow(),
self.id.into(),
epoch.into(),
fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
let built_display_list = DisplayListBuilder::build(
layout_context,
stacking_context_tree,
fragment_tree,
&self.debug,
);
self.compositor_api.send_display_list(
self.webview_id,
&stacking_context_tree.compositor_info,
built_display_list,
);
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.webview_id,
display_list.compositor_info,
display_list.wr.end().1,
);
let (keys, instance_keys) = self
.font_context
.collect_unused_webrender_resources(false /* all */);
self.compositor_api
.remove_unused_font_resources(keys, instance_keys)
}
let (keys, instance_keys) = self
.font_context
.collect_unused_webrender_resources(false /* all */);
self.compositor_api
.remove_unused_font_resources(keys, instance_keys)
}
fn update_scroll_node_state(&self, state: &ScrollState) {
@ -930,9 +950,6 @@ impl LayoutThread {
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 size_did_change = device.au_viewport_size() != new_viewport_size;
let pixel_ratio_did_change = device.device_pixel_ratio().get() != new_pixel_ratio;
@ -940,7 +957,8 @@ impl LayoutThread {
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()
}
@ -948,7 +966,7 @@ impl LayoutThread {
fn update_device(
&mut self,
viewport_details: ViewportDetails,
theme: PrefersColorScheme,
theme: Theme,
guards: &StylesheetGuards,
) {
let device = Device::new(
@ -958,7 +976,7 @@ impl LayoutThread {
Scale::new(viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(self.font_context.clone())),
self.stylist.device().default_computed_values().to_arc(),
theme,
theme.into(),
);
// 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 style::logical_geometry::WritingMode;
use style::properties::ComputedValues;
use style::values::computed::TextDecorationLine;
use crate::geom::{LogicalVec2, SizeConstraint};
use crate::style_ext::AspectRatio;
@ -163,39 +162,20 @@ impl<'a> From<&'_ DefiniteContainingBlock<'a>> for ContainingBlock<'a> {
/// propoagation, but only during `BoxTree` construction.
#[derive(Clone, Copy, Debug)]
struct PropagatedBoxTreeData {
text_decoration: TextDecorationLine,
allow_percentage_column_in_tables: bool,
}
impl Default for PropagatedBoxTreeData {
fn default() -> Self {
Self {
text_decoration: Default::default(),
allow_percentage_column_in_tables: true,
}
}
}
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 {
Self {
text_decoration: self.text_decoration,
allow_percentage_column_in_tables: false,
}
}

View file

@ -24,12 +24,11 @@ use crate::fragment_tree::{
BoxFragment, Fragment, FragmentFlags, HoistedSharedFragment, SpecificLayoutInfo,
};
use crate::geom::{
AuOrAuto, LengthPercentageOrAuto, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2,
PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, PhysicalVec, Size, Sizes, ToLogical,
ToLogicalWithContainingBlock,
AuOrAuto, LazySize, LengthPercentageOrAuto, LogicalRect, LogicalSides, LogicalSides1D,
LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, PhysicalVec, Size,
Sizes, ToLogical, ToLogicalWithContainingBlock,
};
use crate::layout_box_base::LayoutBoxBase;
use crate::sizing::ContentSizes;
use crate::style_ext::{Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, DisplayInside};
use crate::{
ConstraintSpace, ContainingBlock, ContainingBlockSize, DefiniteContainingBlock,
@ -560,18 +559,32 @@ impl HoistedAbsolutelyPositionedBox {
// The block size can depend on layout results, so we only solve it extrinsically,
// we may have to resolve it properly later on.
let extrinsic_block_size = block_axis_solver.solve_size_extrinsically();
let block_automatic_size = block_axis_solver.automatic_size();
let block_stretch_size = Some(block_axis_solver.stretch_size());
let extrinsic_block_size = block_axis_solver.computed_sizes.resolve_extrinsic(
block_automatic_size,
Au::zero(),
block_stretch_size,
);
// The inline axis can be fully resolved, computing intrinsic sizes using the
// extrinsic block size.
let inline_size = inline_axis_solver.solve_size(|| {
let get_inline_content_size = || {
let ratio = context.preferred_aspect_ratio(&pbm.padding_border_sums);
let constraint_space =
ConstraintSpace::new(extrinsic_block_size, style.writing_mode, ratio);
context
.inline_content_sizes(layout_context, &constraint_space)
.sizes
});
};
let inline_size = inline_axis_solver.computed_sizes.resolve(
Direction::Inline,
inline_axis_solver.automatic_size(),
Au::zero,
Some(inline_axis_solver.stretch_size()),
get_inline_content_size,
is_table,
);
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
@ -587,6 +600,14 @@ impl HoistedAbsolutelyPositionedBox {
"Mixed horizontal and vertical writing modes are not supported yet"
);
let lazy_block_size = LazySize::new(
&block_axis_solver.computed_sizes,
Direction::Block,
block_automatic_size,
Au::zero,
block_stretch_size,
is_table,
);
let independent_layout = non_replaced.layout(
layout_context,
&mut positioning_context,
@ -594,6 +615,7 @@ impl HoistedAbsolutelyPositionedBox {
containing_block,
&context.base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
// Tables can become narrower than predicted due to collapsed columns
@ -602,8 +624,8 @@ impl HoistedAbsolutelyPositionedBox {
.unwrap_or(inline_size);
// Now we can properly solve the block size.
let block_size = block_axis_solver
.solve_size(|| independent_layout.content_block_size.into());
let block_size =
lazy_block_size.resolve(|| independent_layout.content_block_size);
content_size = LogicalVec2 {
inline: inline_size,
@ -765,27 +787,6 @@ impl AbsoluteAxisSolver<'_> {
)
}
#[inline]
fn solve_size_extrinsically(&self) -> SizeConstraint {
self.computed_sizes.resolve_extrinsic(
self.automatic_size(),
Au::zero(),
Some(self.stretch_size()),
)
}
#[inline]
fn solve_size(&self, get_content_size: impl FnOnce() -> ContentSizes) -> Au {
self.computed_sizes.resolve(
self.axis,
self.automatic_size(),
Au::zero,
Some(self.stretch_size()),
get_content_size,
self.is_table,
)
}
fn solve_margins(&self, size: Au) -> LogicalSides1D<Au> {
if self.box_offsets.either_auto() {
LogicalSides1D::new(

View file

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

View file

@ -87,6 +87,15 @@ input[type="file"] {
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="center"] { text-align: center; }
td[align="right"] { text-align: right; }
@ -256,4 +265,8 @@ select {
padding: 0 0.25em;
/* Don't show a text cursor when hovering selected option */
cursor: default;
}
slot {
display: contents;
}

View file

@ -81,12 +81,7 @@ impl Table {
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
) -> Self {
let mut traversal = TableBuilderTraversal::new(
context,
info,
grid_style,
propagated_data.union(&info.style),
);
let mut traversal = TableBuilderTraversal::new(context, info, grid_style, propagated_data);
contents.traverse(context, info, &mut traversal);
traversal.finish()
}
@ -771,9 +766,6 @@ impl<'dom> TraversalHandler<'dom> for TableBuilderTraversal<'_, 'dom> {
});
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;
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.current_row_group_index = None;
self.current_propagated_data = previous_propagated_data;
self.builder.incoming_rowspans.clear();
box_slot.set(LayoutBox::TableLevelBox(TableLevelBox::TrackGroup(
@ -936,7 +927,7 @@ impl<'style, 'builder, 'dom, 'a> TableRowBuilder<'style, 'builder, 'dom, 'a> {
table_traversal,
info,
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;
use euclid::{Point2D, Size2D, UnknownUnit, Vector2D};
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutElement;
use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_arc::Arc;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
@ -425,13 +425,14 @@ impl TableLevelBox {
pub(crate) fn repair_style(
&self,
context: &SharedStyleContext<'_>,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
match self {
TableLevelBox::Caption(caption) => caption
.borrow_mut()
.context
.repair_style(context, new_style),
.repair_style(context, node, new_style),
TableLevelBox::Cell(cell) => cell.borrow_mut().repair_style(new_style),
TableLevelBox::TrackGroup(track_group) => {
track_group.borrow_mut().repair_style(new_style);

View file

@ -23,8 +23,8 @@ use crate::fragment_tree::{
BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
};
use crate::geom::{
LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size, SizeConstraint,
Sizes,
LazySize, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size,
SizeConstraint, Sizes,
};
use crate::layout_box_base::CacheableLayoutResult;
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength};
@ -251,6 +251,12 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> {
style,
};
let lazy_block_size = match content_box_known_dimensions.height {
// FIXME: use the correct min/max sizes.
None => LazySize::intrinsic(),
Some(height) => Au::from_f32_px(height).into(),
};
child.positioning_context = PositioningContext::default();
let layout = non_replaced.layout_without_caching(
self.layout_context,
@ -258,13 +264,16 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> {
&content_box_size_override,
containing_block,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
child.child_fragments = layout.fragments;
self.child_specific_layout_infos[usize::from(node_id)] =
layout.specific_layout_info;
let block_size = layout.content_block_size.to_f32_px();
let block_size = lazy_block_size
.resolve(|| layout.content_block_size)
.to_f32_px();
let computed_size = taffy::Size {
width: inline_size + pb_sum.inline,

View file

@ -7,6 +7,7 @@ use std::fmt;
use app_units::Au;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
@ -35,8 +36,7 @@ impl TaffyContainer {
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
) -> Self {
let mut builder =
ModernContainerBuilder::new(context, info, propagated_data.union(&info.style));
let mut builder = ModernContainerBuilder::new(context, info, propagated_data);
contents.traverse(context, info, &mut builder);
let items = builder.finish();
@ -152,17 +152,18 @@ impl TaffyItemBox {
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
self.style = new_style.clone();
match &mut self.taffy_level_box {
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
.borrow_mut()
.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,
) -> RestyleDamage {
let original_damage;
let damage = {
let damage;
{
let mut element_data = node
.style_data()
.expect("Should not run `compute_damage` before styling.")
.element_data
.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 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;
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);
}

View file

@ -22,6 +22,8 @@ indexmap = { workspace = true }
ipc-channel = { workspace = true }
keyboard-types = { workspace = true }
markup5ever = { workspace = true }
mime = { workspace = true }
resvg = { workspace = true }
servo_allocator = { path = "../allocator" }
servo_arc = { 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!(app_units::Au);
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::NonZeroUsize);
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::Instant);
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::font_face::SourceList);
malloc_size_of_is_0!(style::properties::ComputedValues);

View file

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

View file

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

View file

@ -18,6 +18,7 @@ use http::{HeaderValue, Method, StatusCode};
use ipc_channel::ipc;
use log::{debug, trace, warn};
use mime::{self, Mime};
use net_traits::fetch::headers::extract_mime_type_as_mime;
use net_traits::filemanager_thread::{FileTokenCheck, RelativePos};
use net_traits::http_status::HttpStatus;
use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
@ -886,7 +887,7 @@ pub fn should_be_blocked_due_to_nosniff(
// Step 2
// 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>
#[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())
}
match content_type_header {
match mime_type {
// Step 4
Some(ref ct) if destination.is_script_like() => {
!is_javascript_mime_type(&ct.clone().into())
},
Some(ref mime_type) if destination.is_script_like() => !is_javascript_mime_type(mime_type),
// Step 5
Some(ref ct) if destination == Destination::Style => {
let m: mime::Mime = ct.clone().into();
m.type_() != mime::TEXT && m.subtype() != mime::CSS
Some(ref mime_type) if destination == Destination::Style => {
mime_type.type_() != mime::TEXT && mime_type.subtype() != mime::CSS
},
None if destination == Destination::Style || destination.is_script_like() => true,
@ -938,18 +935,22 @@ fn should_be_blocked_due_to_mime_type(
destination: Destination,
response_headers: &HeaderMap,
) -> bool {
// Step 1
let mime_type: mime::Mime = match response_headers.typed_get::<ContentType>() {
Some(header) => header.into(),
// Step 1: Let mimeType be the result of extracting a MIME type from responses header list.
let mime_type: mime::Mime = match extract_mime_type_as_mime(response_headers) {
Some(mime_type) => mime_type,
// Step 2: If mimeType is failure, then return allowed.
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() &&
match mime_type.type_() {
mime::AUDIO | mime::VIDEO | mime::IMAGE => true,
mime::TEXT if mime_type.subtype() == mime::CSV => true,
// Step 4
_ => false,
}
}

View file

@ -4,26 +4,39 @@
use std::collections::HashMap;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::num::NonZeroU64;
use std::sync::LazyLock;
use std::time::Duration;
use base::cross_process_instant::CrossProcessInstant;
use embedder_traits::resources::{self, Resource};
use fst::{Map, MapBuilder};
use headers::{HeaderMapExt, StrictTransportSecurity};
use http::HeaderMap;
use log::{error, info};
use log::{debug, error, info};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use malloc_size_of_derive::MallocSizeOf;
use net_traits::IncludeSubdomains;
use net_traits::pub_domains::reg_suffix;
use serde::{Deserialize, Serialize};
use servo_config::pref;
use servo_url::{Host, ServoUrl};
use time::UtcDateTime;
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct HstsEntry {
pub host: String,
pub include_subdomains: bool,
pub max_age: Option<Duration>,
pub timestamp: Option<CrossProcessInstant>,
// Nonzero to allow for memory optimization
pub expires_at: Option<NonZeroU64>,
}
// Zero and negative times are all expired
fn unix_timestamp_to_nonzerou64(timestamp: i64) -> NonZeroU64 {
if timestamp <= 0 {
NonZeroU64::new(1).unwrap()
} else {
NonZeroU64::new(timestamp.try_into().unwrap()).unwrap()
}
}
impl HstsEntry {
@ -32,73 +45,117 @@ impl HstsEntry {
subdomains: IncludeSubdomains,
max_age: Option<Duration>,
) -> Option<HstsEntry> {
let expires_at = max_age.map(|duration| {
unix_timestamp_to_nonzerou64((UtcDateTime::now() + duration).unix_timestamp())
});
if host.parse::<Ipv4Addr>().is_ok() || host.parse::<Ipv6Addr>().is_ok() {
None
} else {
Some(HstsEntry {
host,
include_subdomains: (subdomains == IncludeSubdomains::Included),
max_age,
timestamp: Some(CrossProcessInstant::now()),
expires_at,
})
}
}
pub fn is_expired(&self) -> bool {
match (self.max_age, self.timestamp) {
(Some(max_age), Some(timestamp)) => CrossProcessInstant::now() - timestamp >= max_age,
match self.expires_at {
Some(timestamp) => {
unix_timestamp_to_nonzerou64(UtcDateTime::now().unix_timestamp()) >= timestamp
},
_ => false,
}
}
fn matches_domain(&self, host: &str) -> bool {
!self.is_expired() && self.host == host
self.host == host
}
fn matches_subdomain(&self, host: &str) -> bool {
!self.is_expired() && host.ends_with(&format!(".{}", self.host))
host.ends_with(&format!(".{}", self.host))
}
}
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
pub struct HstsList {
// Map from base domains to a list of entries that are subdomains of base domain
pub entries_map: HashMap<String, Vec<HstsEntry>>,
}
impl HstsList {
/// Represents the portion of the HSTS list that comes from the preload list
/// 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
/// and more efficient like FSTs or DAFSA/DAWGs.
/// To generate a new version of the FST map file run `./mach update-hsts-preload`
#[derive(Clone, Debug)]
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()) }
}
}
static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> =
LazyLock::new(HstsPreloadList::from_servo_preload);
pub fn hsts_preload_size_of(ops: &mut MallocSizeOfOps) -> usize {
PRELOAD_LIST_ENTRIES.size_of(ops)
}
impl HstsPreloadList {
/// Create an `HstsList` from the bytes of a JSON preload file.
pub fn from_preload(preload_content: &str) -> Option<HstsList> {
#[derive(Deserialize)]
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: HstsList = HstsList::default();
for hsts_entry in hsts_entries.entries {
hsts_list.push(hsts_entry);
}
hsts_list
})
pub fn from_preload(preload_content: Vec<u8>) -> Option<HstsPreloadList> {
Map::new(preload_content).map(HstsPreloadList).ok()
}
pub fn from_servo_preload() -> HstsList {
let list = resources::read_string(Resource::HstsPreloadList);
HstsList::from_preload(&list).unwrap_or_else(|| {
pub fn from_servo_preload() -> HstsPreloadList {
debug!("Intializing HSTS Preload list");
let map_bytes = resources::read_bytes(Resource::HstsPreloadList);
HstsPreloadList::from_preload(map_bytes).unwrap_or_else(|| {
error!("HSTS preload file is invalid. Setting HSTS list to default values");
HstsList::default()
HstsPreloadList(MapBuilder::memory().into_map())
})
}
pub fn is_host_secure(&self, host: &str) -> bool {
let base_domain = reg_suffix(host);
let parts = host[..host.len() - base_domain.len()].rsplit_terminator('.');
let mut domain_to_test = base_domain.to_owned();
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
}) {
return true;
}
// Check all further subdomains up to the passed host
for part in parts {
domain_to_test = format!("{}.{}", part, domain_to_test);
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
}) {
return true;
}
}
false
}
}
impl HstsList {
pub fn is_host_secure(&self, host: &str) -> bool {
if PRELOAD_LIST_ENTRIES.is_host_secure(host) {
info!("{host} is in the preload list");
return true;
}
let base_domain = reg_suffix(host);
self.entries_map.get(base_domain).is_some_and(|entries| {
entries.iter().any(|e| {
entries.iter().filter(|e| !e.is_expired()).any(|e| {
if e.include_subdomains {
e.matches_subdomain(host) || e.matches_domain(host)
} else {
@ -115,9 +172,11 @@ impl HstsList {
}
fn has_subdomain(&self, host: &str, base_domain: &str) -> bool {
self.entries_map
.get(base_domain)
.is_some_and(|entries| entries.iter().any(|e| e.matches_subdomain(host)))
self.entries_map.get(base_domain).is_some_and(|entries| {
entries
.iter()
.any(|e| e.include_subdomains && e.matches_subdomain(host))
})
}
pub fn push(&mut self, entry: HstsEntry) {
@ -130,13 +189,14 @@ impl HstsList {
if !have_domain && !have_subdomain {
entries.push(entry);
} else if !have_subdomain {
for e in entries {
for e in entries.iter_mut() {
if e.matches_domain(&entry.host) {
e.include_subdomains = entry.include_subdomains;
e.max_age = entry.max_age;
e.expires_at = entry.expires_at;
}
}
}
entries.retain(|e| !e.is_expired());
}
/// Step 2.9 of <https://fetch.spec.whatwg.org/#concept-main-fetch>.

View file

@ -7,21 +7,25 @@ use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::sync::{Arc, Mutex};
use std::{mem, thread};
use base::id::PipelineId;
use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData};
use imsz::imsz_from_reader;
use ipc_channel::ipc::IpcSharedMemory;
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
use log::{debug, warn};
use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps};
use malloc_size_of_derive::MallocSizeOf;
use mime::Mime;
use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse,
PendingImageId, UsePlaceholder,
Image, ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener,
ImageOrMetadataAvailable, ImageResponse, PendingImageId, RasterizationCompleteResponse,
UsePlaceholder, VectorImage,
};
use net_traits::request::CorsSettings;
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::path;
use resvg::{tiny_skia, usvg};
use servo_config::pref;
use servo_url::{ImmutableOrigin, ServoUrl};
use webrender_api::units::DeviceIntSize;
@ -48,12 +52,53 @@ const FALLBACK_RIPPY: &[u8] = include_bytes!("../../resources/rippy.png");
// Helper functions.
// ======================================================================
fn decode_bytes_sync(key: LoadKey, bytes: &[u8], cors: CorsStatus) -> DecoderMsg {
let image = load_from_memory(bytes, cors);
fn parse_svg_document_in_memory(bytes: &[u8]) -> Result<usvg::Tree, &'static str> {
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 }
}
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)
.or_else(|| load_from_memory(FALLBACK_RIPPY, CorsStatus::Unsafe))
.expect("load fallback image failed");
@ -61,15 +106,15 @@ fn get_placeholder_image(compositor_api: &CrossProcessCompositorApi, data: &[u8]
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() {
return;
}
let mut bytes = Vec::new();
let frame_bytes = image.bytes();
let frame_bytes = image.first_frame().bytes;
let is_opaque = match image.format {
PixelFormat::BGRA8 => {
bytes.extend_from_slice(&frame_bytes);
PixelFormat::BGRA8 | PixelFormat::RGBA8 => {
bytes.extend_from_slice(frame_bytes);
pixels::rgba8_premultiply_inplace(bytes.as_mut_slice())
},
PixelFormat::RGB8 => {
@ -80,16 +125,24 @@ fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &m
true
},
PixelFormat::K8 | PixelFormat::KA8 | PixelFormat::RGBA8 => {
PixelFormat::K8 | PixelFormat::KA8 => {
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;
flags.set(ImageDescriptorFlags::IS_OPAQUE, is_opaque);
let size = DeviceIntSize::new(image.metadata.width as i32, image.metadata.height as i32);
let descriptor = ImageDescriptor {
size: DeviceIntSize::new(image.width as i32, image.height as i32),
size,
stride: None,
format: ImageFormat::BGRA8,
format,
offset: 0,
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.
struct DecoderMsg {
key: LoadKey,
image: Option<Image>,
image: Option<DecodedImage>,
}
#[derive(MallocSizeOf)]
@ -265,8 +330,9 @@ impl LoadKeyGenerator {
#[derive(Debug)]
enum LoadResult {
Loaded(Image),
PlaceholderLoaded(Arc<Image>),
LoadedRasterImage(RasterImage),
LoadedVectorImage(VectorImageData),
PlaceholderLoaded(Arc<RasterImage>),
None,
}
@ -285,7 +351,7 @@ struct PendingLoad {
result: Option<Result<(), NetworkError>>,
/// 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
/// if we are loading a data: url.
@ -302,6 +368,9 @@ struct PendingLoad {
/// The URL of the final response that contains a body.
final_url: Option<ServoUrl>,
/// The MIME type from the `Content-type` header of the HTTP response, if any.
content_type: Option<Mime>,
}
impl PendingLoad {
@ -320,33 +389,48 @@ impl PendingLoad {
final_url: None,
cors_setting,
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);
}
}
// ======================================================================
// Image cache implementation.
// ======================================================================
#[derive(Default, MallocSizeOf)]
struct RasterizationTask {
listeners: Vec<(PipelineId, IpcSender<ImageCacheResponseMessage>)>,
result: Option<RasterImage>,
}
/// ## Image cache implementation.
#[derive(MallocSizeOf)]
struct ImageCacheStore {
// Images that are loading over network, or decoding.
/// Images that are loading over network, or decoding.
pending_loads: AllPendingLoads,
// Images that have finished loading (successful or not)
/// Images that have finished loading (successful or not)
completed_loads: HashMap<ImageKey, CompletedLoad>,
// The placeholder image used when an image fails to load
#[conditional_malloc_size_of]
placeholder_image: Arc<Image>,
/// Vector (e.g. SVG) images that have been sucessfully loaded and parsed
/// but are yet to be rasterized. Since the same SVG data can be used for
/// 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,
// Cross-process compositor API instance.
/// Cross-process compositor API instance.
#[ignore_malloc_size_of = "Channel from another crate"]
compositor_api: CrossProcessCompositorApi,
}
@ -361,15 +445,34 @@ impl ImageCacheStore {
};
match load_result {
LoadResult::Loaded(ref mut image) => {
set_webrender_image_key(&self.compositor_api, image)
LoadResult::LoadedRasterImage(ref mut raster_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 => {},
}
let url = pending_load.final_url.clone();
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) => {
ImageResponse::PlaceholderLoaded(image, self.placeholder_url.clone())
},
@ -399,19 +502,18 @@ impl ImageCacheStore {
origin: ImmutableOrigin,
cors_setting: Option<CorsSettings>,
placeholder: UsePlaceholder,
) -> Option<Result<(Arc<Image>, ServoUrl), ()>> {
) -> Option<Result<(Image, ServoUrl), ()>> {
self.completed_loads
.get(&(url, origin, cors_setting))
.map(
|completed_load| match (&completed_load.image_response, placeholder) {
(&ImageResponse::Loaded(ref image, ref url), _) |
(
&ImageResponse::PlaceholderLoaded(ref image, ref url),
UsePlaceholder::Yes,
) => Ok((image.clone(), url.clone())),
(&ImageResponse::PlaceholderLoaded(_, _), UsePlaceholder::No) |
(&ImageResponse::None, _) |
(&ImageResponse::MetadataLoaded(_), _) => Err(()),
(ImageResponse::Loaded(image, url), _) => Ok((image.clone(), url.clone())),
(ImageResponse::PlaceholderLoaded(image, url), UsePlaceholder::Yes) => {
Ok((Image::Raster(image.clone()), url.clone()))
},
(ImageResponse::PlaceholderLoaded(_, _), UsePlaceholder::No) |
(ImageResponse::None, _) |
(ImageResponse::MetadataLoaded(_), _) => Err(()),
},
)
}
@ -421,7 +523,10 @@ impl ImageCacheStore {
fn handle_decoder(&mut self, msg: DecoderMsg) {
let image = match msg.image {
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);
}
@ -450,6 +555,8 @@ impl ImageCache for ImageCacheImpl {
store: Arc::new(Mutex::new(ImageCacheStore {
pending_loads: AllPendingLoads::new(),
completed_loads: HashMap::new(),
vector_images: HashMap::new(),
rasterized_vector_images: HashMap::new(),
placeholder_image: get_placeholder_image(&compositor_api, &rippy_data),
placeholder_url: ServoUrl::parse("chrome://resources/rippy.png").unwrap(),
compositor_api,
@ -475,7 +582,7 @@ impl ImageCache for ImageCacheImpl {
url: ServoUrl,
origin: ImmutableOrigin,
cors_setting: Option<CorsSettings>,
) -> Option<Arc<Image>> {
) -> Option<Image> {
let store = self.store.lock().unwrap();
let result =
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) {
(&Some(Ok(_)), _) => {
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)) => {
debug!("Metadata available for {} ({:?})", url, key);
return ImageCacheResult::Available(
ImageOrMetadataAvailable::MetadataAvailable(meta.clone(), key),
ImageOrMetadataAvailable::MetadataAvailable(*meta, key),
);
},
(&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,
/// 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();
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());
pending_load.final_url = final_url;
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), _) => {
debug!("Got some data for {:?}", id);
@ -619,7 +863,7 @@ impl ImageCache for ImageCacheImpl {
height: info.height as u32,
};
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);
}
@ -629,17 +873,21 @@ impl ImageCache for ImageCacheImpl {
debug!("Received EOF for {:?}", key);
match result {
Ok(_) => {
let (bytes, cors_status) = {
let (bytes, cors_status, content_type) = {
let mut store = self.store.lock().unwrap();
let pending_load = store.pending_loads.get_by_key_mut(&id).unwrap();
pending_load.result = Some(Ok(()));
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();
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");
local_store.lock().unwrap().handle_decoder(msg);
});
@ -669,6 +917,8 @@ impl ImageCache for ImageCacheImpl {
placeholder_image,
placeholder_url,
compositor_api,
vector_images: HashMap::new(),
rasterized_vector_images: HashMap::new(),
})),
thread_pool: self.thread_pool.clone(),
})
@ -681,9 +931,16 @@ impl Drop for ImageCacheStore {
.completed_loads
.values()
.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,
})
.chain(
self.rasterized_vector_images
.values()
.filter_map(|task| task.result.as_ref()?.id.map(ImageUpdate::DeleteImage)),
)
.collect();
self.compositor_api.update_images(image_updates);
}
@ -691,11 +948,11 @@ impl Drop for ImageCacheStore {
impl ImageCacheImpl {
/// 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;
if let Some(load) = store.pending_loads.get_by_key_mut(&id) {
if let Some(ref metadata) = load.metadata {
listener.respond(ImageResponse::MetadataLoaded(metadata.clone()));
listener.respond(ImageResponse::MetadataLoaded(*metadata));
}
load.add_listener(listener);
return;

View file

@ -23,6 +23,7 @@ use ipc_channel::ipc::{self, IpcReceiver, IpcReceiverSet, IpcSender};
use log::{debug, trace, warn};
use net_traits::blob_url_store::parse_blob_url;
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::response::{Response, ResponseInit};
use net_traits::storage_thread::StorageThreadMsg;
@ -32,8 +33,10 @@ use net_traits::{
WebSocketDomAction, WebSocketNetworkEvent,
};
use profile_traits::mem::{
ProcessReports, ProfilerChan as MemProfilerChan, ReportsChan, perform_memory_report,
ProcessReports, ProfilerChan as MemProfilerChan, Report, ReportKind, ReportsChan,
perform_memory_report,
};
use profile_traits::path;
use profile_traits::time::ProfilerChan;
use rustls::RootCertStore;
use serde::{Deserialize, Serialize};
@ -50,7 +53,7 @@ use crate::fetch::cors_cache::CorsCache;
use crate::fetch::fetch_params::FetchParams;
use crate::fetch::methods::{CancellationListener, FetchContext, fetch};
use crate::filemanager_thread::FileManager;
use crate::hsts::HstsList;
use crate::hsts::{self, HstsList};
use crate::http_cache::HttpCache;
use crate::http_loader::{HttpState, http_redirect_fetch};
use crate::protocols::ProtocolRegistry;
@ -94,14 +97,15 @@ pub fn new_resource_threads(
let (public_core, private_core) = new_core_resource_thread(
devtools_sender,
time_profiler_chan,
mem_profiler_chan,
mem_profiler_chan.clone(),
embedder_proxy,
config_dir.clone(),
ca_certificates,
ignore_certificate_errors,
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(private_core, storage),
@ -176,7 +180,7 @@ fn create_http_states(
ignore_certificate_errors: bool,
embedder_proxy: EmbedderProxy,
) -> (Arc<HttpState>, Arc<HttpState>) {
let mut hsts_list = HstsList::from_servo_preload();
let mut hsts_list = HstsList::default();
let mut auth_cache = AuthCache::default();
let http_cache = HttpCache::default();
let mut cookie_jar = CookieStorage::new(150);
@ -205,7 +209,7 @@ fn create_http_states(
let override_manager = CertificateErrorOverrideManager::new();
let private_http_state = HttpState {
hsts_list: RwLock::new(HstsList::from_servo_preload()),
hsts_list: RwLock::new(HstsList::default()),
cookie_jar: RwLock::new(CookieStorage::new(150)),
auth_cache: RwLock::new(AuthCache::default()),
history_states: RwLock::new(HashMap::new()),
@ -284,6 +288,18 @@ impl ResourceChannelManager {
perform_memory_report(|ops| {
let mut reports = public_http_state.memory_reports("public", ops);
reports.extend(private_http_state.memory_reports("private", ops));
reports.extend(vec![
Report {
path: path!["hsts-preload-list"],
kind: ReportKind::ExplicitJemallocHeapSize,
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));
})
}
@ -437,9 +453,6 @@ impl ResourceChannelManager {
history_states.remove(&history_state);
}
},
CoreResourceMsg::Synchronize(sender) => {
let _ = sender.send(());
},
CoreResourceMsg::ClearCache => {
http_state.http_cache.write().unwrap().clear();
},

View file

@ -8,7 +8,12 @@ use std::path::PathBuf;
use std::thread;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use malloc_size_of::MallocSizeOf;
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 crate::resource_thread;
@ -16,17 +21,26 @@ use crate::resource_thread;
const QUOTA_SIZE_LIMIT: usize = 5 * 1024 * 1024;
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> {
/// 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 chan2 = chan.clone();
thread::Builder::new()
.name("StorageManager".to_owned())
.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");
chan
@ -83,6 +97,10 @@ impl StorageManager {
self.clear(sender, url, storage_type);
self.save_state()
},
StorageThreadMsg::CollectMemoryReport(sender) => {
let reports = self.collect_memory_reports();
sender.send(ProcessReports::new(reports));
},
StorageThreadMsg::Exit(sender) => {
// Nothing to do since we save localstorage set eagerly.
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) {
if let Some(ref config_dir) = self.config_dir {
resource_thread::write_json_to_file(&self.local_data, config_dir, "local_data.json");

View file

@ -3,32 +3,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::HashMap;
use std::num::NonZeroU64;
use std::time::Duration as StdDuration;
use base::cross_process_instant::CrossProcessInstant;
use net::hsts::{HstsEntry, HstsList};
use base64::Engine;
use net::hsts::{HstsEntry, HstsList, HstsPreloadList};
use net_traits::IncludeSubdomains;
use time::Duration;
#[test]
fn test_hsts_entry_is_not_expired_when_it_has_no_timestamp() {
fn test_hsts_entry_is_not_expired_when_it_has_no_expires_at() {
let entry = HstsEntry {
host: "mozilla.org".to_owned(),
host: "example.com".to_owned(),
include_subdomains: false,
max_age: Some(StdDuration::from_secs(20)),
timestamp: None,
};
assert!(!entry.is_expired());
}
#[test]
fn test_hsts_entry_is_not_expired_when_it_has_no_max_age() {
let entry = HstsEntry {
host: "mozilla.org".to_owned(),
include_subdomains: false,
max_age: None,
timestamp: Some(CrossProcessInstant::now()),
expires_at: None,
};
assert!(!entry.is_expired());
@ -37,10 +24,9 @@ fn test_hsts_entry_is_not_expired_when_it_has_no_max_age() {
#[test]
fn test_hsts_entry_is_expired_when_it_has_reached_its_max_age() {
let entry = HstsEntry {
host: "mozilla.org".to_owned(),
host: "example.com".to_owned(),
include_subdomains: false,
max_age: Some(StdDuration::from_secs(10)),
timestamp: Some(CrossProcessInstant::now() - Duration::seconds(20)),
expires_at: Some(NonZeroU64::new(1).unwrap()),
};
assert!(entry.is_expired());
@ -74,7 +60,7 @@ fn test_base_domain_in_entries_map() {
list.push(
HstsEntry::new(
"servo.mozilla.org".to_owned(),
"servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -82,7 +68,7 @@ fn test_base_domain_in_entries_map() {
);
list.push(
HstsEntry::new(
"firefox.mozilla.org".to_owned(),
"firefox.example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -90,7 +76,7 @@ fn test_base_domain_in_entries_map() {
);
list.push(
HstsEntry::new(
"bugzilla.org".to_owned(),
"example.org".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -98,17 +84,17 @@ fn test_base_domain_in_entries_map() {
);
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]
fn test_push_entry_with_0_max_age_evicts_entry_from_list() {
fn test_push_entry_with_0_max_age_is_not_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
"example.com".to_owned(),
vec![
HstsEntry::new(
"mozilla.org".to_owned(),
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
Some(StdDuration::from_secs(500000)),
)
@ -121,22 +107,52 @@ fn test_push_entry_with_0_max_age_evicts_entry_from_list() {
list.push(
HstsEntry::new(
"mozilla.org".to_owned(),
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
Some(StdDuration::ZERO),
)
.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() {
let mut entries_map = HashMap::new();
entries_map.insert(
"example.com".to_owned(),
vec![
HstsEntry::new(
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
Some(StdDuration::from_secs(500000)),
)
.unwrap(),
],
);
let mut list = HstsList {
entries_map: entries_map,
};
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1);
list.push(
HstsEntry::new(
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
Some(StdDuration::ZERO),
)
.unwrap(),
);
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 0);
}
#[test]
fn test_push_entry_to_hsts_list_should_not_add_subdomains_whose_superdomain_is_already_matched() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()],
"example.com".to_owned(),
vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let mut list = HstsList {
entries_map: entries_map,
@ -144,49 +160,24 @@ fn test_push_entry_to_hsts_list_should_not_add_subdomains_whose_superdomain_is_a
list.push(
HstsEntry::new(
"servo.mozilla.org".to_owned(),
"servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
);
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1)
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1)
}
#[test]
fn test_push_entry_to_hsts_list_should_update_existing_domain_entrys_include_subdomains() {
fn test_push_entry_to_hsts_list_should_add_subdomains_whose_superdomain_doesnt_include() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let mut list = HstsList {
entries_map: entries_map,
};
assert!(list.is_host_secure("servo.mozilla.org"));
list.push(
HstsEntry::new(
"mozilla.org".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
);
assert!(!list.is_host_secure("servo.mozilla.org"))
}
#[test]
fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
"example.com".to_owned(),
vec![
HstsEntry::new(
"mozilla.org".to_owned(),
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -199,14 +190,69 @@ fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
list.push(
HstsEntry::new(
"mozilla.org".to_owned(),
"servo.example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
);
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1)
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 2)
}
#[test]
fn test_push_entry_to_hsts_list_should_update_existing_domain_entrys_include_subdomains() {
let mut entries_map = HashMap::new();
entries_map.insert(
"example.com".to_owned(),
vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let mut list = HstsList {
entries_map: entries_map,
};
assert!(list.is_host_secure("servo.example.com"));
list.push(
HstsEntry::new(
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
);
assert!(!list.is_host_secure("servo.example.com"))
}
#[test]
fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
let mut entries_map = HashMap::new();
entries_map.insert(
"example.com".to_owned(),
vec![
HstsEntry::new(
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
],
);
let mut list = HstsList {
entries_map: entries_map,
};
list.push(
HstsEntry::new(
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
.unwrap(),
);
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1)
}
#[test]
@ -215,16 +261,14 @@ fn test_push_multiple_entrie_to_hsts_list_should_add_them_all() {
entries_map: HashMap::new(),
};
assert!(!list.is_host_secure("mozilla.org"));
assert!(!list.is_host_secure("bugzilla.org"));
assert!(!list.is_host_secure("example.com"));
assert!(!list.is_host_secure("example.org"));
list.push(HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap());
list.push(
HstsEntry::new("bugzilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap(),
);
list.push(HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap());
list.push(HstsEntry::new("example.org".to_owned(), IncludeSubdomains::Included, None).unwrap());
assert!(list.is_host_secure("mozilla.org"));
assert!(list.is_host_secure("bugzilla.org"));
assert!(list.is_host_secure("example.com"));
assert!(list.is_host_secure("example.org"));
}
#[test]
@ -233,47 +277,35 @@ fn test_push_entry_to_hsts_list_should_add_an_entry() {
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]
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!(
HstsList::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!(
HstsList::from_preload(mock_preload_content).is_none(),
HstsPreloadList::from_preload(mock_preload_content).is_none(),
"invalid preload list should not have parsed"
)
}
#[test]
fn test_parse_hsts_preload_should_decode_host_and_includes_subdomains() {
let mock_preload_content = "{\
\"entries\": [\
{\"host\": \"mozilla.org\",\
\"include_subdomains\": false}\
]\
}";
let hsts_list = HstsList::from_preload(mock_preload_content);
let entries_map = hsts_list.unwrap().entries_map;
// Generated with `fst map --sorted` on a csv of "example.com,0\nexample.org,3"
let mock_preload_content = base64::engine::general_purpose::STANDARD
.decode("AwAAAAAAAAAAAAAAAAAAAAAQkMQAEJfHAwABBW9jEQLNws/J0MXqwgIAAAAAAAAAJwAAAAAAAADVOFe6")
.unwrap();
let hsts_list = HstsPreloadList::from_preload(mock_preload_content).unwrap();
assert_eq!(
entries_map.get("mozilla.org").unwrap()[0].host,
"mozilla.org"
);
assert!(!entries_map.get("mozilla.org").unwrap()[0].include_subdomains);
assert_eq!(hsts_list.is_host_secure("derp"), false);
assert_eq!(hsts_list.is_host_secure("example.com"), true);
assert_eq!(hsts_list.is_host_secure("servo.example.com"), false);
assert_eq!(hsts_list.is_host_secure("example.org"), true);
assert_eq!(hsts_list.is_host_secure("servo.example.org"), true);
}
#[test]
@ -282,17 +314,17 @@ fn test_hsts_list_with_no_entries_map_does_not_is_host_secure() {
entries_map: HashMap::new(),
};
assert!(!hsts_list.is_host_secure("mozilla.org"));
assert!(!hsts_list.is_host_secure("example.com"));
}
#[test]
fn test_hsts_list_with_exact_domain_entry_is_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
"example.com".to_owned(),
vec![
HstsEntry::new(
"mozilla.org".to_owned(),
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -304,31 +336,31 @@ fn test_hsts_list_with_exact_domain_entry_is_is_host_secure() {
entries_map: entries_map,
};
assert!(hsts_list.is_host_secure("mozilla.org"));
assert!(hsts_list.is_host_secure("example.com"));
}
#[test]
fn test_hsts_list_with_subdomain_when_include_subdomains_is_true_is_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()],
"example.com".to_owned(),
vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let hsts_list = HstsList {
entries_map: entries_map,
};
assert!(hsts_list.is_host_secure("servo.mozilla.org"));
assert!(hsts_list.is_host_secure("servo.example.com"));
}
#[test]
fn test_hsts_list_with_subdomain_when_include_subdomains_is_false_is_not_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
"example.com".to_owned(),
vec![
HstsEntry::new(
"mozilla.org".to_owned(),
"example.com".to_owned(),
IncludeSubdomains::NotIncluded,
None,
)
@ -339,58 +371,57 @@ fn test_hsts_list_with_subdomain_when_include_subdomains_is_false_is_not_is_host
entries_map: entries_map,
};
assert!(!hsts_list.is_host_secure("servo.mozilla.org"));
assert!(!hsts_list.is_host_secure("servo.example.com"));
}
#[test]
fn test_hsts_list_with_subdomain_when_host_is_not_a_subdomain_is_not_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()],
"example.com".to_owned(),
vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let hsts_list = HstsList {
entries_map: entries_map,
};
assert!(!hsts_list.is_host_secure("servo-mozilla.org"));
assert!(!hsts_list.is_host_secure("servo-example.com"));
}
#[test]
fn test_hsts_list_with_subdomain_when_host_is_exact_match_is_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
vec![HstsEntry::new("mozilla.org".to_owned(), IncludeSubdomains::Included, None).unwrap()],
"example.com".to_owned(),
vec![HstsEntry::new("example.com".to_owned(), IncludeSubdomains::Included, None).unwrap()],
);
let hsts_list = HstsList {
entries_map: entries_map,
};
assert!(hsts_list.is_host_secure("mozilla.org"));
assert!(hsts_list.is_host_secure("example.com"));
}
#[test]
fn test_hsts_list_with_expired_entry_is_not_is_host_secure() {
let mut entries_map = HashMap::new();
entries_map.insert(
"mozilla.org".to_owned(),
"example.com".to_owned(),
vec![HstsEntry {
host: "mozilla.org".to_owned(),
host: "example.com".to_owned(),
include_subdomains: false,
max_age: Some(StdDuration::from_secs(20)),
timestamp: Some(CrossProcessInstant::now() - Duration::seconds(100)),
expires_at: Some(NonZeroU64::new(1).unwrap()),
}],
);
let hsts_list = HstsList {
entries_map: entries_map,
};
assert!(!hsts_list.is_host_secure("mozilla.org"));
assert!(!hsts_list.is_host_secure("example.com"));
}
#[test]
fn test_preload_hsts_domains_well_formed() {
let hsts_list = HstsList::from_servo_preload();
assert!(!hsts_list.entries_map.is_empty());
let hsts_list = HstsPreloadList::from_servo_preload();
assert_ne!(hsts_list.0.len(), 0);
}

View file

@ -4,6 +4,7 @@
use std::borrow::Cow;
use std::io::Cursor;
use std::ops::Range;
use std::time::Duration;
use std::{cmp, fmt, vec};
@ -120,48 +121,65 @@ pub enum CorsStatus {
}
#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
pub struct Image {
pub width: u32,
pub height: u32,
pub struct RasterImage {
pub metadata: ImageMetadata,
pub format: PixelFormat,
pub id: Option<ImageKey>,
pub cors_status: CorsStatus,
pub bytes: IpcSharedMemory,
pub frames: Vec<ImageFrame>,
}
#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
pub struct ImageFrame {
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 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 {
self.frames.len() > 1
}
pub fn bytes(&self) -> IpcSharedMemory {
self.frames
.first()
.expect("Should have at least one frame")
.bytes
.clone()
pub fn frames(&self) -> impl Iterator<Item = ImageFrameView> {
self.frames.iter().map(|frame| ImageFrameView {
delay: frame.delay,
bytes: self.bytes.get(frame.byte_range.clone()).unwrap(),
width: frame.width,
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 {
write!(
f,
"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 width: u32,
pub height: u32,
@ -170,7 +188,7 @@ pub struct ImageMetadata {
// FIXME: Images must not be copied every frame. Instead we should atomically
// 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() {
return None;
}
@ -189,15 +207,18 @@ pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Image>
rgba8_byte_swap_colors_inplace(&mut rgba);
let frame = ImageFrame {
delay: None,
bytes: IpcSharedMemory::from_bytes(&rgba),
byte_range: 0..rgba.len(),
width: rgba.width(),
height: rgba.height(),
};
Some(Image {
width: rgba.width(),
height: rgba.height(),
Some(RasterImage {
metadata: ImageMetadata {
width: rgba.width(),
height: rgba.height(),
},
format: PixelFormat::BGRA8,
frames: vec![frame],
bytes: IpcSharedMemory::from_bytes(&rgba),
id: None,
cors_status,
})
@ -354,7 +375,7 @@ fn is_webp(buffer: &[u8]) -> bool {
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 {
return None;
};
@ -364,45 +385,60 @@ fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> {
// This uses `map_while`, because the first non-decodable frame seems to
// send the frame iterator into an infinite loop. See
// <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
.into_frames()
.map_while(|decoded_frame| {
let mut frame = match decoded_frame {
let mut gif_frame = match decoded_frame {
Ok(decoded_frame) => decoded_frame,
Err(error) => {
debug!("decode GIF frame error: {error}");
return None;
},
};
rgba8_byte_swap_colors_inplace(frame.buffer_mut());
let frame = ImageFrame {
bytes: IpcSharedMemory::from_bytes(frame.buffer()),
delay: Some(Duration::from(frame.delay())),
width: frame.buffer().width(),
height: frame.buffer().height(),
};
rgba8_byte_swap_colors_inplace(gif_frame.buffer_mut());
let frame_start = total_number_of_bytes;
total_number_of_bytes += gif_frame.buffer().len();
// The image size should be at least as large as the largest frame.
width = cmp::max(width, frame.width);
height = cmp::max(height, frame.height);
let frame_width = gif_frame.buffer().width();
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)
})
.collect();
if frames.is_empty() {
debug!("Animated Image decoding error");
None
} else {
Some(Image {
width,
height,
cors_status,
frames,
id: None,
format: PixelFormat::BGRA8,
})
return None;
}
// Coalesce the frame data into one single shared memory region.
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,
frames,
id: None,
format: PixelFormat::BGRA8,
bytes: IpcSharedMemory::from_bytes(&bytes),
})
}
#[cfg(test)]

View file

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

View file

@ -12,9 +12,9 @@ use ipc_channel::ipc::{self, IpcReceiver};
use ipc_channel::router::ROUTER;
use log::debug;
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;
@ -100,28 +100,18 @@ impl Profiler {
ProfilerMsg::Report(sender) => {
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();
// Turn the pid -> reports map into a vector and add the
// hint to find the main process.
let json_reports: Vec<JsonReport> = reports
let results: Vec<MemoryReport> = reports
.into_iter()
.map(|(pid, reports)| JsonReport {
.map(|(pid, reports)| MemoryReport {
pid,
reports,
is_main_process: pid == main_pid,
})
.collect();
let content = serde_json::to_string(&json_reports)
.unwrap_or_else(|_| "{ error: \"failed to create memory report\"}".to_owned());
let _ = sender.send(MemoryReportResult { content });
let _ = sender.send(MemoryReportResult { results });
true
},

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>>;
// back end of the profiler that handles data aggregation and performance metrics
@ -276,7 +255,7 @@ impl Profiler {
writeln!(
file,
"{}\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),
mean.as_seconds_f64() * 1000.,
median.as_seconds_f64() * 1000.,
@ -319,7 +298,7 @@ impl Profiler {
writeln!(
&mut lock,
"{:-35}{} {:15.4} {:15.4} {:15.4} {:15.4} {:15}",
category.format(&self.output),
category.variant_name(),
meta.format(&self.output),
mean.as_seconds_f64() * 1000.,
median.as_seconds_f64() * 1000.,

View file

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

View file

@ -76,6 +76,10 @@ impl Animations {
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
// specified `current_timeline_value`. Returns true if animations were marked
// dirty or false otherwise.

View file

@ -17,7 +17,7 @@ use cssparser::color::clamp_unit_f32;
use cssparser::{Parser, ParserInput};
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
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::request::CorsSettings;
use pixels::PixelFormat;
@ -54,6 +54,7 @@ use crate::dom::dommatrix::DOMMatrix;
use crate::dom::element::{Element, cors_setting_for_element};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::htmlvideoelement::HTMLVideoElement;
use crate::dom::imagedata::ImageData;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::offscreencanvas::OffscreenCanvas;
@ -310,14 +311,15 @@ impl CanvasState {
self.origin_clean.set(false)
}
// https://html.spec.whatwg.org/multipage/#the-image-argument-is-not-origin-clean
fn is_origin_clean(&self, image: CanvasImageSource) -> bool {
match image {
CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(),
CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(),
/// <https://html.spec.whatwg.org/multipage/#the-image-argument-is-not-origin-clean>
fn is_origin_clean(&self, source: CanvasImageSource) -> bool {
match source {
CanvasImageSource::HTMLImageElement(image) => {
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,
}
}
@ -328,7 +330,15 @@ impl CanvasState {
cors_setting: Option<CorsSettings>,
) -> Option<snapshot::Snapshot> {
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::None |
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 {
PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA,
PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA,
@ -350,7 +360,7 @@ impl CanvasState {
size.cast(),
format,
alpha_mode,
img.bytes(),
IpcSharedMemory::from_bytes(img.first_frame().bytes),
))
}
@ -430,6 +440,17 @@ impl CanvasState {
}
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) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
@ -490,6 +511,52 @@ impl CanvasState {
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)]
fn draw_offscreen_canvas(
&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(
&self,
global: &GlobalScope,
@ -968,7 +1035,7 @@ impl CanvasState {
) -> Fallible<Option<DomRoot<CanvasPattern>>> {
let snapshot = match 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()? {
return Ok(None);
}
@ -980,10 +1047,28 @@ impl CanvasState {
})
.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) => {
// <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)?
},
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)?
},
CanvasImageSource::CSSStyleValue(ref value) => value

View file

@ -0,0 +1,71 @@
/* 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 dom_struct::dom_struct;
use js::jsapi::Heap;
use js::jsval::JSVal;
use js::rust::{HandleObject, MutableHandleValue};
use crate::dom::bindings::codegen::Bindings::AbortSignalBinding::AbortSignalMethods;
use crate::dom::bindings::reflector::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/#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>,
}
impl AbortSignal {
#[allow(dead_code)]
fn new_inherited() -> AbortSignal {
AbortSignal {
eventtarget: EventTarget::new_inherited(),
abort_reason: 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,
)
}
}
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};
#[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};
#[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};

View file

@ -11,7 +11,7 @@ use crate::DomTypes;
use crate::dom::bindings::conversions::DerivedFrom;
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::realms::InRealm;
use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc;
/// Create the reflector for a new DOM object and yield ownership to the
@ -42,7 +42,16 @@ where
}
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>;
/// 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>;
}
@ -51,7 +60,8 @@ impl<T: DomGlobalGeneric<crate::DomTypeHolder>> DomGlobal for T {
<Self as DomGlobalGeneric<crate::DomTypeHolder>>::global_(self, realm)
}
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 base::id::{
BlobId, DomExceptionId, DomPointId, Index, MessagePortId, NamespaceIndex, PipelineNamespaceId,
BlobId, DomExceptionId, DomPointId, ImageBitmapId, Index, MessagePortId, NamespaceIndex,
PipelineNamespaceId,
};
use constellation_traits::{
BlobImpl, DomException, DomPoint, MessagePortImpl, Serializable as SerializableInterface,
StructuredSerializedData, Transferrable as TransferrableInterface,
SerializableImageBitmap, StructuredSerializedData, Transferrable as TransferrableInterface,
TransformStreamData,
};
use js::gc::RootedVec;
use js::glue::{
@ -42,6 +44,7 @@ use crate::dom::blob::Blob;
use crate::dom::dompoint::DOMPoint;
use crate::dom::dompointreadonly::DOMPointReadOnly;
use crate::dom::globalscope::GlobalScope;
use crate::dom::imagebitmap::ImageBitmap;
use crate::dom::messageport::MessagePort;
use crate::dom::readablestream::ReadableStream;
use crate::dom::types::{DOMException, TransformStream};
@ -66,6 +69,7 @@ pub(super) enum StructuredCloneTags {
DomException = 0xFFFF8007,
WritableStream = 0xFFFF8008,
TransformStream = 0xFFFF8009,
ImageBitmap = 0xFFFF800A,
Max = 0xFFFFFFFF,
}
@ -76,6 +80,7 @@ impl From<SerializableInterface> for StructuredCloneTags {
SerializableInterface::DomPointReadOnly => StructuredCloneTags::DomPointReadOnly,
SerializableInterface::DomPoint => StructuredCloneTags::DomPoint,
SerializableInterface::DomException => StructuredCloneTags::DomException,
SerializableInterface::ImageBitmap => StructuredCloneTags::ImageBitmap,
}
}
}
@ -83,6 +88,7 @@ impl From<SerializableInterface> for StructuredCloneTags {
impl From<TransferrableInterface> for StructuredCloneTags {
fn from(v: TransferrableInterface) -> Self {
match v {
TransferrableInterface::ImageBitmap => StructuredCloneTags::ImageBitmap,
TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort,
TransferrableInterface::ReadableStream => StructuredCloneTags::ReadableStream,
TransferrableInterface::WritableStream => StructuredCloneTags::WritableStream,
@ -104,6 +110,7 @@ fn reader_for_type(
SerializableInterface::DomPointReadOnly => read_object::<DOMPointReadOnly>,
SerializableInterface::DomPoint => read_object::<DOMPoint>,
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::DomPoint => try_serialize::<DOMPoint>,
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<(), ()>
{
match val {
TransferrableInterface::ImageBitmap => receive_object::<ImageBitmap>,
TransferrableInterface::MessagePort => receive_object::<MessagePort>,
TransferrableInterface::ReadableStream => receive_object::<ReadableStream>,
TransferrableInterface::WritableStream => receive_object::<WritableStream>,
@ -390,6 +399,7 @@ type TransferOperation = unsafe fn(
fn transfer_for_type(val: TransferrableInterface) -> TransferOperation {
match val {
TransferrableInterface::ImageBitmap => try_transfer::<ImageBitmap>,
TransferrableInterface::MessagePort => try_transfer::<MessagePort>,
TransferrableInterface::ReadableStream => try_transfer::<ReadableStream>,
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))
}
match transferable {
TransferrableInterface::ImageBitmap => can_transfer::<ImageBitmap>(obj, cx),
TransferrableInterface::MessagePort => can_transfer::<MessagePort>(obj, cx),
TransferrableInterface::ReadableStream => can_transfer::<ReadableStream>(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,
/// to produce the DOM ports stored in `message_ports` above.
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,
/// used as part of the "deserialize" steps of blobs,
/// 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>>,
/// A map of serialized exceptions.
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.
@ -535,12 +552,18 @@ pub(crate) struct StructuredDataWriter {
pub(crate) errors: DOMErrorRecord,
/// Transferred ports.
pub(crate) ports: Option<HashMap<MessagePortId, MessagePortImpl>>,
/// Transferred transform streams.
pub(crate) transform_streams_port: Option<HashMap<MessagePortId, TransformStreamData>>,
/// Serialized points.
pub(crate) points: Option<HashMap<DomPointId, DomPoint>>,
/// Serialized exceptions.
pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>,
/// Serialized blobs.
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.
@ -591,9 +614,12 @@ pub(crate) fn write(
let data = StructuredSerializedData {
serialized: data,
ports: sc_writer.ports.take(),
transform_streams: sc_writer.transform_streams_port.take(),
points: sc_writer.points.take(),
exceptions: sc_writer.exceptions.take(),
blobs: sc_writer.blobs.take(),
image_bitmaps: sc_writer.image_bitmaps.take(),
transferred_image_bitmaps: sc_writer.transferred_image_bitmaps.take(),
};
Ok(data)
@ -613,10 +639,13 @@ pub(crate) fn read(
let mut sc_reader = StructuredDataReader {
roots,
port_impls: data.ports.take(),
transform_streams_port_impls: data.transform_streams.take(),
blob_impls: data.blobs.take(),
points: data.points.take(),
exceptions: data.exceptions.take(),
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 _;
unsafe {

View file

@ -12,11 +12,10 @@ use dom_struct::dom_struct;
use encoding_rs::UTF_8;
use js::jsapi::JSObject;
use js::rust::HandleObject;
use js::typedarray::Uint8;
use js::typedarray::{ArrayBufferU8, Uint8};
use net_traits::filemanager_thread::RelativePos;
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::codegen::Bindings::BlobBinding;
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)
}
// https://w3c.github.io/FileAPI/#text-method-algo
/// <https://w3c.github.io/FileAPI/#text-method-algo>
fn Text(&self, can_gc: CanGc) -> Rc<Promise> {
let global = self.global();
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
fn ArrayBuffer(&self, 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);
fn ArrayBuffer(&self, in_realm: InRealm, can_gc: CanGc) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
let global = GlobalScope::from_safe_context(cx, in_realm);
let promise = Promise::new_in_current_realm(in_realm, can_gc);
let id = self.get_blob_url_id();
// 1. Let stream be the result of calling get stream on this.
let stream = self.get_stream(can_gc);
global.read_file_async(
id,
p.clone(),
Box::new(|promise, bytes| {
match bytes {
Ok(b) => {
let cx = GlobalScope::get_cx();
let result = run_array_buffer_data_algorithm(cx, b, 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;
},
};
match result {
Ok(FetchedData::ArrayBuffer(a)) => {
promise.resolve_native(&a, CanGc::note())
},
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>

View file

@ -5,6 +5,7 @@
use canvas_traits::canvas::{Canvas2dMsg, CanvasId, CanvasMsg, FromScriptMsg};
use dom_struct::dom_struct;
use euclid::default::Size2D;
use ipc_channel::ipc::IpcSender;
use profile_traits::ipc;
use script_bindings::inheritance::Castable;
use servo_url::ServoUrl;
@ -36,27 +37,50 @@ use crate::dom::path2d::Path2D;
use crate::dom::textmetrics::TextMetrics;
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
#[dom_struct]
pub(crate) struct CanvasRenderingContext2D {
reflector_: Reflector,
canvas: HTMLCanvasElementOrOffscreenCanvas,
canvas_state: CanvasState,
droppable: DroppableCanvasRenderingContext2D,
}
impl CanvasRenderingContext2D {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_inherited(
global: &GlobalScope,
canvas: HTMLCanvasElementOrOffscreenCanvas,
size: Size2D<u32>,
) -> 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 {
reflector_: Reflector::new(),
canvas,
canvas_state: CanvasState::new(
global,
Size2D::new(size.width as u64, size.height as u64),
),
canvas_state,
droppable: DroppableCanvasRenderingContext2D {
ipc_sender,
canvas_id,
},
}
}
@ -689,15 +713,3 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
.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 js::rust::{HandleObject, MutableHandleValue};
use net_traits::image_cache::Image;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::DataTransferBinding::DataTransferMethods;
@ -155,7 +156,10 @@ impl DataTransferMethods<crate::DomTypeHolder> for DataTransfer {
// Step 3
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

@ -2384,6 +2384,9 @@ impl Document {
let mut cancel_state = event.get_cancel_state();
// 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 &&
is_character_value_key(&(keyboard_event.key)) &&
!keyboard_event.is_composing &&
@ -5074,6 +5077,35 @@ impl Document {
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>
pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8]) {
// 1. If document's will declaratively refresh is true, then return.
@ -6770,6 +6802,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)]
pub(crate) enum AnimationFrameCallback {
DevtoolsFramerateTick {

View file

@ -66,7 +66,7 @@ use xml5ever::serialize::TraversalScope::{
use crate::conversions::Convert;
use crate::dom::activation::Activatable;
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::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::{
@ -1480,7 +1480,7 @@ impl Element {
// 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
// "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| {
if attr.namespace() != &ns!(xmlns) {
return false;
@ -1493,7 +1493,8 @@ impl Element {
_ => false,
}
})
});
})
.ok();
if let Some(attr) = attr {
return (**attr.value()).into();

View file

@ -74,6 +74,7 @@ use super::bindings::codegen::Bindings::WebGPUBinding::GPUDeviceLostReason;
use super::bindings::error::Fallible;
use super::bindings::trace::{HashMapTracedValues, RootedTraceableBox};
use super::serviceworkerglobalscope::ServiceWorkerGlobalScope;
use super::transformstream::CrossRealmTransform;
use crate::dom::bindings::cell::{DomRefCell, RefMut};
use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::BroadcastChannelMethods;
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,
/// so it can be removed.
explicitly_closed: bool,
/// Note: it may seem strange to use a pair of options, versus for example an enum.
/// But it looks like tranform streams will require both of those in their transfer.
/// This will be resolved when we reach that point of the implementation.
/// <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>,
/// The handler for `message` or `messageerror` used in the cross realm transform,
/// if any was setup with this port.
cross_realm_transform: Option<CrossRealmTransform>,
}
/// 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");
};
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>
@ -1368,7 +1367,9 @@ impl GlobalScope {
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
@ -1380,8 +1381,7 @@ impl GlobalScope {
can_gc: CanGc,
) {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut cross_realm_transform_readable = None);
rooted!(in(*cx) let mut cross_realm_transform_writable = None);
rooted!(in(*cx) let mut cross_realm_transform = None);
let should_dispatch = if let MessagePortState::Managed(_id, message_ports) =
&mut *self.message_port_state.borrow_mut()
@ -1399,10 +1399,7 @@ impl GlobalScope {
let to_dispatch = port_impl.handle_incoming(task).map(|to_dispatch| {
(DomRoot::from_ref(&*managed_port.dom_port), to_dispatch)
});
cross_realm_transform_readable
.set(managed_port.cross_realm_transform_readable.clone());
cross_realm_transform_writable
.set(managed_port.cross_realm_transform_writable.clone());
cross_realm_transform.set(managed_port.cross_realm_transform.clone());
to_dispatch
} else {
panic!("managed-port has no port-impl.");
@ -1429,10 +1426,6 @@ impl GlobalScope {
// Re-ordered because we need to pass it to `structuredclone::read`.
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 comp = InRealm::Entered(&realm);
@ -1447,26 +1440,28 @@ impl GlobalScope {
// if any, maintaining their relative order.
// Note: both done in `structuredclone::read`.
if let Ok(ports) = structuredclone::read(self, data, message_clone.handle_mut()) {
// Add a handler for ports message event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
if let Some(transform) = cross_realm_transform_readable.as_ref() {
transform.handle_message(
cx,
self,
&dom_port,
message_clone.handle(),
comp,
can_gc,
);
}
// Add a handler for ports message event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
if let Some(transform) = cross_realm_transform_writable.as_ref() {
transform.handle_message(cx, self, message_clone.handle(), comp, can_gc);
}
if !has_cross_realm_tansform {
// 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:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
CrossRealmTransform::Readable(readable) => {
readable.handle_message(
cx,
self,
&dom_port,
message_clone.handle(),
comp,
can_gc,
);
},
// Add a handler for ports message event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
CrossRealmTransform::Writable(writable) => {
writable.handle_message(cx, self, message_clone.handle(), comp, can_gc);
},
}
} else {
// Fire an event named message at messageEventTarget,
// using MessageEvent,
// with the data attribute initialized to messageClone
@ -1481,25 +1476,24 @@ impl GlobalScope {
can_gc,
);
}
} else if let Some(transform) = cross_realm_transform.as_ref() {
match transform {
// Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
CrossRealmTransform::Readable(readable) => {
readable.handle_error(cx, self, &dom_port, comp, can_gc);
},
// Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
CrossRealmTransform::Writable(writable) => {
writable.handle_error(cx, self, &dom_port, comp, can_gc);
},
}
} else {
// Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable>
if let Some(transform) = cross_realm_transform_readable.as_ref() {
transform.handle_error(cx, self, &dom_port, comp, can_gc);
}
// Add a handler for ports messageerror event with the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable>
if let Some(transform) = cross_realm_transform_writable.as_ref() {
transform.handle_error(cx, self, &dom_port, comp, can_gc);
}
if !has_cross_realm_tansform {
// If this throws an exception, catch it,
// fire an event named messageerror at messageEventTarget,
// using MessageEvent, and then return.
MessageEvent::dispatch_error(message_event_target, self, can_gc);
}
// If this throws an exception, catch it,
// fire an event named messageerror at messageEventTarget,
// using MessageEvent, and then return.
MessageEvent::dispatch_error(message_event_target, self, can_gc);
}
}
}
@ -1689,8 +1683,7 @@ impl GlobalScope {
dom_port: Dom::from_ref(dom_port),
pending: true,
explicitly_closed: false,
cross_realm_transform_readable: None,
cross_realm_transform_writable: None,
cross_realm_transform: None,
},
);
@ -1713,8 +1706,7 @@ impl GlobalScope {
dom_port: Dom::from_ref(dom_port),
pending: false,
explicitly_closed: false,
cross_realm_transform_readable: None,
cross_realm_transform_writable: None,
cross_realm_transform: None,
},
);
let _ = self.script_to_constellation_chan().send(
@ -2991,10 +2983,7 @@ impl GlobalScope {
}
if let Some(snapshot) = canvas.get_image_data() {
let size = snapshot.size().cast();
let image_bitmap =
ImageBitmap::new(self, size.width, size.height, can_gc).unwrap();
image_bitmap.set_bitmap_data(snapshot.to_vec());
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc);
}
@ -3008,10 +2997,7 @@ impl GlobalScope {
}
if let Some(snapshot) = canvas.get_image_data() {
let size = snapshot.size().cast();
let image_bitmap =
ImageBitmap::new(self, size.width, size.height, can_gc).unwrap();
image_bitmap.set_bitmap_data(snapshot.to_vec());
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc);
}

View file

@ -5,14 +5,15 @@
use std::cell::Cell;
use std::str::{self, FromStr};
use data_url::mime::Mime as DataUrlMime;
use dom_struct::dom_struct;
use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue};
use js::rust::HandleObject;
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::trim_http_whitespace;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods};
@ -33,7 +34,7 @@ pub(crate) struct Headers {
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)]
pub(crate) enum Guard {
Immutable,
@ -66,7 +67,7 @@ impl Headers {
}
impl HeadersMethods<crate::DomTypeHolder> for Headers {
// https://fetch.spec.whatwg.org/#dom-headers
/// <https://fetch.spec.whatwg.org/#dom-headers>
fn Constructor(
global: &GlobalScope,
proto: Option<HandleObject>,
@ -78,47 +79,41 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
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 {
// Step 1
let value = normalize_value(value);
// 1. Normalize value.
let value = trim_http_whitespace(&value);
// Step 2
// https://fetch.spec.whatwg.org/#headers-validate
let (mut valid_name, valid_value) = validate_name_and_value(name, value)?;
// 2. If validating (name, value) for headers 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();
if self.guard.get() == Guard::Immutable {
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
// 3. If headerss guard is "request-no-cors":
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) =
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(valid_value.clone());
value.extend(valid_value.to_vec());
value
} 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) {
return Ok(());
}
}
// Step 4
// 4. Append (name, value) to headerss header list.
match HeaderValue::from_bytes(&valid_value) {
Ok(value) => {
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 {
self.remove_privileged_no_cors_request_headers();
}
@ -142,50 +137,53 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
Ok(())
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
/// <https://fetch.spec.whatwg.org/#dom-headers-delete>
fn Delete(&self, name: ByteString) -> ErrorResult {
// Step 1
let (mut valid_name, valid_value) = validate_name_and_value(name, ByteString::new(vec![]))?;
// Step 1 If validating (name, ``) for this returns false, then return.
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();
// Step 2
if self.guard.get() == Guard::Immutable {
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
// Step 2 If thiss guard is "request-no-cors", name is not a no-CORS-safelisted request-header name,
// and name is not a privileged no-CORS request-header name, then return.
if self.guard.get() == Guard::RequestNoCors &&
!is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec())
{
return Ok(());
}
// Step 5
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
return Ok(());
// 3. If thiss header list does not contain name, then return.
// 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(())
}
// https://fetch.spec.whatwg.org/#dom-headers-get
/// <https://fetch.spec.whatwg.org/#dom-headers-get>
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)?;
// 2. Return the result of getting name from thiss header list.
Ok(
get_value_from_header_list(&valid_name, &self.header_list.borrow())
.map(ByteString::new),
)
}
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
/// <https://fetch.spec.whatwg.org/#dom-headers-getsetcookie>
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
.borrow()
.get_all("set-cookie")
@ -194,42 +192,36 @@ impl HeadersMethods<crate::DomTypeHolder> for Headers {
.collect()
}
// https://fetch.spec.whatwg.org/#dom-headers-has
/// <https://fetch.spec.whatwg.org/#dom-headers-has>
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)?;
// Step 2
// 2. Return true if thiss header list contains name; otherwise false.
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<()> {
// Step 1
let value = normalize_value(value);
// Step 2
let (mut valid_name, valid_value) = validate_name_and_value(name, value)?;
// 1. Normalize value
let value = trim_http_whitespace(&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();
// Step 3
if self.guard.get() == Guard::Immutable {
return Err(Error::Type("Guard is immutable".to_string()));
}
// Step 4
if self.guard.get() == Guard::Request &&
is_forbidden_request_header(&valid_name, &valid_value)
{
return Ok(());
}
// Step 5
// 3. If thiss guard is "request-no-cors" and (name, value) is not a
// no-CORS-safelisted request-header, then return.
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(());
}
// Step 6
if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
return Ok(());
}
// Step 7
// 4. Set (name, value) in thiss header list.
// https://fetch.spec.whatwg.org/#concept-header-list-set
match HeaderValue::from_bytes(&valid_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(())
}
}
@ -260,7 +258,7 @@ impl Headers {
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 {
match filler {
Some(HeadersInit::ByteStringSequenceSequence(v)) => {
@ -316,12 +314,12 @@ impl Headers {
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> {
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>)> {
let borrowed_header_list = self.header_list.borrow();
let mut header_vec = vec![];
@ -341,11 +339,38 @@ impl Headers {
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) {
// 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");
}
/// <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 {
@ -391,6 +416,7 @@ pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
"keep-alive",
"origin",
"referer",
"set-cookie",
"te",
"trailer",
"transfer-encoding",
@ -448,26 +474,11 @@ pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
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 {
matches!(name, "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()))
// 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")
}
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.
// 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
/// <http://tools.ietf.org/html/rfc7230#section-3.2>
fn is_field_name(name: &ByteString) -> bool {
is_token(name)
}
// https://fetch.spec.whatg.org/#concept-header-value
fn is_legal_header_value(value: &ByteString) -> bool {
// 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.
/// <https://fetch.spec.whatg.org/#concept-header-value>
fn is_legal_header_value(value: &[u8]) -> bool {
let value_len = value.len();
if value_len == 0 {
return true;
@ -533,7 +517,7 @@ fn is_legal_header_value(value: &ByteString) -> bool {
b' ' | b'\t' => return false,
_ => {},
};
for &ch in &value[..] {
for &ch in value {
match ch {
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 {
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 {
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),
},
Polygon {
/// Stored as a flat array of coordinates
/// e.g. [x1, y1, x2, y2, x3, y3] for a triangle
points: Vec<f32>,
},
}
@ -203,8 +205,28 @@ impl Area {
p.y >= top_left.1
},
//TODO polygon hit_test
_ => false,
Area::Polygon { ref points } => {
// 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

@ -27,11 +27,12 @@ use servo_media::streams::registry::MediaStreamId;
use snapshot::Snapshot;
use style::attr::AttrValue;
use super::node::NodeDamage;
pub(crate) use crate::canvas_context::*;
use crate::conversions::Convert;
use crate::dom::attr::Attr;
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::{
BlobCallback, HTMLCanvasElementMethods, RenderingContext as RootedRenderingContext,
};
@ -225,7 +226,7 @@ impl LayoutHTMLCanvasElementHelpers for LayoutDom<'_, HTMLCanvasElement> {
impl HTMLCanvasElement {
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>> {
@ -687,8 +688,11 @@ impl VirtualMethods for HTMLCanvasElement {
.unwrap()
.attribute_mutated(attr, mutation, can_gc);
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 js::rust::HandleObject;
use net_traits::ReferrerPolicy;
use net_traits::request::Destination;
use profile_traits::ipc as ProfiledIpc;
use script_traits::{NewLayoutInfo, UpdatePipelineIdReason};
use servo_url::ServoUrl;
@ -222,6 +223,7 @@ impl HTMLIFrameElement {
old_pipeline_id,
sandbox: sandboxed,
viewport_details,
theme: window.theme(),
};
window
.as_global_scope()
@ -237,6 +239,7 @@ impl HTMLIFrameElement {
opener: None,
load_data,
viewport_details,
theme: window.theme(),
};
self.pipeline_id.set(Some(new_pipeline_id));
@ -249,6 +252,7 @@ impl HTMLIFrameElement {
old_pipeline_id,
sandbox: sandboxed,
viewport_details,
theme: window.theme(),
};
window
.as_global_scope()
@ -282,6 +286,7 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_or_current_origin(),
);
load_data.destination = Destination::IFrame;
load_data.policy_container = Some(window.as_global_scope().policy_container());
let element = self.upcast::<Element>();
load_data.srcdoc = String::from(element.get_string_attribute(&local_name!("srcdoc")));
@ -375,6 +380,8 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()),
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();
// If the initial `about:blank` page is the current page, load with replacement enabled,
@ -382,10 +389,6 @@ impl HTMLIFrameElement {
let is_about_blank =
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 {
NavigationHistoryBehavior::Replace
} else {
@ -425,6 +428,7 @@ impl HTMLIFrameElement {
Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_or_current_origin(),
);
load_data.destination = Destination::IFrame;
load_data.policy_container = Some(window.as_global_scope().policy_container());
let browsing_context_id = BrowsingContextId::new();
let webview_id = window.window_proxy().webview_id();

View file

@ -20,8 +20,8 @@ use js::rust::HandleObject;
use mime::{self, Mime};
use net_traits::http_status::HttpStatus;
use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse,
PendingImageId, UsePlaceholder,
Image, ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable,
ImageResponse, PendingImageId, UsePlaceholder,
};
use net_traits::request::{Destination, Initiator, RequestId};
use net_traits::{
@ -29,7 +29,7 @@ use net_traits::{
ResourceFetchTiming, ResourceTimingType,
};
use num_traits::ToPrimitive;
use pixels::{CorsStatus, Image, ImageMetadata};
use pixels::{CorsStatus, ImageMetadata};
use servo_url::ServoUrl;
use servo_url::origin::MutableOrigin;
use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_integer, parse_length};
@ -146,9 +146,8 @@ struct ImageRequest {
parsed_url: Option<ServoUrl>,
source_url: Option<USVString>,
blocker: DomRefCell<Option<LoadBlocker>>,
#[conditional_malloc_size_of]
#[no_trace]
image: Option<Arc<Image>>,
image: Option<Image>,
#[no_trace]
metadata: Option<ImageMetadata>,
#[no_trace]
@ -177,7 +176,8 @@ impl HTMLImageElement {
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 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);
}
}
@ -191,7 +191,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()
}
}
@ -341,10 +341,12 @@ impl HTMLImageElement {
is_placeholder,
}) => {
if is_placeholder {
self.process_image_response(
ImageResponse::PlaceholderLoaded(image, url),
can_gc,
)
if let Some(raster_image) = image.as_raster_image() {
self.process_image_response(
ImageResponse::PlaceholderLoaded(raster_image, url),
can_gc,
)
}
} else {
self.process_image_response(ImageResponse::Loaded(image, url), can_gc)
}
@ -403,7 +405,7 @@ impl HTMLImageElement {
window
.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) {
@ -448,11 +450,8 @@ impl HTMLImageElement {
}
// Steps common to when an image has been loaded.
fn handle_loaded_image(&self, image: Arc<Image>, url: ServoUrl, can_gc: CanGc) {
self.current_request.borrow_mut().metadata = Some(ImageMetadata {
height: image.height,
width: image.width,
});
fn handle_loaded_image(&self, image: Image, url: ServoUrl, can_gc: CanGc) {
self.current_request.borrow_mut().metadata = Some(image.metadata());
self.current_request.borrow_mut().final_url = Some(url);
self.current_request.borrow_mut().image = Some(image);
self.current_request.borrow_mut().state = State::CompletelyAvailable;
@ -471,7 +470,7 @@ impl HTMLImageElement {
(true, false)
},
(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)
},
(ImageResponse::Loaded(image, url), ImageRequestPhase::Pending) => {
@ -483,7 +482,7 @@ impl HTMLImageElement {
(ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Pending) => {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
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)
},
(ImageResponse::MetadataLoaded(meta), ImageRequestPhase::Current) => {
@ -536,11 +535,15 @@ impl HTMLImageElement {
can_gc: CanGc,
) {
match image {
ImageResponse::Loaded(image, url) | ImageResponse::PlaceholderLoaded(image, url) => {
self.pending_request.borrow_mut().metadata = Some(ImageMetadata {
height: image.height,
width: image.width,
});
ImageResponse::Loaded(image, url) => {
self.pending_request.borrow_mut().metadata = Some(image.metadata());
self.pending_request.borrow_mut().final_url = Some(url);
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().image = Some(image);
self.finish_reacting_to_environment_change(src, generation, selected_pixel_density);
@ -1020,10 +1023,7 @@ impl HTMLImageElement {
// set on this element.
self.generation.set(self.generation.get() + 1);
// Step 6.3
let metadata = ImageMetadata {
height: image.height,
width: image.width,
};
let metadata = image.metadata();
// Step 6.3.2 abort requests
self.abort_request(
State::CompletelyAvailable,
@ -1033,7 +1033,7 @@ impl HTMLImageElement {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
let mut current_request = self.current_request.borrow_mut();
current_request.final_url = Some(img_url.clone());
current_request.image = Some(image.clone());
current_request.image = Some(image);
current_request.metadata = Some(metadata);
// Step 6.3.6
current_request.current_pixel_density = pixel_density;
@ -1360,7 +1360,7 @@ impl HTMLImageElement {
pub(crate) fn same_origin(&self, origin: &MutableOrigin) -> bool {
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
@ -1432,7 +1432,7 @@ impl MicrotaskRunnable for ImageElementMicrotask {
pub(crate) trait LayoutHTMLImageElementHelpers {
fn image_url(self) -> Option<ServoUrl>;
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_height(self) -> LengthOrPercentageOrAuto;
}
@ -1449,12 +1449,9 @@ impl LayoutHTMLImageElementHelpers for LayoutDom<'_, HTMLImageElement> {
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();
(
current_request.image.clone(),
current_request.metadata.clone(),
)
(current_request.image.clone(), current_request.metadata)
}
fn image_density(self) -> Option<f64> {

View file

@ -12,9 +12,13 @@ use std::str::FromStr;
use std::{f64, ptr};
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 euclid::{Point2D, Rect, Size2D};
use html5ever::{LocalName, Prefix, local_name, ns};
use ipc_channel::ipc;
use js::jsapi::{
ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject,
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::filemanager_thread::FileManagerThreadMsg;
use net_traits::{CoreResourceMsg, IpcSend};
use profile_traits::ipc;
use script_bindings::codegen::GenericBindings::ShadowRootBinding::{
ShadowRootMode, SlotAssignmentMode,
};
use style::attr::AttrValue;
use style::str::{split_commas, str_join};
use stylo_atoms::Atom;
@ -33,12 +39,12 @@ use stylo_dom::ElementState;
use time::{Month, OffsetDateTime, Time};
use unicode_bidi::{BidiClass, bidi_class};
use url::Url;
use webrender_api::units::DeviceIntRect;
use super::bindings::str::{FromInputValueString, ToInputValueString};
use crate::clipboard_provider::EmbedderClipboardProvider;
use crate::dom::activation::Activatable;
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::EventBinding::EventMethods;
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::inheritance::Castable;
use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, FromInputValueString, ToInputValueString, USVString};
use crate::dom::clipboardevent::ClipboardEvent;
use crate::dom::compositionevent::CompositionEvent;
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::eventtarget::EventTarget;
use crate::dom::file::File;
use crate::dom::filelist::{FileList, LayoutFileListHelpers};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmldatalistelement::HTMLDataListElement;
use crate::dom::htmldivelement::HTMLDivElement;
use crate::dom::htmlelement::HTMLElement;
use crate::dom::htmlfieldsetelement::HTMLFieldSetElement;
use crate::dom::htmlformelement::{
FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom,
SubmittedFrom,
};
use crate::dom::htmlstyleelement::HTMLStyleElement;
use crate::dom::keyboardevent::KeyboardEvent;
use crate::dom::mouseevent::MouseEvent;
use crate::dom::node::{
BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext,
};
use crate::dom::nodelist::NodeList;
use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot};
use crate::dom::textcontrol::{TextControlElement, TextControlSelection};
use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
use crate::dom::validitystate::{ValidationFlags, ValidityState};
@ -92,6 +101,34 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
const PASSWORD_REPLACEMENT_CHAR: char = '●';
const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
#[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>,
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[non_exhaustive]
enum ShadowTree {
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>
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
#[allow(dead_code)]
@ -172,8 +209,7 @@ impl InputType {
fn is_textual(&self) -> bool {
matches!(
*self,
InputType::Color |
InputType::Date |
InputType::Date |
InputType::DatetimeLocal |
InputType::Email |
InputType::Hidden |
@ -277,9 +313,16 @@ impl From<&Atom> for InputType {
#[derive(Debug, PartialEq)]
enum ValueMode {
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-value>
Value,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default>
Default,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default-on>
DefaultOn,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-filename>
Filename,
}
@ -314,6 +357,7 @@ pub(crate) struct HTMLInputElement {
form_owner: MutNullableDom<HTMLFormElement>,
labels_node_list: MutNullableDom<NodeList>,
validity_state: MutNullableDom<ValidityState>,
shadow_tree: DomRefCell<Option<ShadowTree>>,
}
#[derive(JSTraceable)]
@ -372,6 +416,7 @@ impl HTMLInputElement {
form_owner: Default::default(),
labels_node_list: MutNullableDom::new(None),
validity_state: Default::default(),
shadow_tree: Default::default(),
}
}
@ -475,6 +520,7 @@ impl HTMLInputElement {
let mut value = textinput.single_line_content().clone();
self.sanitize_value(&mut value);
textinput.set_content(value);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
fn does_minmaxlength_apply(&self) -> bool {
@ -803,7 +849,7 @@ impl HTMLInputElement {
.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 {
match self.input_type() {
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing
@ -958,9 +1004,9 @@ impl HTMLInputElement {
failed_flags
}
// 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-a-step-mismatch
/// * <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-a-step-mismatch>
fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags {
if value.is_empty() || !self.does_value_as_number_apply() {
return ValidationFlags::empty();
@ -1014,9 +1060,109 @@ impl HTMLInputElement {
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,
false,
SlotAssignmentMode::Manual,
can_gc,
)
.expect("Attaching UA shadow root failed")
})
}
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()?;
let ShadowTree::Color(color_tree) = shadow_tree;
Some(color_tree)
})
.ok()
.expect("UA shadow tree was not created")
}
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
if self.input_type() == 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> {
/// Return a string that represents the contents of the element for layout.
fn value_for_layout(self) -> Cow<'dom, str>;
fn size_for_layout(self) -> u32;
fn selection_for_layout(self) -> Option<Range<usize>>;
@ -1075,16 +1221,15 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
Some(filelist) => {
let length = filelist.len();
if length == 0 {
return DEFAULT_FILE_INPUT_VALUE.into();
}
if length == 1 {
DEFAULT_FILE_INPUT_VALUE.into()
} else if length == 1 {
match filelist.file_for_layout(0) {
Some(file) => return file.name().to_string().into(),
None => return DEFAULT_FILE_INPUT_VALUE.into(),
Some(file) => file.name().to_string().into(),
None => DEFAULT_FILE_INPUT_VALUE.into(),
}
} else {
format!("{} files", length).into()
}
format!("{} files", length).into()
},
None => DEFAULT_FILE_INPUT_VALUE.into(),
}
@ -1103,6 +1248,9 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
self.placeholder().into()
}
},
InputType::Color => {
unreachable!("Input type color is explicitly not rendered as text");
},
_ => {
let text = self.get_raw_textinput_value();
if !text.is_empty() {
@ -1178,11 +1326,11 @@ impl TextControlElement for HTMLInputElement {
InputType::Week |
InputType::Time |
InputType::DatetimeLocal |
InputType::Number |
InputType::Color => true,
InputType::Number => true,
InputType::Button |
InputType::Checkbox |
InputType::Color |
InputType::File |
InputType::Hidden |
InputType::Image |
@ -1257,7 +1405,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
// https://html.spec.whatwg.org/multipage/#dom-input-checked
fn SetChecked(&self, checked: bool) {
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
@ -1349,7 +1497,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);
Ok(())
}
@ -1724,18 +1872,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
fn in_same_group(
other: &HTMLInputElement,
@ -1905,7 +2041,7 @@ impl HTMLInputElement {
InputType::Radio | InputType::Checkbox => {
self.update_checked_state(self.DefaultChecked(), false);
self.checked_changed.set(false);
update_related_validity_states(self, can_gc);
self.value_changed(can_gc);
},
InputType::Image => (),
_ => (),
@ -1949,8 +2085,9 @@ impl HTMLInputElement {
.collect()
});
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel");
let (chan, recv) =
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel");
let msg =
FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths);
resource_threads
@ -1977,8 +2114,9 @@ impl HTMLInputElement {
None => None,
};
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel");
let (chan, recv) =
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
.expect("Error initializing channel");
let msg =
FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path);
resource_threads
@ -2348,6 +2486,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 {
@ -2359,6 +2561,7 @@ impl VirtualMethods for HTMLInputElement {
self.super_type()
.unwrap()
.attribute_mutated(attr, mutation, can_gc);
match *attr.local_name() {
local_name!("disabled") => {
let disabled_state = match mutation {
@ -2466,6 +2669,7 @@ impl VirtualMethods for HTMLInputElement {
let mut value = textinput.single_line_content().clone();
self.sanitize_value(&mut value);
textinput.set_content(value);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
// Steps 7-9
if !previously_selectable && self.selection_api_applies() {
@ -2493,6 +2697,8 @@ impl VirtualMethods for HTMLInputElement {
self.sanitize_value(&mut value);
self.textinput.borrow_mut().set_content(value);
self.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
},
local_name!("name") if self.input_type() == InputType::Radio => {
self.radio_group_updated(
@ -2553,7 +2759,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 {
@ -2584,7 +2790,8 @@ impl VirtualMethods for HTMLInputElement {
if self.input_type() == InputType::Radio {
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) {
@ -2676,6 +2883,17 @@ impl VirtualMethods for HTMLInputElement {
self.implicit_submission(can_gc);
},
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.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
@ -2692,17 +2910,9 @@ impl VirtualMethods for HTMLInputElement {
!event.DefaultPrevented() &&
self.input_type().is_textual_or_password()
{
if event.IsTrusted() {
self.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
// keypress should be deprecated and replaced by beforeinput.
// keypress was supposed to fire "blur" and "focus" events
// but already done in `document.rs`
} else if (event.type_() == atom!("compositionstart") ||
event.type_() == atom!("compositionupdate") ||
event.type_() == atom!("compositionend")) &&
@ -2730,7 +2940,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
@ -2752,7 +2962,7 @@ impl VirtualMethods for HTMLInputElement {
elem.textinput
.borrow_mut()
.set_content(self.textinput.borrow().get_content());
update_related_validity_states(self, can_gc);
self.value_changed(can_gc);
}
}
@ -2861,7 +3071,8 @@ impl Activatable for HTMLInputElement {
},
// 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
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,
}
}
@ -2907,7 +3118,7 @@ impl Activatable for HTMLInputElement {
};
if activation_state.is_some() {
update_related_validity_states(self, can_gc);
self.value_changed(can_gc);
}
activation_state
@ -2966,7 +3177,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>
@ -3028,6 +3239,10 @@ impl Activatable for HTMLInputElement {
},
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior
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

@ -24,10 +24,10 @@ use js::jsapi::JSAutoRealm;
use media::{GLPlayerMsg, GLPlayerMsgForward, WindowGLContext};
use net_traits::request::{Destination, RequestId};
use net_traits::{
FetchMetadata, FetchResponseListener, Metadata, NetworkError, ResourceFetchTiming,
ResourceTimingType,
FetchMetadata, FetchResponseListener, FilteredMetadata, Metadata, NetworkError,
ResourceFetchTiming, ResourceTimingType,
};
use pixels::Image;
use pixels::RasterImage;
use script_bindings::codegen::GenericBindings::TimeRangesBinding::TimeRangesMethods;
use script_bindings::codegen::InheritTypes::{
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 {
self.current_frame = Some(MediaFrame {
image_key,
width: image.width as i32,
height: image.height as i32,
width: image.metadata.width as i32,
height: image.metadata.height as i32,
});
self.show_poster = true;
}
@ -1358,18 +1358,17 @@ impl HTMLMediaElement {
}
/// <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() {
return;
}
// 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
.lock()
.unwrap()
.render_poster_frame(image);
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
if pref!(media_testing_enabled) {
self.owner_global()
@ -1618,7 +1617,6 @@ impl HTMLMediaElement {
// TODO: 6. Abort the overall resource selection algorithm.
},
PlayerEvent::VideoFrameUpdated => {
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
// Check if the frame was resized
if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
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) {
self.handle_resize(None, 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>) {
if let Some(video_elem) = self.downcast::<HTMLVideoElement>() {
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]
@ -2656,6 +2676,8 @@ pub(crate) struct HTMLMediaElementFetchContext {
cancel_reason: Option<CancelReason>,
/// Indicates whether the fetched stream is seekable.
is_seekable: bool,
/// Indicates whether the fetched stream is origin clean.
origin_clean: bool,
/// Fetch canceller. Allows cancelling the current fetch request by
/// manually calling its .cancel() method or automatically on Drop.
fetch_canceller: FetchCanceller,
@ -2666,6 +2688,7 @@ impl HTMLMediaElementFetchContext {
HTMLMediaElementFetchContext {
cancel_reason: None,
is_seekable: false,
origin_clean: true,
fetch_canceller: FetchCanceller::new(request_id),
}
}
@ -2678,6 +2701,14 @@ impl HTMLMediaElementFetchContext {
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) {
if self.cancel_reason.is_some() {
return;
@ -2732,6 +2763,16 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
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 {
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,

View file

@ -7,7 +7,7 @@ use std::default::Default;
use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject;
use pixels::Image;
use pixels::RasterImage;
use servo_arc::Arc;
use crate::dom::attr::Attr;
@ -31,7 +31,7 @@ pub(crate) struct HTMLObjectElement {
htmlelement: HTMLElement,
#[ignore_malloc_size_of = "Arc"]
#[no_trace]
image: DomRefCell<Option<Arc<Image>>>,
image: DomRefCell<Option<Arc<RasterImage>>>,
form_owner: MutNullableDom<HTMLFormElement>,
validity_state: MutNullableDom<ValidityState>,
}

View file

@ -152,25 +152,6 @@ impl HTMLOptionElement {
}
}
// FIXME(ajeffrey): Provide a way of buffering DOMStrings other than using Strings
fn collect_text(element: &Element, value: &mut String) {
let svg_script =
*element.namespace() == ns!(svg) && element.local_name() == &local_name!("script");
let html_script = element.is::<HTMLScriptElement>();
if svg_script || html_script {
return;
}
for child in element.upcast::<Node>().children() {
if child.is::<Text>() {
let characterdata = child.downcast::<CharacterData>().unwrap();
value.push_str(&characterdata.Data());
} else if let Some(element_child) = child.downcast() {
collect_text(element_child, value);
}
}
}
impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
/// <https://html.spec.whatwg.org/multipage/#dom-option>
fn Option(
@ -216,8 +197,28 @@ impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
/// <https://html.spec.whatwg.org/multipage/#dom-option-text>
fn Text(&self) -> DOMString {
let mut content = String::new();
collect_text(self.upcast(), &mut content);
let mut content = DOMString::new();
let mut iterator = self.upcast::<Node>().traverse_preorder(ShadowIncluding::No);
while let Some(node) = iterator.peek() {
if let Some(element) = node.downcast::<Element>() {
let html_script = element.is::<HTMLScriptElement>();
let svg_script = *element.namespace() == ns!(svg) &&
element.local_name() == &local_name!("script");
if html_script || svg_script {
iterator.next_skipping_children();
continue;
}
}
if node.is::<Text>() {
let characterdata = node.downcast::<CharacterData>().unwrap();
content.push_str(&characterdata.Data());
}
iterator.next();
}
DOMString::from(str_join(split_html_space_chars(&content), " "))
}

View file

@ -14,7 +14,7 @@ use style::attr::AttrValue;
use stylo_dom::ElementState;
use embedder_traits::{SelectElementOptionOrOptgroup, SelectElementOption};
use euclid::{Size2D, Point2D, Rect};
use embedder_traits::EmbedderMsg;
use embedder_traits::{FormControl as EmbedderFormControl, EmbedderMsg};
use crate::dom::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods;
use crate::dom::activation::Activatable;
@ -406,12 +406,10 @@ impl HTMLSelectElement {
let selected_index = self.list_of_options().position(|option| option.Selected());
let document = self.owner_document();
document.send_to_embedder(EmbedderMsg::ShowSelectElementMenu(
document.send_to_embedder(EmbedderMsg::ShowFormControl(
document.webview_id(),
options,
selected_index,
DeviceIntRect::from_untyped(&rect.to_box2d()),
ipc_sender,
EmbedderFormControl::SelectElement(options, selected_index, ipc_sender),
));
let Ok(response) = ipc_receiver.recv() else {

View file

@ -9,6 +9,9 @@ use style::attr::{AttrValue, LengthOrPercentageOrAuto};
use style::color::AbsoluteColor;
use style::context::QuirksMode;
use super::attr::Attr;
use super::element::AttributeMutation;
use super::node::NodeDamage;
use crate::dom::bindings::codegen::Bindings::HTMLTableCellElementBinding::HTMLTableCellElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::inheritance::Castable;
@ -174,6 +177,19 @@ impl VirtualMethods for HTMLTableCellElement {
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
if let Some(super_type) = self.super_type() {
super_type.attribute_mutated(attr, mutation, can_gc);
}
if matches!(*attr.local_name(), local_name!("colspan")) {
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
if matches!(*attr.local_name(), local_name!("rowspan")) {
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
}
fn parse_plain_attribute(&self, local_name: &LocalName, value: DOMString) -> AttrValue {
match *local_name {
local_name!("colspan") => {

View file

@ -7,8 +7,10 @@ use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject;
use style::attr::{AttrValue, LengthOrPercentageOrAuto};
use super::attr::Attr;
use super::bindings::root::LayoutDom;
use super::element::Element;
use super::element::{AttributeMutation, Element};
use super::node::NodeDamage;
use crate::dom::bindings::codegen::Bindings::HTMLTableColElementBinding::HTMLTableColElementMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::DomRoot;
@ -93,6 +95,16 @@ impl VirtualMethods for HTMLTableColElement {
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
if let Some(super_type) = self.super_type() {
super_type.attribute_mutated(attr, mutation, can_gc);
}
if matches!(*attr.local_name(), local_name!("span")) {
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
}
fn parse_plain_attribute(&self, local_name: &LocalName, value: DOMString) -> AttrValue {
match *local_name {
local_name!("span") => {

View file

@ -643,6 +643,17 @@ impl VirtualMethods for HTMLTextAreaElement {
match action {
KeyReaction::TriggerDefaultAction => (),
KeyReaction::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.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
@ -656,17 +667,9 @@ impl VirtualMethods for HTMLTextAreaElement {
}
}
} else if event.type_() == atom!("keypress") && !event.DefaultPrevented() {
if event.IsTrusted() {
self.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
// keypress should be deprecated and replaced by beforeinput.
// keypress was supposed to fire "blur" and "focus" events
// but already done in `document.rs`
} else if event.type_() == atom!("compositionstart") ||
event.type_() == atom!("compositionupdate") ||
event.type_() == atom!("compositionend")

View file

@ -9,10 +9,9 @@ use content_security_policy as csp;
use dom_struct::dom_struct;
use euclid::default::Size2D;
use html5ever::{LocalName, Prefix, local_name, ns};
use ipc_channel::ipc;
use js::rust::HandleObject;
use net_traits::image_cache::{
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse,
ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable, ImageResponse,
PendingImageId, UsePlaceholder,
};
use net_traits::request::{CredentialsMode, Destination, RequestBuilder, RequestId};
@ -23,6 +22,7 @@ use net_traits::{
use script_layout_interface::{HTMLMediaData, MediaMetadata};
use servo_media::player::video::VideoFrame;
use servo_url::ServoUrl;
use snapshot::Snapshot;
use style::attr::{AttrValue, LengthOrPercentageOrAuto};
use crate::document_loader::{LoadBlocker, LoadType};
@ -133,9 +133,7 @@ impl HTMLVideoElement {
sent_resize
}
pub(crate) fn get_current_frame_data(
&self,
) -> Option<(Option<ipc::IpcSharedMemory>, Size2D<u32>)> {
pub(crate) fn get_current_frame_data(&self) -> Option<Snapshot> {
let frame = self.htmlmediaelement.get_current_frame();
if frame.is_some() {
*self.last_frame.borrow_mut() = frame;
@ -145,11 +143,19 @@ impl HTMLVideoElement {
Some(frame) => {
let size = Size2D::new(frame.get_width() as u32, frame.get_height() as u32);
if !frame.is_gl_texture() {
let data = Some(ipc::IpcSharedMemory::from_bytes(&frame.get_data()));
Some((data, size))
let alpha_mode = snapshot::AlphaMode::Transparent {
premultiplied: false,
};
Some(Snapshot::from_vec(
size.cast(),
snapshot::PixelFormat::BGRA,
alpha_mode,
frame.get_data().to_vec(),
))
} else {
// XXX(victor): here we only have the GL texture ID.
Some((None, size))
Some(Snapshot::cleared(size.cast()))
}
},
None => None,
@ -217,7 +223,7 @@ impl HTMLVideoElement {
element.process_image_response(response.response, CanGc::note());
});
image_cache.add_listener(ImageResponder::new(sender, window.pipeline_id(), id));
image_cache.add_listener(ImageLoadListener::new(sender, window.pipeline_id(), id));
}
/// <https://html.spec.whatwg.org/multipage/#poster-frame>
@ -262,7 +268,10 @@ impl HTMLVideoElement {
match response {
ImageResponse::Loaded(image, url) => {
debug!("Loaded poster image for video element: {:?}", url);
self.htmlmediaelement.process_poster_image_loaded(image);
match image.as_raster_image() {
Some(image) => self.htmlmediaelement.process_poster_image_loaded(image),
None => warn!("Vector images are not yet supported in video poster"),
}
LoadBlocker::terminate(&self.load_blocker, can_gc);
},
ImageResponse::MetadataLoaded(..) => {},
@ -273,6 +282,18 @@ impl HTMLVideoElement {
},
}
}
/// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
pub(crate) fn is_usable(&self) -> bool {
!matches!(
self.htmlmediaelement.get_ready_state(),
ReadyState::HaveNothing | ReadyState::HaveMetadata
)
}
pub(crate) fn origin_is_clean(&self) -> bool {
self.htmlmediaelement.origin_is_clean()
}
}
impl HTMLVideoElementMethods<crate::DomTypeHolder> for HTMLVideoElement {

View file

@ -2,58 +2,64 @@
* 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::vec::Vec;
use std::cell::{Cell, Ref};
use std::collections::HashMap;
use base::id::{ImageBitmapId, ImageBitmapIndex};
use constellation_traits::SerializableImageBitmap;
use dom_struct::dom_struct;
use snapshot::Snapshot;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::ImageBitmapMethods;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::serializable::Serializable;
use crate::dom::bindings::structuredclone::StructuredData;
use crate::dom::bindings::transferable::Transferable;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct ImageBitmap {
reflector_: Reflector,
width: u32,
height: u32,
/// The actual pixel data of the bitmap
///
/// If this is `None`, then the bitmap data has been released by calling
/// [`close`](https://html.spec.whatwg.org/multipage/#dom-imagebitmap-close)
bitmap_data: DomRefCell<Option<Vec<u8>>>,
#[no_trace]
bitmap_data: DomRefCell<Option<Snapshot>>,
origin_clean: Cell<bool>,
}
impl ImageBitmap {
fn new_inherited(width_arg: u32, height_arg: u32) -> ImageBitmap {
fn new_inherited(bitmap_data: Snapshot) -> ImageBitmap {
ImageBitmap {
reflector_: Reflector::new(),
width: width_arg,
height: height_arg,
bitmap_data: DomRefCell::new(Some(vec![])),
bitmap_data: DomRefCell::new(Some(bitmap_data)),
origin_clean: Cell::new(true),
}
}
#[allow(dead_code)]
pub(crate) fn new(
global: &GlobalScope,
width: u32,
height: u32,
bitmap_data: Snapshot,
can_gc: CanGc,
) -> Fallible<DomRoot<ImageBitmap>> {
//assigning to a variable the return object of new_inherited
let imagebitmap = Box::new(ImageBitmap::new_inherited(width, height));
Ok(reflect_dom_object(imagebitmap, global, can_gc))
) -> DomRoot<ImageBitmap> {
reflect_dom_object(
Box::new(ImageBitmap::new_inherited(bitmap_data)),
global,
can_gc,
)
}
pub(crate) fn set_bitmap_data(&self, data: Vec<u8>) {
*self.bitmap_data.borrow_mut() = Some(data);
#[allow(dead_code)]
pub(crate) fn bitmap_data(&self) -> Ref<Option<Snapshot>> {
self.bitmap_data.borrow()
}
pub(crate) fn origin_is_clean(&self) -> bool {
self.origin_clean.get()
}
pub(crate) fn set_origin_clean(&self, origin_is_clean: bool) {
@ -67,6 +73,108 @@ impl ImageBitmap {
}
}
impl Serializable for ImageBitmap {
type Index = ImageBitmapIndex;
type Data = SerializableImageBitmap;
/// <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:serialization-steps>
fn serialize(&self) -> Result<(ImageBitmapId, Self::Data), ()> {
// Step 1. If value's origin-clean flag is not set, then throw a "DataCloneError" DOMException.
if !self.origin_is_clean() {
return Err(());
}
// If value has a [[Detached]] internal slot whose value is true,
// then throw a "DataCloneError" DOMException.
if self.is_detached() {
return Err(());
}
// Step 2. Set serialized.[[BitmapData]] to a copy of value's bitmap data.
let serialized = SerializableImageBitmap {
bitmap_data: self.bitmap_data.borrow().clone().unwrap(),
};
Ok((ImageBitmapId::new(), serialized))
}
/// <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:deserialization-steps>
fn deserialize(
owner: &GlobalScope,
serialized: Self::Data,
can_gc: CanGc,
) -> Result<DomRoot<Self>, ()> {
// Step 1. Set value's bitmap data to serialized.[[BitmapData]].
Ok(ImageBitmap::new(owner, serialized.bitmap_data, can_gc))
}
fn serialized_storage<'a>(
reader: StructuredData<'a, '_>,
) -> &'a mut Option<HashMap<ImageBitmapId, Self::Data>> {
match reader {
StructuredData::Reader(r) => &mut r.image_bitmaps,
StructuredData::Writer(w) => &mut w.image_bitmaps,
}
}
}
impl Transferable for ImageBitmap {
type Index = ImageBitmapIndex;
type Data = SerializableImageBitmap;
fn can_transfer(&self) -> bool {
if !self.origin_is_clean() || self.is_detached() {
return false;
}
true
}
/// <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:transfer-steps>
fn transfer(&self) -> Result<(ImageBitmapId, SerializableImageBitmap), ()> {
// Step 1. If value's origin-clean flag is not set, then throw a "DataCloneError" DOMException.
if !self.origin_is_clean() {
return Err(());
}
// If value has a [[Detached]] internal slot whose value is true,
// then throw a "DataCloneError" DOMException.
if self.is_detached() {
return Err(());
}
// Step 2. Set dataHolder.[[BitmapData]] to value's bitmap data.
// Step 3. Unset value's bitmap data.
let serialized = SerializableImageBitmap {
bitmap_data: self.bitmap_data.borrow_mut().take().unwrap(),
};
Ok((ImageBitmapId::new(), serialized))
}
/// <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:transfer-receiving-steps>
fn transfer_receive(
owner: &GlobalScope,
_: ImageBitmapId,
serialized: SerializableImageBitmap,
) -> Result<DomRoot<Self>, ()> {
// Step 1. Set value's bitmap data to serialized.[[BitmapData]].
Ok(ImageBitmap::new(
owner,
serialized.bitmap_data,
CanGc::note(),
))
}
fn serialized_storage<'a>(
data: StructuredData<'a, '_>,
) -> &'a mut Option<HashMap<ImageBitmapId, Self::Data>> {
match data {
StructuredData::Reader(r) => &mut r.transferred_image_bitmaps,
StructuredData::Writer(w) => &mut w.transferred_image_bitmaps,
}
}
}
impl ImageBitmapMethods<crate::DomTypeHolder> for ImageBitmap {
/// <https://html.spec.whatwg.org/multipage/#dom-imagebitmap-height>
fn Height(&self) -> u32 {
@ -76,7 +184,13 @@ impl ImageBitmapMethods<crate::DomTypeHolder> for ImageBitmap {
}
// Step 2. Return this's height, in CSS pixels.
self.height
self.bitmap_data
.borrow()
.as_ref()
.unwrap()
.size()
.cast()
.height
}
/// <https://html.spec.whatwg.org/multipage/#dom-imagebitmap-width>
@ -87,7 +201,13 @@ impl ImageBitmapMethods<crate::DomTypeHolder> for ImageBitmap {
}
// Step 2. Return this's width, in CSS pixels.
self.width
self.bitmap_data
.borrow()
.as_ref()
.unwrap()
.size()
.cast()
.width
}
/// <https://html.spec.whatwg.org/multipage/#dom-imagebitmap-close>

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