mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Merge branch 'main' into issue_36590
This commit is contained in:
commit
0e2a826a0c
2293 changed files with 53299 additions and 391712 deletions
18
.flake8
18
.flake8
|
@ -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
|
21
.github/workflows/ohos.yml
vendored
21
.github/workflows/ohos.yml
vendored
|
@ -169,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:
|
||||
|
@ -239,10 +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 --bencher -b "org.servo.servo" -p "https://www.servo.org" -n 5
|
||||
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 }}'
|
||||
bencher run --adapter json --file bench.json --project '${{ env.BENCHER_PROJECT }}' --token '${{ secrets.BENCHER_API_TOKEN }}' --github-actions '${{ secrets.GITHUB_TOKEN }}' --testbed="$MODEL_NAME"
|
||||
|
|
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
|
@ -16,6 +16,8 @@
|
|||
// IDL language support
|
||||
"mythmon.idl",
|
||||
// TOML files
|
||||
"tamasfe.even-better-toml"
|
||||
"tamasfe.even-better-toml",
|
||||
// Python files
|
||||
"charliermarsh.ruff"
|
||||
]
|
||||
}
|
627
Cargo.lock
generated
627
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
10
README.md
10
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -97,7 +97,10 @@ impl<'a> CanvasPaintThread<'a> {
|
|||
let canvas_data = canvas_paint_thread.create_canvas(size);
|
||||
creator.send(canvas_data).unwrap();
|
||||
},
|
||||
Ok(ConstellationCanvasMsg::Exit) => break,
|
||||
Ok(ConstellationCanvasMsg::Exit(exit_sender)) => {
|
||||
let _ = exit_sender.send(());
|
||||
break;
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Error on CanvasPaintThread receive ({})", e);
|
||||
break;
|
||||
|
|
|
@ -34,11 +34,11 @@ log = { workspace = true }
|
|||
net = { path = "../net" }
|
||||
pixels = { path = "../pixels" }
|
||||
profile_traits = { workspace = true }
|
||||
script_traits = { workspace = true }
|
||||
servo_allocator = { path = "../allocator" }
|
||||
servo_config = { path = "../config" }
|
||||
servo_geometry = { path = "../geometry" }
|
||||
stylo_traits = { workspace = true }
|
||||
timers = { path = "../timers" }
|
||||
tracing = { workspace = true, optional = true }
|
||||
webrender = { workspace = true }
|
||||
webrender_api = { workspace = true }
|
||||
|
|
|
@ -7,10 +7,9 @@ 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};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base::cross_process_instant::CrossProcessInstant;
|
||||
use base::id::{PipelineId, WebViewId};
|
||||
|
@ -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};
|
||||
|
@ -54,8 +53,9 @@ use webrender_api::{
|
|||
};
|
||||
|
||||
use crate::InitialCompositorState;
|
||||
use crate::refresh_driver::RefreshDriver;
|
||||
use crate::webview_manager::WebViewManager;
|
||||
use crate::webview_renderer::{UnknownWebView, WebViewRenderer};
|
||||
use crate::webview_renderer::{PinchZoomResult, UnknownWebView, WebViewRenderer};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum UnableToComposite {
|
||||
|
@ -87,6 +87,9 @@ pub enum WebRenderDebugOption {
|
|||
}
|
||||
/// Data that is shared by all WebView renderers.
|
||||
pub struct ServoRenderer {
|
||||
/// The [`RefreshDriver`] which manages the rythym of painting.
|
||||
refresh_driver: RefreshDriver,
|
||||
|
||||
/// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once
|
||||
/// all renderer operations become per-`WebView` this map can be removed, but we still sometimes
|
||||
/// need to work backwards to figure out what `WebView` is associated with a `Pipeline`.
|
||||
|
@ -152,18 +155,14 @@ pub struct IOCompositor {
|
|||
/// The number of frames pending to receive from WebRender.
|
||||
pending_frames: usize,
|
||||
|
||||
/// The [`Instant`] of the last animation tick, used to avoid flooding the Constellation and
|
||||
/// ScriptThread with a deluge of animation ticks.
|
||||
last_animation_tick: Instant,
|
||||
|
||||
/// A handle to the memory profiler which will automatically unregister
|
||||
/// when it's dropped.
|
||||
_mem_profiler_registration: ProfilerRegistration,
|
||||
}
|
||||
|
||||
/// Why we need to be repainted. This is used for debugging.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct RepaintReason(u8);
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
pub(crate) struct RepaintReason(u8);
|
||||
|
||||
bitflags! {
|
||||
impl RepaintReason: u8 {
|
||||
|
@ -387,6 +386,10 @@ impl IOCompositor {
|
|||
);
|
||||
let compositor = IOCompositor {
|
||||
global: Rc::new(RefCell::new(ServoRenderer {
|
||||
refresh_driver: RefreshDriver::new(
|
||||
state.constellation_chan.clone(),
|
||||
state.event_loop_waker,
|
||||
),
|
||||
shutdown_state: state.shutdown_state,
|
||||
pipeline_to_webview_map: Default::default(),
|
||||
compositor_receiver: state.receiver,
|
||||
|
@ -407,7 +410,6 @@ impl IOCompositor {
|
|||
webrender: Some(state.webrender),
|
||||
rendering_context: state.rendering_context,
|
||||
pending_frames: 0,
|
||||
last_animation_tick: Instant::now(),
|
||||
_mem_profiler_registration: registration,
|
||||
};
|
||||
|
||||
|
@ -451,7 +453,16 @@ impl IOCompositor {
|
|||
}
|
||||
|
||||
pub fn needs_repaint(&self) -> bool {
|
||||
!self.needs_repaint.get().is_empty()
|
||||
let repaint_reason = self.needs_repaint.get();
|
||||
if repaint_reason.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
!self
|
||||
.global
|
||||
.borrow()
|
||||
.refresh_driver
|
||||
.wait_to_paint(repaint_reason)
|
||||
}
|
||||
|
||||
pub fn finish_shutting_down(&mut self) {
|
||||
|
@ -520,15 +531,17 @@ impl IOCompositor {
|
|||
pipeline_id,
|
||||
animation_state,
|
||||
) => {
|
||||
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) {
|
||||
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if webview_renderer
|
||||
.change_pipeline_running_animations_state(pipeline_id, animation_state) &&
|
||||
webview_renderer.animating()
|
||||
.change_pipeline_running_animations_state(pipeline_id, animation_state)
|
||||
{
|
||||
// These operations should eventually happen per-WebView, but they are
|
||||
// global now as rendering is still global to all WebViews.
|
||||
self.process_animations(true);
|
||||
}
|
||||
self.global
|
||||
.borrow()
|
||||
.refresh_driver
|
||||
.notify_animation_state_changed(webview_renderer);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -573,14 +586,15 @@ impl IOCompositor {
|
|||
},
|
||||
|
||||
CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => {
|
||||
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) {
|
||||
if webview_renderer.set_throttled(pipeline_id, throttled) &&
|
||||
webview_renderer.animating()
|
||||
{
|
||||
// These operations should eventually happen per-WebView, but they are
|
||||
// global now as rendering is still global to all WebViews.
|
||||
self.process_animations(true);
|
||||
}
|
||||
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if webview_renderer.set_throttled(pipeline_id, throttled) {
|
||||
self.global
|
||||
.borrow()
|
||||
.refresh_driver
|
||||
.notify_animation_state_changed(webview_renderer);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -621,29 +635,37 @@ impl IOCompositor {
|
|||
}
|
||||
},
|
||||
|
||||
CompositorMsg::WebDriverMouseButtonEvent(webview_id, action, button, x, y) => {
|
||||
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,
|
||||
CompositorMsg::WebDriverMouseButtonEvent(
|
||||
webview_id,
|
||||
action,
|
||||
button,
|
||||
}));
|
||||
},
|
||||
|
||||
CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y) => {
|
||||
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::MouseButton(MouseButtonEvent::new(action, button, point))
|
||||
.with_webdriver_message_id(Some(message_id)),
|
||||
);
|
||||
},
|
||||
|
||||
CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y, message_id) => {
|
||||
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
|
||||
warn!("Handling input event for unknown webview: {webview_id}");
|
||||
return;
|
||||
};
|
||||
let dppx = webview_renderer.device_pixels_per_page_pixel();
|
||||
let point = dppx.transform_point(Point2D::new(x, y));
|
||||
webview_renderer.dispatch_input_event(
|
||||
InputEvent::MouseMove(MouseMoveEvent::new(point))
|
||||
.with_webdriver_message_id(Some(message_id)),
|
||||
);
|
||||
},
|
||||
|
||||
CompositorMsg::WebDriverWheelScrollEvent(webview_id, x, y, delta_x, delta_y) => {
|
||||
|
@ -1264,39 +1286,6 @@ impl IOCompositor {
|
|||
self.set_needs_repaint(RepaintReason::Resize);
|
||||
}
|
||||
|
||||
/// If there are any animations running, dispatches appropriate messages to the constellation.
|
||||
fn process_animations(&mut self, force: bool) {
|
||||
// When running animations in order to dump a screenshot (not after a full composite), don't send
|
||||
// animation ticks faster than about 60Hz.
|
||||
//
|
||||
// TODO: This should be based on the refresh rate of the screen and also apply to all
|
||||
// animation ticks, not just ones sent while waiting to dump screenshots. This requires
|
||||
// something like a refresh driver concept though.
|
||||
if !force && (Instant::now() - self.last_animation_tick) < Duration::from_millis(16) {
|
||||
return;
|
||||
}
|
||||
self.last_animation_tick = Instant::now();
|
||||
|
||||
let animating_webviews: Vec<_> = self
|
||||
.webview_renderers
|
||||
.iter()
|
||||
.filter_map(|webview_renderer| {
|
||||
if webview_renderer.animating() {
|
||||
Some(webview_renderer.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !animating_webviews.is_empty() {
|
||||
if let Err(error) = self.global.borrow().constellation_sender.send(
|
||||
EmbedderToConstellationMessage::TickAnimation(animating_webviews),
|
||||
) {
|
||||
warn!("Sending tick to constellation failed ({error:?}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_zoom_reset_window_event(&mut self, webview_id: WebViewId) {
|
||||
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
|
||||
return;
|
||||
|
@ -1403,6 +1392,11 @@ impl IOCompositor {
|
|||
/// Render the WebRender scene to the active `RenderingContext`. If successful, trigger
|
||||
/// the next round of animations.
|
||||
pub fn render(&mut self) -> bool {
|
||||
self.global
|
||||
.borrow()
|
||||
.refresh_driver
|
||||
.notify_will_paint(self.webview_renderers.iter());
|
||||
|
||||
if let Err(error) = self.render_inner() {
|
||||
warn!("Unable to render: {error:?}");
|
||||
return false;
|
||||
|
@ -1412,9 +1406,6 @@ impl IOCompositor {
|
|||
// the scene no longer needs to be repainted.
|
||||
self.needs_repaint.set(RepaintReason::empty());
|
||||
|
||||
// Queue up any subsequent paints for animations.
|
||||
self.process_animations(true);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -1424,7 +1415,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 +1442,19 @@ impl IOCompositor {
|
|||
Ok(self
|
||||
.rendering_context
|
||||
.read_to_image(rect)
|
||||
.map(|image| Image {
|
||||
.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,
|
||||
}))
|
||||
|
@ -1482,10 +1476,8 @@ impl IOCompositor {
|
|||
|
||||
if opts::get().wait_for_stable_image {
|
||||
// The current image may be ready to output. However, if there are animations active,
|
||||
// tick those instead and continue waiting for the image output to be stable AND
|
||||
// all active animations to complete.
|
||||
// continue waiting for the image output to be stable AND all active animations to complete.
|
||||
if self.animations_or_animation_callbacks_running() {
|
||||
self.process_animations(false);
|
||||
return Err(UnableToComposite::NotReadyToPaintImage(
|
||||
NotReadyToPaint::AnimationsActive,
|
||||
));
|
||||
|
@ -1668,11 +1660,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);
|
||||
}
|
||||
self.webview_renderers = webview_renderers;
|
||||
for update in scroll_offset_updates {
|
||||
let offset = LayoutVector2D::new(-update.offset.x, -update.offset.y);
|
||||
transaction.set_scroll_offsets(
|
||||
update.external_scroll_id,
|
||||
vec![SampledScrollOffset {
|
||||
offset,
|
||||
generation: 0,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
self.generate_frame(&mut transaction, RenderReasons::APZ);
|
||||
self.global.borrow_mut().send_transaction(transaction);
|
||||
}
|
||||
|
||||
self.global.borrow().shutdown_state() != ShutdownState::FinishedShuttingDown
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ use compositing_traits::rendering_context::RenderingContext;
|
|||
use compositing_traits::{CompositorMsg, CompositorProxy};
|
||||
use constellation_traits::EmbedderToConstellationMessage;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use embedder_traits::ShutdownState;
|
||||
use embedder_traits::{EventLoopWaker, ShutdownState};
|
||||
use profile_traits::{mem, time};
|
||||
use webrender::RenderApi;
|
||||
use webrender_api::DocumentId;
|
||||
|
@ -22,9 +22,10 @@ pub use crate::compositor::{IOCompositor, WebRenderDebugOption};
|
|||
mod tracing;
|
||||
|
||||
mod compositor;
|
||||
mod refresh_driver;
|
||||
mod touch;
|
||||
pub mod webview_manager;
|
||||
pub mod webview_renderer;
|
||||
mod webview_manager;
|
||||
mod webview_renderer;
|
||||
|
||||
/// Data used to construct a compositor.
|
||||
pub struct InitialCompositorState {
|
||||
|
@ -49,4 +50,7 @@ pub struct InitialCompositorState {
|
|||
pub webrender_gl: Rc<dyn gleam::gl::Gl>,
|
||||
#[cfg(feature = "webxr")]
|
||||
pub webxr_main_thread: webxr::MainThreadRegistry,
|
||||
/// An [`EventLoopWaker`] used in order to wake up the embedder when it is
|
||||
/// time to paint.
|
||||
pub event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
}
|
||||
|
|
234
components/compositing/refresh_driver.rs
Normal file
234
components/compositing/refresh_driver.rs
Normal file
|
@ -0,0 +1,234 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::collections::hash_map::Values;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
use base::id::WebViewId;
|
||||
use constellation_traits::EmbedderToConstellationMessage;
|
||||
use crossbeam_channel::{Sender, select};
|
||||
use embedder_traits::EventLoopWaker;
|
||||
use log::warn;
|
||||
use timers::{BoxedTimerCallback, TimerEventId, TimerEventRequest, TimerScheduler, TimerSource};
|
||||
|
||||
use crate::compositor::RepaintReason;
|
||||
use crate::webview_renderer::WebViewRenderer;
|
||||
|
||||
const FRAME_DURATION: Duration = Duration::from_millis(1000 / 120);
|
||||
|
||||
/// The [`RefreshDriver`] is responsible for controlling updates to aall `WebView`s
|
||||
/// onscreen presentation. Currently, it only manages controlling animation update
|
||||
/// requests.
|
||||
///
|
||||
/// The implementation is very basic at the moment, only requesting new animation
|
||||
/// frames at a constant time after a repaint.
|
||||
pub(crate) struct RefreshDriver {
|
||||
/// The channel on which messages can be sent to the Constellation.
|
||||
pub(crate) constellation_sender: Sender<EmbedderToConstellationMessage>,
|
||||
|
||||
/// Whether or not we are currently animating via a timer.
|
||||
pub(crate) animating: Cell<bool>,
|
||||
|
||||
/// Whether or not we are waiting for our frame timeout to trigger
|
||||
pub(crate) waiting_for_frame_timeout: Arc<AtomicBool>,
|
||||
|
||||
/// A [`TimerThread`] which is used to schedule frame timeouts in the future.
|
||||
timer_thread: TimerThread,
|
||||
|
||||
/// An [`EventLoopWaker`] to be used to wake up the embedder when it is
|
||||
/// time to paint a frame.
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
}
|
||||
|
||||
impl RefreshDriver {
|
||||
pub(crate) fn new(
|
||||
constellation_sender: Sender<EmbedderToConstellationMessage>,
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
) -> Self {
|
||||
Self {
|
||||
constellation_sender,
|
||||
animating: Default::default(),
|
||||
waiting_for_frame_timeout: Default::default(),
|
||||
timer_thread: Default::default(),
|
||||
event_loop_waker,
|
||||
}
|
||||
}
|
||||
|
||||
fn timer_callback(&self) -> BoxedTimerCallback {
|
||||
let waiting_for_frame_timeout = self.waiting_for_frame_timeout.clone();
|
||||
let event_loop_waker = self.event_loop_waker.clone_box();
|
||||
Box::new(move |_| {
|
||||
waiting_for_frame_timeout.store(false, Ordering::Relaxed);
|
||||
event_loop_waker.wake();
|
||||
})
|
||||
}
|
||||
|
||||
/// Notify the [`RefreshDriver`] that a paint is about to happen. This will trigger
|
||||
/// new animation frames for all active `WebView`s and schedule a new frame deadline.
|
||||
pub(crate) fn notify_will_paint(
|
||||
&self,
|
||||
webview_renderers: Values<'_, WebViewId, WebViewRenderer>,
|
||||
) {
|
||||
// If we are still waiting for the frame to timeout this paint was caused for some
|
||||
// non-animation related reason and we should wait until the frame timeout to trigger
|
||||
// the next one.
|
||||
if self.waiting_for_frame_timeout.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If any WebViews are animating ask them to paint again for another animation tick.
|
||||
let animating_webviews: Vec<_> = webview_renderers
|
||||
.filter_map(|webview_renderer| {
|
||||
if webview_renderer.animating() {
|
||||
Some(webview_renderer.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If nothing is animating any longer, update our state and exit early without requesting
|
||||
// any noew frames nor triggering a new animation deadline.
|
||||
if animating_webviews.is_empty() {
|
||||
self.animating.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) =
|
||||
self.constellation_sender
|
||||
.send(EmbedderToConstellationMessage::TickAnimation(
|
||||
animating_webviews,
|
||||
))
|
||||
{
|
||||
warn!("Sending tick to constellation failed ({error:?}).");
|
||||
}
|
||||
|
||||
// Queue the next frame deadline.
|
||||
self.animating.set(true);
|
||||
self.waiting_for_frame_timeout
|
||||
.store(true, Ordering::Relaxed);
|
||||
self.timer_thread
|
||||
.queue_timer(FRAME_DURATION, self.timer_callback());
|
||||
}
|
||||
|
||||
/// Notify the [`RefreshDriver`] that the animation state of a particular `WebView`
|
||||
/// via its associated [`WebViewRenderer`] has changed. In the case that a `WebView`
|
||||
/// has started animating, the [`RefreshDriver`] will request a new frame from it
|
||||
/// immediately, but only render that frame at the next frame deadline.
|
||||
pub(crate) fn notify_animation_state_changed(&self, webview_renderer: &WebViewRenderer) {
|
||||
if !webview_renderer.animating() {
|
||||
// If no other WebView is animating we will officially stop animated once the
|
||||
// next frame has been painted.
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) =
|
||||
self.constellation_sender
|
||||
.send(EmbedderToConstellationMessage::TickAnimation(vec![
|
||||
webview_renderer.id,
|
||||
]))
|
||||
{
|
||||
warn!("Sending tick to constellation failed ({error:?}).");
|
||||
}
|
||||
|
||||
if self.animating.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.animating.set(true);
|
||||
self.waiting_for_frame_timeout
|
||||
.store(true, Ordering::Relaxed);
|
||||
self.timer_thread
|
||||
.queue_timer(FRAME_DURATION, self.timer_callback());
|
||||
}
|
||||
|
||||
/// Whether or not the renderer should trigger a message to the embedder to request a
|
||||
/// repaint. This might be false if: we are animating and the repaint reason is just
|
||||
/// for a new frame. In that case, the renderer should wait until the frame timeout to
|
||||
/// ask the embedder to repaint.
|
||||
pub(crate) fn wait_to_paint(&self, repaint_reason: RepaintReason) -> bool {
|
||||
if !self.animating.get() || repaint_reason != RepaintReason::NewWebRenderFrame {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.waiting_for_frame_timeout.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
enum TimerThreadMessage {
|
||||
Request(TimerEventRequest),
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// A thread that manages a [`TimerScheduler`] running in the background of the
|
||||
/// [`RefreshDriver`]. This is necessary because we need a reliable way of waking up the
|
||||
/// embedder's main thread, which may just be sleeping until the `EventLoopWaker` asks it
|
||||
/// to wake up.
|
||||
///
|
||||
/// It would be nice to integrate this somehow into the embedder thread, but it would
|
||||
/// require both some communication with the embedder and for all embedders to be well
|
||||
/// behave respecting wakeup timeouts -- a bit too much to ask at the moment.
|
||||
struct TimerThread {
|
||||
sender: Sender<TimerThreadMessage>,
|
||||
join_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for TimerThread {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.sender.send(TimerThreadMessage::Quit);
|
||||
if let Some(join_handle) = self.join_handle.take() {
|
||||
let _ = join_handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimerThread {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = crossbeam_channel::unbounded::<TimerThreadMessage>();
|
||||
let join_handle = thread::Builder::new()
|
||||
.name(String::from("CompositorTimerThread"))
|
||||
.spawn(move || {
|
||||
let mut scheduler = TimerScheduler::default();
|
||||
|
||||
loop {
|
||||
select! {
|
||||
recv(receiver) -> message => {
|
||||
match message {
|
||||
Ok(TimerThreadMessage::Request(request)) => {
|
||||
scheduler.schedule_timer(request);
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
},
|
||||
recv(scheduler.wait_channel()) -> _message => {
|
||||
scheduler.dispatch_completed_timers();
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
.expect("Could not create RefreshDriver timer thread.");
|
||||
|
||||
Self {
|
||||
sender,
|
||||
join_handle: Some(join_handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TimerThread {
|
||||
fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) {
|
||||
let _ = self
|
||||
.sender
|
||||
.send(TimerThreadMessage::Request(TimerEventRequest {
|
||||
callback,
|
||||
source: TimerSource::FromWorker,
|
||||
id: TimerEventId(0),
|
||||
duration,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
@ -148,6 +148,7 @@ use net_traits::pub_domains::reg_host;
|
|||
use net_traits::request::Referrer;
|
||||
use net_traits::storage_thread::{StorageThreadMsg, StorageType};
|
||||
use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads};
|
||||
use profile_traits::mem::ProfilerMsg;
|
||||
use profile_traits::{mem, time};
|
||||
use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
|
||||
use script_traits::{
|
||||
|
@ -172,6 +173,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 +231,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 +314,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 +522,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 +531,7 @@ impl WebDriverData {
|
|||
WebDriverData {
|
||||
load_channel: None,
|
||||
resize_channel: None,
|
||||
input_command_response_sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -892,6 +885,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 +973,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 +1437,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)
|
||||
|
@ -1485,6 +1489,9 @@ where
|
|||
) => {
|
||||
self.handle_evaluate_javascript(webview_id, evaluation_id, script);
|
||||
},
|
||||
EmbedderToConstellationMessage::CreateMemoryReport(sender) => {
|
||||
self.mem_profiler_chan.send(ProfilerMsg::Report(sender));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1867,6 +1874,18 @@ where
|
|||
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => {
|
||||
self.handle_finish_javascript_evaluation(evaluation_id, result)
|
||||
},
|
||||
ScriptToConstellationMessage::WebDriverInputComplete(msg_id) => {
|
||||
if let Some(ref reply_sender) = self.webdriver.input_command_response_sender {
|
||||
reply_sender
|
||||
.send(WebDriverCommandResponse { id: msg_id })
|
||||
.unwrap_or_else(|_| {
|
||||
warn!("Failed to send WebDriverInputComplete {:?}", msg_id);
|
||||
self.webdriver.input_command_response_sender = None;
|
||||
});
|
||||
} else {
|
||||
warn!("No WebDriver input_command_response_sender");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2743,7 +2762,11 @@ where
|
|||
}
|
||||
|
||||
debug!("Exiting Canvas Paint thread.");
|
||||
if let Err(e) = self.canvas_sender.send(ConstellationCanvasMsg::Exit) {
|
||||
let (canvas_exit_sender, canvas_exit_receiver) = unbounded();
|
||||
if let Err(e) = self
|
||||
.canvas_sender
|
||||
.send(ConstellationCanvasMsg::Exit(canvas_exit_sender))
|
||||
{
|
||||
warn!("Exit Canvas Paint thread failed ({})", e);
|
||||
}
|
||||
|
||||
|
@ -2785,6 +2808,10 @@ where
|
|||
debug!("Exiting GLPlayer thread.");
|
||||
WindowGLContext::get().exit();
|
||||
|
||||
// Wait for the canvas thread to exit before shutting down the font service, as
|
||||
// canvas might still be using the system font service before shutting down.
|
||||
let _ = canvas_exit_receiver.recv();
|
||||
|
||||
debug!("Exiting the system font service thread.");
|
||||
self.system_font_service.exit();
|
||||
|
||||
|
@ -3127,13 +3154,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 +3561,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 +4855,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 +4867,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 +5627,31 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle theme change events from the embedder and forward them to the script thread
|
||||
/// Handle theme change events from the embedder and forward them to all appropriate `ScriptThread`s.
|
||||
#[cfg_attr(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
45
components/constellation/constellation_webview.rs
Normal file
45
components/constellation/constellation_webview.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ mod tracing;
|
|||
|
||||
mod browsingcontext;
|
||||
mod constellation;
|
||||
mod constellation_webview;
|
||||
mod event_loop;
|
||||
mod logging;
|
||||
mod pipeline;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -78,6 +78,7 @@ mod from_compositor {
|
|||
Self::SetScrollStates(..) => target!("SetScrollStates"),
|
||||
Self::PaintMetric(..) => target!("PaintMetric"),
|
||||
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),
|
||||
Self::CreateMemoryReport(..) => target!("CreateMemoryReport"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,6 +178,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 +241,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")
|
||||
},
|
||||
|
|
55
components/devtools/actors/breakpoint.rs
Normal file
55
components/devtools/actors/breakpoint.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::EmptyReplyMsg;
|
||||
use crate::actor::{Actor, ActorMessageStatus};
|
||||
use crate::protocol::JsonPacketStream;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BreakpointListActorMsg {
|
||||
actor: String,
|
||||
}
|
||||
|
||||
pub struct BreakpointListActor {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Actor for BreakpointListActor {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn handle_message(
|
||||
&self,
|
||||
_registry: &crate::actor::ActorRegistry,
|
||||
msg_type: &str,
|
||||
_msg: &serde_json::Map<String, serde_json::Value>,
|
||||
stream: &mut std::net::TcpStream,
|
||||
_stream_id: crate::StreamId,
|
||||
) -> Result<crate::actor::ActorMessageStatus, ()> {
|
||||
Ok(match msg_type {
|
||||
"setBreakpoint" => {
|
||||
let msg = EmptyReplyMsg { from: self.name() };
|
||||
let _ = stream.write_json_packet(&msg);
|
||||
|
||||
ActorMessageStatus::Processed
|
||||
},
|
||||
"setActiveEventBreakpoints" => {
|
||||
let msg = EmptyReplyMsg { from: self.name() };
|
||||
let _ = stream.write_json_packet(&msg);
|
||||
|
||||
ActorMessageStatus::Processed
|
||||
},
|
||||
_ => ActorMessageStatus::Ignored,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BreakpointListActor {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ use serde::Serialize;
|
|||
use serde_json::{Map, Value};
|
||||
|
||||
use self::network_parent::{NetworkParentActor, NetworkParentActorMsg};
|
||||
use super::breakpoint::BreakpointListActor;
|
||||
use super::thread::ThreadActor;
|
||||
use super::worker::WorkerMsg;
|
||||
use crate::actor::{Actor, ActorMessageStatus, ActorRegistry};
|
||||
|
@ -362,10 +363,14 @@ impl Actor for WatcherActor {
|
|||
ActorMessageStatus::Processed
|
||||
},
|
||||
"getBreakpointListActor" => {
|
||||
let breakpoint_list_name = registry.new_name("breakpoint-list");
|
||||
let breakpoint_list = BreakpointListActor::new(breakpoint_list_name.clone());
|
||||
registry.register_later(Box::new(breakpoint_list));
|
||||
|
||||
let _ = stream.write_json_packet(&GetBreakpointListActorReply {
|
||||
from: self.name(),
|
||||
breakpoint_list: GetBreakpointListActorReplyInner {
|
||||
actor: registry.new_name("breakpoint-list"),
|
||||
actor: breakpoint_list_name,
|
||||
},
|
||||
});
|
||||
ActorMessageStatus::Processed
|
||||
|
|
|
@ -53,6 +53,7 @@ use crate::protocol::JsonPacketStream;
|
|||
mod actor;
|
||||
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors>
|
||||
mod actors {
|
||||
pub mod breakpoint;
|
||||
pub mod browsing_context;
|
||||
pub mod console;
|
||||
pub mod device;
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
||||
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 image_info = WebRenderImageInfo {
|
||||
size: Size2D::new(image.width, image.height),
|
||||
key: image.id,
|
||||
};
|
||||
if image_info.key.is_none() {
|
||||
Ok(image_info)
|
||||
} else {
|
||||
let mut webrender_image_cache = self.webrender_image_cache.write();
|
||||
webrender_image_cache.insert((url, use_placeholder), image_info);
|
||||
Ok(image_info)
|
||||
}
|
||||
|
||||
let mut resolved_images_cache = self.resolved_images_cache.write();
|
||||
resolved_images_cache.insert((url, use_placeholder), Ok(image.clone()));
|
||||
Ok(image)
|
||||
},
|
||||
ImageOrMetadataAvailable::MetadataAvailable(..) => {
|
||||
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 image’s 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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
276
components/layout/display_list/clip.rs
Normal file
276
components/layout/display_list/clip.rs
Normal 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(),
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
// Paint the canvas’ background (if any) before/under everything else
|
||||
stacking_context_tree
|
||||
.root_stacking_context
|
||||
.build_canvas_background_display_list(&mut builder, fragment_tree);
|
||||
stacking_context_tree
|
||||
.root_stacking_context
|
||||
.build_display_list(&mut builder);
|
||||
builder.paint_dom_inspector_highlight();
|
||||
|
||||
webrender_display_list_builder.end().1
|
||||
}
|
||||
|
||||
impl DisplayListBuilder<'_> {
|
||||
fn wr(&mut self) -> &mut wr::DisplayListBuilder {
|
||||
&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)
|
||||
{
|
||||
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, &color);
|
||||
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)
|
||||
{
|
||||
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, &color);
|
||||
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
|
||||
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, &color);
|
||||
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
|
@ -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 it’s 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);
|
||||
|
|
|
@ -59,8 +59,14 @@ impl<'dom> NodeAndStyleInfo<'dom> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether this is a container for the editable text within a single-line text input.
|
||||
/// This is used to solve the special case of line height for a text editor.
|
||||
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
|
||||
// FIXME(stevennovaryo): Now, this would also refer to HTMLInputElement, to handle input
|
||||
// elements without shadow DOM.
|
||||
pub(crate) fn is_single_line_text_input(&self) -> bool {
|
||||
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
|
||||
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) ||
|
||||
self.node.is_text_control_inner_editor()
|
||||
}
|
||||
|
||||
pub(crate) fn pseudo(
|
||||
|
@ -201,17 +207,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 +225,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()));
|
||||
|
|
|
@ -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 item’s 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 container’s 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);
|
||||
|
@ -1929,7 +1924,11 @@ impl FlexItem<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
let lazy_block_size = if cross_axis_is_item_block_axis {
|
||||
let lazy_block_size = if !cross_axis_is_item_block_axis {
|
||||
used_main_size.into()
|
||||
} else if let Some(cross_size) = used_cross_size_override {
|
||||
cross_size.into()
|
||||
} else {
|
||||
// This means that an auto size with stretch alignment will behave different than
|
||||
// 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.
|
||||
|
@ -1946,8 +1945,6 @@ impl FlexItem<'_> {
|
|||
stretch_size,
|
||||
self.is_table(),
|
||||
)
|
||||
} else {
|
||||
used_main_size.into()
|
||||
};
|
||||
|
||||
let layout = non_replaced.layout(
|
||||
|
@ -2456,6 +2453,8 @@ impl FlexItemBox {
|
|||
};
|
||||
|
||||
let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main;
|
||||
let outer_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,
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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, 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 document’s 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,
|
||||
|
@ -395,31 +392,9 @@ impl BoxTree {
|
|||
&mut root_fragments,
|
||||
);
|
||||
|
||||
let scrollable_overflow = root_fragments
|
||||
.iter()
|
||||
.fold(PhysicalRect::zero(), |acc, child| {
|
||||
let child_overflow = child.scrollable_overflow_for_parent();
|
||||
|
||||
// https://drafts.csswg.org/css-overflow/#scrolling-direction
|
||||
// We want to clip scrollable overflow on box-start and inline-start
|
||||
// sides of the scroll container.
|
||||
//
|
||||
// FIXME(mrobinson, bug 25564): This should take into account writing
|
||||
// mode.
|
||||
let child_overflow = PhysicalRect::new(
|
||||
euclid::Point2D::zero(),
|
||||
euclid::Size2D::new(
|
||||
child_overflow.size.width + child_overflow.origin.x,
|
||||
child_overflow.size.height + child_overflow.origin.y,
|
||||
),
|
||||
);
|
||||
acc.union(&child_overflow)
|
||||
});
|
||||
|
||||
FragmentTree::new(
|
||||
layout_context,
|
||||
root_fragments,
|
||||
scrollable_overflow,
|
||||
physical_containing_block,
|
||||
self.viewport_scroll_sensitivity,
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
@ -223,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(..) => {},
|
||||
}
|
||||
|
@ -356,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)
|
||||
},
|
||||
|
|
|
@ -89,10 +89,10 @@ pub(crate) struct BoxFragment {
|
|||
block_margins_collapsed_with_children: Option<Box<CollapsedBlockMargins>>,
|
||||
|
||||
/// The scrollable overflow of this box fragment.
|
||||
pub scrollable_overflow_from_children: PhysicalRect<Au>,
|
||||
scrollable_overflow: Option<PhysicalRect<Au>>,
|
||||
|
||||
/// The resolved box insets if this box is `position: sticky`. These are calculated
|
||||
/// 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>>>,
|
||||
|
||||
|
@ -114,11 +114,6 @@ impl BoxFragment {
|
|||
margin: PhysicalSides<Au>,
|
||||
clearance: Option<Au>,
|
||||
) -> BoxFragment {
|
||||
let scrollable_overflow_from_children =
|
||||
children.iter().fold(PhysicalRect::zero(), |acc, child| {
|
||||
acc.union(&child.scrollable_overflow_for_parent())
|
||||
});
|
||||
|
||||
BoxFragment {
|
||||
base: base_fragment_info.into(),
|
||||
style,
|
||||
|
@ -131,7 +126,7 @@ impl BoxFragment {
|
|||
clearance,
|
||||
baselines: Baselines::default(),
|
||||
block_margins_collapsed_with_children: None,
|
||||
scrollable_overflow_from_children,
|
||||
scrollable_overflow: None,
|
||||
resolved_sticky_insets: AtomicRefCell::default(),
|
||||
background_mode: BackgroundMode::Normal,
|
||||
specific_layout_info: None,
|
||||
|
@ -203,13 +198,23 @@ impl BoxFragment {
|
|||
/// Get the scrollable overflow for this [`BoxFragment`] relative to its
|
||||
/// containing block.
|
||||
pub fn scrollable_overflow(&self) -> PhysicalRect<Au> {
|
||||
self.scrollable_overflow
|
||||
.expect("Should only call `scrollable_overflow()` after calculating overflow")
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_scrollable_overflow(&mut self) {
|
||||
let scrollable_overflow_from_children = self
|
||||
.children
|
||||
.iter()
|
||||
.fold(PhysicalRect::zero(), |acc, child| {
|
||||
acc.union(&child.calculate_scrollable_overflow_for_parent())
|
||||
});
|
||||
let physical_padding_rect = self.padding_rect();
|
||||
let content_origin = self.content_rect.origin.to_vector();
|
||||
physical_padding_rect.union(
|
||||
&self
|
||||
.scrollable_overflow_from_children
|
||||
.translate(content_origin),
|
||||
)
|
||||
self.scrollable_overflow = Some(
|
||||
physical_padding_rect
|
||||
.union(&scrollable_overflow_from_children.translate(content_origin)),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) {
|
||||
|
@ -275,7 +280,12 @@ impl BoxFragment {
|
|||
tree.end_level();
|
||||
}
|
||||
|
||||
pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
// TODO: Properly handle absolutely positioned fragments.
|
||||
if self.style.get_box().position.is_absolutely_positioned() {
|
||||
return PhysicalRect::zero();
|
||||
}
|
||||
|
||||
let mut overflow = self.border_rect();
|
||||
if !self.style.establishes_scroll_container(self.base.flags) {
|
||||
// https://www.w3.org/TR/css-overflow-3/#scrollable
|
||||
|
@ -328,7 +338,7 @@ impl BoxFragment {
|
|||
///
|
||||
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
|
||||
/// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block.
|
||||
pub fn clip_unreachable_scrollable_overflow_region(
|
||||
pub(crate) fn clip_unreachable_scrollable_overflow_region(
|
||||
&self,
|
||||
scrollable_overflow: PhysicalRect<Au>,
|
||||
clipping_rect: PhysicalRect<Au>,
|
||||
|
@ -362,7 +372,7 @@ impl BoxFragment {
|
|||
///
|
||||
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
|
||||
/// This will coincides with the scrollport if the fragment is a scroll container.
|
||||
pub fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> {
|
||||
pub(crate) fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> {
|
||||
self.clip_unreachable_scrollable_overflow_region(
|
||||
self.scrollable_overflow(),
|
||||
self.padding_rect(),
|
||||
|
@ -421,9 +431,7 @@ impl BoxFragment {
|
|||
return convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left));
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
position == ComputedPosition::Fixed || position == ComputedPosition::Absolute
|
||||
);
|
||||
debug_assert!(position.is_absolutely_positioned());
|
||||
|
||||
let margin_rect = self.margin_rect();
|
||||
let (top, bottom) = match (&insets.top, &insets.bottom) {
|
||||
|
|
|
@ -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>>,
|
||||
|
@ -187,19 +183,36 @@ impl Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
match self {
|
||||
Fragment::Box(fragment) | Fragment::Float(fragment) => {
|
||||
fragment.borrow().scrollable_overflow_for_parent()
|
||||
return fragment.borrow().scrollable_overflow_for_parent();
|
||||
},
|
||||
Fragment::AbsoluteOrFixedPositioned(_) => PhysicalRect::zero(),
|
||||
Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow,
|
||||
Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow_for_parent(),
|
||||
Fragment::Text(fragment) => fragment.borrow().rect,
|
||||
Fragment::Image(fragment) => fragment.borrow().rect,
|
||||
Fragment::IFrame(fragment) => fragment.borrow().rect,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
self.calculate_scrollable_overflow();
|
||||
self.scrollable_overflow_for_parent()
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_scrollable_overflow(&self) {
|
||||
match self {
|
||||
Fragment::Box(fragment) | Fragment::Float(fragment) => {
|
||||
fragment.borrow_mut().calculate_scrollable_overflow()
|
||||
},
|
||||
Fragment::Positioning(fragment) => {
|
||||
fragment.borrow_mut().calculate_scrollable_overflow()
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cumulative_border_box_rect(&self) -> Option<PhysicalRect<Au>> {
|
||||
match self {
|
||||
Fragment::Box(fragment) | Fragment::Float(fragment) => {
|
||||
|
|
|
@ -2,19 +2,18 @@
|
|||
* 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 app_units::Au;
|
||||
use base::print_tree::PrintTree;
|
||||
use compositing_traits::display_list::AxesScrollSensitivity;
|
||||
use euclid::default::Size2D;
|
||||
use fxhash::FxHashSet;
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use style::animation::AnimationSetKey;
|
||||
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)]
|
||||
|
@ -31,7 +30,7 @@ pub struct FragmentTree {
|
|||
|
||||
/// The scrollable overflow rectangle for the entire tree
|
||||
/// <https://drafts.csswg.org/css-overflow/#scrollable>
|
||||
pub(crate) scrollable_overflow: PhysicalRect<Au>,
|
||||
scrollable_overflow: Cell<Option<PhysicalRect<Au>>>,
|
||||
|
||||
/// The containing block used in the layout of this fragment tree.
|
||||
pub(crate) initial_containing_block: PhysicalRect<Au>,
|
||||
|
@ -44,13 +43,12 @@ impl FragmentTree {
|
|||
pub(crate) fn new(
|
||||
layout_context: &LayoutContext,
|
||||
root_fragments: Vec<Fragment>,
|
||||
scrollable_overflow: PhysicalRect<Au>,
|
||||
initial_containing_block: PhysicalRect<Au>,
|
||||
viewport_scroll_sensitivity: AxesScrollSensitivity,
|
||||
) -> Self {
|
||||
let fragment_tree = Self {
|
||||
root_fragments,
|
||||
scrollable_overflow,
|
||||
scrollable_overflow: Cell::default(),
|
||||
initial_containing_block,
|
||||
viewport_scroll_sensitivity,
|
||||
};
|
||||
|
@ -91,16 +89,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 {
|
||||
|
@ -108,11 +96,35 @@ impl FragmentTree {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn scrollable_overflow(&self) -> units::LayoutSize {
|
||||
units::LayoutSize::from_untyped(Size2D::new(
|
||||
self.scrollable_overflow.size.width.to_f32_px(),
|
||||
self.scrollable_overflow.size.height.to_f32_px(),
|
||||
))
|
||||
pub(crate) fn scrollable_overflow(&self) -> PhysicalRect<Au> {
|
||||
self.scrollable_overflow
|
||||
.get()
|
||||
.expect("Should only call `scrollable_overflow()` after calculating overflow")
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_scrollable_overflow(&self) {
|
||||
self.scrollable_overflow
|
||||
.set(Some(self.root_fragments.iter().fold(
|
||||
PhysicalRect::zero(),
|
||||
|acc, child| {
|
||||
let child_overflow = child.calculate_scrollable_overflow_for_parent();
|
||||
|
||||
// https://drafts.csswg.org/css-overflow/#scrolling-direction
|
||||
// We want to clip scrollable overflow on box-start and inline-start
|
||||
// sides of the scroll container.
|
||||
//
|
||||
// FIXME(mrobinson, bug 25564): This should take into account writing
|
||||
// mode.
|
||||
let child_overflow = PhysicalRect::new(
|
||||
euclid::Point2D::zero(),
|
||||
euclid::Size2D::new(
|
||||
child_overflow.size.width + child_overflow.origin.x,
|
||||
child_overflow.size.height + child_overflow.origin.y,
|
||||
),
|
||||
);
|
||||
acc.union(&child_overflow)
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
pub(crate) fn find<T>(
|
||||
|
|
|
@ -22,7 +22,7 @@ pub(crate) struct PositioningFragment {
|
|||
pub children: Vec<Fragment>,
|
||||
|
||||
/// The scrollable overflow of this anonymous fragment's children.
|
||||
pub scrollable_overflow: PhysicalRect<Au>,
|
||||
scrollable_overflow: Option<PhysicalRect<Au>>,
|
||||
|
||||
/// The style of the fragment.
|
||||
pub style: ServoArc<ComputedValues>,
|
||||
|
@ -55,20 +55,12 @@ impl PositioningFragment {
|
|||
rect: PhysicalRect<Au>,
|
||||
children: Vec<Fragment>,
|
||||
) -> ArcRefCell<Self> {
|
||||
let content_origin = rect.origin;
|
||||
let scrollable_overflow = children.iter().fold(PhysicalRect::zero(), |acc, child| {
|
||||
acc.union(
|
||||
&child
|
||||
.scrollable_overflow_for_parent()
|
||||
.translate(content_origin.to_vector()),
|
||||
)
|
||||
});
|
||||
ArcRefCell::new(PositioningFragment {
|
||||
base,
|
||||
style,
|
||||
rect,
|
||||
children,
|
||||
scrollable_overflow,
|
||||
scrollable_overflow: None,
|
||||
cumulative_containing_block_rect: PhysicalRect::zero(),
|
||||
})
|
||||
}
|
||||
|
@ -81,6 +73,25 @@ impl PositioningFragment {
|
|||
rect.translate(self.cumulative_containing_block_rect.origin.to_vector())
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_scrollable_overflow(&mut self) {
|
||||
self.scrollable_overflow = Some(self.children.iter().fold(
|
||||
PhysicalRect::zero(),
|
||||
|acc, child| {
|
||||
acc.union(
|
||||
&child
|
||||
.calculate_scrollable_overflow_for_parent()
|
||||
.translate(self.rect.origin.to_vector()),
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
|
||||
self.scrollable_overflow.expect(
|
||||
"Should only call `scrollable_overflow_for_parent()` after calculating overflow",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print(&self, tree: &mut PrintTree) {
|
||||
tree.new_level(format!(
|
||||
"PositioningFragment\
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::cell::{Cell, RefCell};
|
|||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::process;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use app_units::Au;
|
||||
|
@ -15,8 +16,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, WebFontDocumentContext};
|
||||
|
@ -76,8 +77,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,
|
||||
|
@ -142,19 +143,21 @@ pub struct LayoutThread {
|
|||
box_tree: RefCell<Option<Arc<BoxTree>>>,
|
||||
|
||||
/// The fragment tree.
|
||||
fragment_tree: RefCell<Option<Arc<FragmentTree>>>,
|
||||
fragment_tree: RefCell<Option<Rc<FragmentTree>>>,
|
||||
|
||||
/// The [`StackingContextTree`] cached from previous layouts.
|
||||
stacking_context_tree: RefCell<Option<StackingContextTree>>,
|
||||
|
||||
/// A counter for epoch messages
|
||||
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,
|
||||
|
@ -510,8 +513,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 {
|
||||
|
@ -527,16 +529,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(),
|
||||
}
|
||||
}
|
||||
|
@ -615,7 +614,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());
|
||||
}
|
||||
|
@ -647,8 +647,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,
|
||||
))),
|
||||
|
@ -657,25 +658,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.calculate_overflow(damage);
|
||||
self.build_stacking_context_tree(&reflow_request, damage);
|
||||
self.build_display_list(&reflow_request, &mut layout_context);
|
||||
self.first_reflow.set(false);
|
||||
|
||||
self.first_reflow.set(false);
|
||||
if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal {
|
||||
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,
|
||||
})
|
||||
|
@ -684,12 +692,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(
|
||||
|
@ -697,7 +705,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>(
|
||||
|
@ -755,7 +763,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()
|
||||
|
@ -771,17 +780,20 @@ impl LayoutThread {
|
|||
|
||||
if !token.should_traverse() {
|
||||
layout_context.style_context.stylist.rule_tree().maybe_gc();
|
||||
return;
|
||||
return RestyleDamage::empty();
|
||||
}
|
||||
|
||||
let dirty_root: ServoLayoutNode =
|
||||
driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node();
|
||||
|
||||
let root_node = root_element.as_node();
|
||||
let damage = compute_damage_and_repair_style(layout_context.shared_context(), root_node);
|
||||
if damage == RestyleDamage::REPAINT {
|
||||
let mut damage =
|
||||
compute_damage_and_repair_style(layout_context.shared_context(), root_node);
|
||||
if viewport_changed {
|
||||
damage = RestyleDamage::REBUILD_BOX;
|
||||
} else if !damage.contains(RestyleDamage::REBUILD_BOX) {
|
||||
layout_context.style_context.stylist.rule_tree().maybe_gc();
|
||||
return;
|
||||
return damage;
|
||||
}
|
||||
|
||||
let mut box_tree = self.box_tree.borrow_mut();
|
||||
|
@ -800,17 +812,14 @@ 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()
|
||||
.unwrap()
|
||||
.layout(recalc_style_traversal.context(), viewport_size)
|
||||
};
|
||||
let fragment_tree = Arc::new(if let Some(pool) = rayon_pool {
|
||||
let fragment_tree = Rc::new(if let Some(pool) = rayon_pool {
|
||||
pool.install(run_layout)
|
||||
} else {
|
||||
run_layout()
|
||||
|
@ -818,6 +827,10 @@ impl LayoutThread {
|
|||
|
||||
*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!(
|
||||
"{:?}",
|
||||
|
@ -835,6 +848,61 @@ impl LayoutThread {
|
|||
|
||||
// GC the rule tree if some heuristics are met.
|
||||
layout_context.style_context.stylist.rule_tree().maybe_gc();
|
||||
damage
|
||||
}
|
||||
|
||||
fn calculate_overflow(&self, damage: RestyleDamage) {
|
||||
if !damage.contains(RestyleDamage::RECALCULATE_OVERFLOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(fragment_tree) = &*self.fragment_tree.borrow() {
|
||||
fragment_tree.calculate_scrollable_overflow();
|
||||
if self.debug.dump_flow_tree {
|
||||
fragment_tree.print();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) {
|
||||
if !reflow_request.reflow_goal.needs_display_list() &&
|
||||
!reflow_request.reflow_goal.needs_display()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
|
||||
return;
|
||||
};
|
||||
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) &&
|
||||
self.stacking_context_tree.borrow().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let viewport_size = self.stylist.device().au_viewport_size();
|
||||
let viewport_size = LayoutSize::new(
|
||||
viewport_size.width.to_f32_px(),
|
||||
viewport_size.height.to_f32_px(),
|
||||
);
|
||||
|
||||
let scrollable_overflow = fragment_tree.scrollable_overflow();
|
||||
let scrollable_overflow = LayoutSize::from_untyped(Size2D::new(
|
||||
scrollable_overflow.size.width.to_f32_px(),
|
||||
scrollable_overflow.size.height.to_f32_px(),
|
||||
));
|
||||
|
||||
// Build the StackingContextTree. This turns the `FragmentTree` into a
|
||||
// tree of fragments in CSS painting order and also creates all
|
||||
// applicable spatial and clip nodes.
|
||||
*self.stacking_context_tree.borrow_mut() = Some(StackingContextTree::new(
|
||||
fragment_tree,
|
||||
viewport_size,
|
||||
scrollable_overflow,
|
||||
self.id.into(),
|
||||
fragment_tree.viewport_scroll_sensitivity,
|
||||
self.first_reflow.get(),
|
||||
&self.debug,
|
||||
));
|
||||
}
|
||||
|
||||
fn build_display_list(
|
||||
|
@ -842,60 +910,33 @@ 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,
|
||||
);
|
||||
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,
|
||||
&stacking_context_tree.compositor_info,
|
||||
built_display_list,
|
||||
);
|
||||
|
||||
let (keys, instance_keys) = self
|
||||
|
@ -904,7 +945,6 @@ impl LayoutThread {
|
|||
self.compositor_api
|
||||
.remove_unused_font_resources(keys, instance_keys)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_scroll_node_state(&self, state: &ScrollState) {
|
||||
self.scroll_offsets
|
||||
|
@ -943,9 +983,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;
|
||||
|
@ -953,7 +990,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()
|
||||
}
|
||||
|
||||
|
@ -961,7 +999,7 @@ impl LayoutThread {
|
|||
fn update_device(
|
||||
&mut self,
|
||||
viewport_details: ViewportDetails,
|
||||
theme: PrefersColorScheme,
|
||||
theme: Theme,
|
||||
guards: &StylesheetGuards,
|
||||
) {
|
||||
let device = Device::new(
|
||||
|
@ -971,7 +1009,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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,10 +305,7 @@ impl PositioningContext {
|
|||
}
|
||||
|
||||
pub(crate) fn push(&mut self, hoisted_box: HoistedAbsolutelyPositionedBox) {
|
||||
debug_assert!(matches!(
|
||||
hoisted_box.position(),
|
||||
Position::Absolute | Position::Fixed
|
||||
));
|
||||
debug_assert!(hoisted_box.position().is_absolutely_positioned());
|
||||
self.absolutes.push(hoisted_box);
|
||||
}
|
||||
|
||||
|
@ -380,7 +377,7 @@ impl HoistedAbsolutelyPositionedBox {
|
|||
.context
|
||||
.style()
|
||||
.clone_position();
|
||||
assert!(position == Position::Fixed || position == Position::Absolute);
|
||||
assert!(position.is_absolutely_positioned());
|
||||
position
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Utilities for querying the layout, as needed by layout.
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
|
||||
use app_units::Au;
|
||||
use euclid::default::{Point2D, Rect};
|
||||
|
@ -80,7 +80,7 @@ pub fn process_client_rect_request(node: ServoLayoutNode<'_>) -> Rect<i32> {
|
|||
/// <https://drafts.csswg.org/cssom-view/#scrolling-area>
|
||||
pub fn process_node_scroll_area_request(
|
||||
requested_node: Option<ServoLayoutNode<'_>>,
|
||||
fragment_tree: Option<Arc<FragmentTree>>,
|
||||
fragment_tree: Option<Rc<FragmentTree>>,
|
||||
) -> Rect<i32> {
|
||||
let Some(tree) = fragment_tree else {
|
||||
return Rect::zero();
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
Ok(ImageOrMetadataAvailable::MetadataAvailable(metadata, _id)) => {
|
||||
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(),
|
||||
|
|
|
@ -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; }
|
||||
|
@ -257,3 +266,7 @@ select {
|
|||
/* Don't show a text cursor when hovering selected option */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
slot {
|
||||
display: contents;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 response’s 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 request’s destination.
|
||||
// Step 4: If destination is script-like and one of the following is true, then return blocked:
|
||||
// - mimeType’s essence starts with "audio/", "image/", or "video/".
|
||||
// - mimeType’s 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ use std::sync::LazyLock;
|
|||
use std::time::Duration;
|
||||
|
||||
use embedder_traits::resources::{self, Resource};
|
||||
use fst::{Map, MapBuilder};
|
||||
use headers::{HeaderMapExt, StrictTransportSecurity};
|
||||
use http::HeaderMap;
|
||||
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;
|
||||
|
@ -85,99 +87,67 @@ pub struct HstsList {
|
|||
/// 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.
|
||||
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
|
||||
pub struct HstsPreloadList {
|
||||
pub entries_map: HashMap<String, Vec<HstsEntry>>,
|
||||
/// 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()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> =
|
||||
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<HstsPreloadList> {
|
||||
#[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: HstsPreloadList = HstsPreloadList::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() -> HstsPreloadList {
|
||||
debug!("Intializing HSTS Preload list");
|
||||
let list = resources::read_string(Resource::HstsPreloadList);
|
||||
HstsPreloadList::from_preload(&list).unwrap_or_else(|| {
|
||||
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");
|
||||
HstsPreloadList {
|
||||
entries_map: Default::default(),
|
||||
}
|
||||
HstsPreloadList(MapBuilder::memory().into_map())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_host_secure(&self, host: &str) -> bool {
|
||||
let base_domain = reg_suffix(host);
|
||||
self.entries_map.get(base_domain).is_some_and(|entries| {
|
||||
// No need to check for expiration in the preload list
|
||||
entries.iter().any(|e| {
|
||||
if e.include_subdomains {
|
||||
e.matches_subdomain(host) || e.matches_domain(host)
|
||||
} else {
|
||||
e.matches_domain(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;
|
||||
}
|
||||
|
||||
pub fn has_domain(&self, host: &str, base_domain: &str) -> bool {
|
||||
self.entries_map
|
||||
.get(base_domain)
|
||||
.is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host)))
|
||||
}
|
||||
|
||||
pub 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.include_subdomains && e.matches_subdomain(host))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entry: HstsEntry) {
|
||||
let host = entry.host.clone();
|
||||
let base_domain = reg_suffix(&host);
|
||||
let have_domain = self.has_domain(&entry.host, base_domain);
|
||||
let have_subdomain = self.has_subdomain(&entry.host, base_domain);
|
||||
|
||||
let entries = self.entries_map.entry(base_domain.to_owned()).or_default();
|
||||
if !have_domain && !have_subdomain {
|
||||
entries.push(entry);
|
||||
} else if !have_subdomain {
|
||||
for e in entries {
|
||||
if e.matches_domain(&entry.host) {
|
||||
e.include_subdomains = entry.include_subdomains;
|
||||
// TODO(sebsebmc): We could shrink the the HSTS preload memory use further by using a type
|
||||
// that doesn't store an expiry since all preload entries should be "forever"
|
||||
e.expires_at = entry.expires_at;
|
||||
}
|
||||
// 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 {
|
||||
debug!("HSTS: is {host} secure?");
|
||||
if PRELOAD_LIST_ENTRIES.is_host_secure(host) {
|
||||
info!("{host} is in the preload list");
|
||||
return true;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,7 +16,6 @@ pub mod http_cache;
|
|||
pub mod http_loader;
|
||||
pub mod image_cache;
|
||||
pub mod local_directory_listing;
|
||||
pub mod mime_classifier;
|
||||
pub mod protocols;
|
||||
pub mod request_interceptor;
|
||||
pub mod resource_thread;
|
||||
|
|
|
@ -21,9 +21,9 @@ use embedder_traits::EmbedderProxy;
|
|||
use hyper_serde::Serde;
|
||||
use ipc_channel::ipc::{self, IpcReceiver, IpcReceiverSet, IpcSender};
|
||||
use log::{debug, trace, warn};
|
||||
use malloc_size_of::MallocSizeOf;
|
||||
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;
|
||||
|
@ -97,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),
|
||||
|
@ -287,11 +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.push(Report {
|
||||
reports.extend(vec![
|
||||
Report {
|
||||
path: path!["hsts-preload-list"],
|
||||
kind: ReportKind::ExplicitJemallocHeapSize,
|
||||
size: hsts::PRELOAD_LIST_ENTRIES.size_of(ops),
|
||||
});
|
||||
size: hsts::hsts_preload_size_of(ops),
|
||||
},
|
||||
Report {
|
||||
path: path!["public-suffix-list"],
|
||||
kind: ReportKind::ExplicitJemallocHeapSize,
|
||||
size: public_suffix_list_size_of(ops),
|
||||
},
|
||||
]);
|
||||
msg.send(ProcessReports::new(reports));
|
||||
})
|
||||
}
|
||||
|
@ -445,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();
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -6,13 +6,14 @@ use std::collections::HashMap;
|
|||
use std::num::NonZeroU64;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
use base64::Engine;
|
||||
use net::hsts::{HstsEntry, HstsList, HstsPreloadList};
|
||||
use net_traits::IncludeSubdomains;
|
||||
|
||||
#[test]
|
||||
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,
|
||||
expires_at: None,
|
||||
};
|
||||
|
@ -23,7 +24,7 @@ fn test_hsts_entry_is_not_expired_when_it_has_no_expires_at() {
|
|||
#[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,
|
||||
expires_at: Some(NonZeroU64::new(1).unwrap()),
|
||||
};
|
||||
|
@ -59,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,
|
||||
)
|
||||
|
@ -67,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,
|
||||
)
|
||||
|
@ -75,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,
|
||||
)
|
||||
|
@ -83,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_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)),
|
||||
)
|
||||
|
@ -106,23 +107,23 @@ fn test_push_entry_with_0_max_age_is_not_secure() {
|
|||
|
||||
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(
|
||||
"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)),
|
||||
)
|
||||
|
@ -133,25 +134,25 @@ fn test_push_entry_with_0_max_age_evicts_entry_from_list() {
|
|||
entries_map: entries_map,
|
||||
};
|
||||
|
||||
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 1);
|
||||
assert_eq!(list.entries_map.get("example.com").unwrap().len(), 1);
|
||||
|
||||
list.push(
|
||||
HstsEntry::new(
|
||||
"mozilla.org".to_owned(),
|
||||
"example.com".to_owned(),
|
||||
IncludeSubdomains::NotIncluded,
|
||||
Some(StdDuration::ZERO),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(list.entries_map.get("mozilla.org").unwrap().len(), 0);
|
||||
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,
|
||||
|
@ -159,24 +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_add_subdomains_whose_superdomain_doesnt_include() {
|
||||
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,
|
||||
)
|
||||
|
@ -189,49 +190,49 @@ fn test_push_entry_to_hsts_list_should_add_subdomains_whose_superdomain_doesnt_i
|
|||
|
||||
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(), 2)
|
||||
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(
|
||||
"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,
|
||||
};
|
||||
|
||||
assert!(list.is_host_secure("servo.mozilla.org"));
|
||||
assert!(list.is_host_secure("servo.example.com"));
|
||||
|
||||
list.push(
|
||||
HstsEntry::new(
|
||||
"mozilla.org".to_owned(),
|
||||
"example.com".to_owned(),
|
||||
IncludeSubdomains::NotIncluded,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
assert!(!list.is_host_secure("servo.mozilla.org"))
|
||||
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(
|
||||
"mozilla.org".to_owned(),
|
||||
"example.com".to_owned(),
|
||||
vec![
|
||||
HstsEntry::new(
|
||||
"mozilla.org".to_owned(),
|
||||
"example.com".to_owned(),
|
||||
IncludeSubdomains::NotIncluded,
|
||||
None,
|
||||
)
|
||||
|
@ -244,14 +245,14 @@ fn test_push_entry_to_hsts_list_should_not_create_duplicate_entry() {
|
|||
|
||||
list.push(
|
||||
HstsEntry::new(
|
||||
"mozilla.org".to_owned(),
|
||||
"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]
|
||||
|
@ -260,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]
|
||||
|
@ -278,25 +277,16 @@ 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";
|
||||
assert!(
|
||||
HstsPreloadList::from_preload(mock_preload_content).is_none(),
|
||||
"invalid preload list should not have parsed"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_hsts_preload_should_return_none_when_json_contains_no_entries_map_key() {
|
||||
let mock_preload_content = "{\"nothing\": \"to see here\"}";
|
||||
let mock_preload_content = "derp".as_bytes().to_vec();
|
||||
assert!(
|
||||
HstsPreloadList::from_preload(mock_preload_content).is_none(),
|
||||
"invalid preload list should not have parsed"
|
||||
|
@ -305,20 +295,17 @@ fn test_parse_hsts_preload_should_return_none_when_json_contains_no_entries_map_
|
|||
|
||||
#[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 = HstsPreloadList::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]
|
||||
|
@ -327,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,
|
||||
)
|
||||
|
@ -349,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,
|
||||
)
|
||||
|
@ -384,44 +371,44 @@ 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,
|
||||
expires_at: Some(NonZeroU64::new(1).unwrap()),
|
||||
}],
|
||||
|
@ -430,11 +417,11 @@ fn test_hsts_list_with_expired_entry_is_not_is_host_secure() {
|
|||
entries_map: entries_map,
|
||||
};
|
||||
|
||||
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 = HstsPreloadList::from_servo_preload();
|
||||
assert!(!hsts_list.entries_map.is_empty());
|
||||
assert_ne!(hsts_list.0.len(), 0);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ mod filemanager_thread;
|
|||
mod hsts;
|
||||
mod http_cache;
|
||||
mod http_loader;
|
||||
mod mime_classifier;
|
||||
mod resource_thread;
|
||||
mod subresource_integrity;
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -30,6 +31,20 @@ pub enum PixelFormat {
|
|||
BGRA8,
|
||||
}
|
||||
|
||||
// Computes image byte length, returning None if overflow occurred or the total length exceeds
|
||||
// the maximum image allocation size.
|
||||
pub fn compute_rgba8_byte_length_if_within_limit(width: usize, height: usize) -> Option<usize> {
|
||||
// Maximum allowed image allocation size (2^31-1 ~ 2GB).
|
||||
const MAX_IMAGE_BYTE_LENGTH: usize = 2147483647;
|
||||
|
||||
// The color components of each pixel must be stored in four sequential
|
||||
// elements in the order of red, green, blue, and then alpha.
|
||||
4usize
|
||||
.checked_mul(width)
|
||||
.and_then(|v| v.checked_mul(height))
|
||||
.filter(|v| *v <= MAX_IMAGE_BYTE_LENGTH)
|
||||
}
|
||||
|
||||
pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u64>, rect: Rect<u64>) -> Cow<[u8]> {
|
||||
assert!(!rect.is_empty());
|
||||
assert!(Rect::from_size(size).contains_rect(&rect));
|
||||
|
@ -120,48 +135,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 +202,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 +221,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 {
|
||||
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 +389,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,46 +399,61 @@ fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> {
|
|||
// This uses `map_while`, because the first non-decodable frame seems to
|
||||
// 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,
|
||||
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)]
|
||||
mod test {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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,31 +100,20 @@ 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
|
||||
},
|
||||
|
||||
ProfilerMsg::Exit => false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,24 +3,33 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use dom_struct::dom_struct;
|
||||
use js::jsapi::Value;
|
||||
use js::rust::{Handle, HandleObject};
|
||||
use js::rust::{HandleObject, HandleValue};
|
||||
|
||||
use crate::dom::abortsignal::AbortSignal;
|
||||
use crate::dom::bindings::codegen::Bindings::AbortControllerBinding::AbortControllerMethods;
|
||||
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
|
||||
use crate::dom::bindings::root::DomRoot;
|
||||
use crate::dom::bindings::root::{Dom, DomRoot};
|
||||
use crate::dom::globalscope::GlobalScope;
|
||||
use crate::script_runtime::{CanGc, JSContext};
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortcontroller>
|
||||
#[dom_struct]
|
||||
pub(crate) struct AbortController {
|
||||
reflector_: Reflector,
|
||||
|
||||
/// An AbortController object has an associated signal (an AbortSignal object).
|
||||
signal: Dom<AbortSignal>,
|
||||
}
|
||||
|
||||
impl AbortController {
|
||||
/// <https://dom.spec.whatwg.org/#dom-abortcontroller-abortcontroller>
|
||||
fn new_inherited() -> AbortController {
|
||||
// The new AbortController() constructor steps are:
|
||||
// Let signal be a new AbortSignal object.
|
||||
// Set this’s signal to signal.
|
||||
AbortController {
|
||||
reflector_: Reflector::new(),
|
||||
signal: Dom::from_ref(&AbortSignal::new_inherited()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +45,12 @@ impl AbortController {
|
|||
can_gc,
|
||||
)
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortcontroller-signal-abort>
|
||||
fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
|
||||
// signal abort on controller’s signal with reason if it is given.
|
||||
self.signal.signal_abort(cx, reason, can_gc);
|
||||
}
|
||||
}
|
||||
|
||||
impl AbortControllerMethods<crate::DomTypeHolder> for AbortController {
|
||||
|
@ -49,5 +64,9 @@ impl AbortControllerMethods<crate::DomTypeHolder> for AbortController {
|
|||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#dom-abortcontroller-abort>
|
||||
fn Abort(&self, _cx: JSContext, _reason: Handle<'_, Value>) {}
|
||||
fn Abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
|
||||
// The abort(reason) method steps are
|
||||
// to signal abort on this with reason if it is given.
|
||||
self.signal_abort(cx, reason, can_gc);
|
||||
}
|
||||
}
|
||||
|
|
140
components/script/dom/abortsignal.rs
Normal file
140
components/script/dom/abortsignal.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::mem;
|
||||
|
||||
use dom_struct::dom_struct;
|
||||
use js::jsapi::Heap;
|
||||
use js::jsval::{JSVal, UndefinedValue};
|
||||
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
|
||||
use script_bindings::inheritance::Castable;
|
||||
|
||||
use crate::dom::bindings::codegen::Bindings::AbortSignalBinding::AbortSignalMethods;
|
||||
use crate::dom::bindings::error::{Error, ErrorToJsval};
|
||||
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
|
||||
use crate::dom::bindings::root::DomRoot;
|
||||
use crate::dom::eventtarget::EventTarget;
|
||||
use crate::dom::globalscope::GlobalScope;
|
||||
use crate::script_runtime::{CanGc, JSContext};
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortcontroller-api-integration>
|
||||
/// TODO: implement algorithms at call point,
|
||||
/// in order to integrate the abort signal with its various use cases.
|
||||
#[derive(JSTraceable, MallocSizeOf)]
|
||||
#[allow(dead_code)]
|
||||
enum AbortAlgorithm {
|
||||
/// <https://dom.spec.whatwg.org/#add-an-event-listener>
|
||||
DomEventLister,
|
||||
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
|
||||
StreamPiping,
|
||||
/// <https://fetch.spec.whatwg.org/#dom-global-fetch>
|
||||
Fetch,
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortsignal>
|
||||
#[dom_struct]
|
||||
pub(crate) struct AbortSignal {
|
||||
eventtarget: EventTarget,
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortsignal-abort-reason>
|
||||
#[ignore_malloc_size_of = "mozjs"]
|
||||
abort_reason: Heap<JSVal>,
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortsignal-abort-algorithms>
|
||||
abort_algorithms: RefCell<Vec<AbortAlgorithm>>,
|
||||
}
|
||||
|
||||
impl AbortSignal {
|
||||
pub(crate) fn new_inherited() -> AbortSignal {
|
||||
AbortSignal {
|
||||
eventtarget: EventTarget::new_inherited(),
|
||||
abort_reason: Default::default(),
|
||||
abort_algorithms: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn new_with_proto(
|
||||
global: &GlobalScope,
|
||||
proto: Option<HandleObject>,
|
||||
can_gc: CanGc,
|
||||
) -> DomRoot<AbortSignal> {
|
||||
reflect_dom_object_with_proto(
|
||||
Box::new(AbortSignal::new_inherited()),
|
||||
global,
|
||||
proto,
|
||||
can_gc,
|
||||
)
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#abortsignal-signal-abort>
|
||||
pub(crate) fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) {
|
||||
// If signal is aborted, then return.
|
||||
if self.Aborted() {
|
||||
return;
|
||||
}
|
||||
|
||||
let abort_reason = reason.get();
|
||||
|
||||
// Set signal’s abort reason to reason if it is given;
|
||||
if !abort_reason.is_undefined() {
|
||||
self.abort_reason.set(abort_reason);
|
||||
} else {
|
||||
// otherwise to a new "AbortError" DOMException.
|
||||
rooted!(in(*cx) let mut rooted_error = UndefinedValue());
|
||||
Error::Abort.to_jsval(cx, &self.global(), rooted_error.handle_mut(), can_gc);
|
||||
self.abort_reason.set(rooted_error.get())
|
||||
}
|
||||
|
||||
// Let dependentSignalsToAbort be a new list.
|
||||
// For each dependentSignal of signal’s dependent signals:
|
||||
// TODO: #36936
|
||||
|
||||
// Run the abort steps for signal.
|
||||
self.run_the_abort_steps(can_gc);
|
||||
|
||||
// For each dependentSignal of dependentSignalsToAbort, run the abort steps for dependentSignal.
|
||||
// TODO: #36936
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#run-the-abort-steps>
|
||||
fn run_the_abort_steps(&self, can_gc: CanGc) {
|
||||
// For each algorithm of signal’s abort algorithms: run algorithm.
|
||||
let algos = mem::take(&mut *self.abort_algorithms.borrow_mut());
|
||||
for _algo in algos {
|
||||
// TODO: match on variant and implement algo steps.
|
||||
// See the various items of #34866
|
||||
}
|
||||
|
||||
// Empty signal’s abort algorithms.
|
||||
// Done above with `take`.
|
||||
|
||||
// Fire an event named abort at signal.
|
||||
self.upcast::<EventTarget>()
|
||||
.fire_event(atom!("abort"), can_gc);
|
||||
}
|
||||
}
|
||||
|
||||
impl AbortSignalMethods<crate::DomTypeHolder> for AbortSignal {
|
||||
/// <https://dom.spec.whatwg.org/#dom-abortsignal-aborted>
|
||||
fn Aborted(&self) -> bool {
|
||||
// TODO
|
||||
false
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#dom-abortsignal-reason>
|
||||
fn Reason(&self, _: JSContext, _rval: MutableHandleValue) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted>
|
||||
#[allow(unsafe_code)]
|
||||
fn ThrowIfAborted(&self) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// <https://dom.spec.whatwg.org/#dom-abortsignal-onabort>
|
||||
event_handler!(abort, GetOnabort, SetOnabort);
|
||||
}
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
let id = self.get_blob_url_id();
|
||||
|
||||
global.read_file_async(
|
||||
id,
|
||||
p.clone(),
|
||||
Box::new(|promise, bytes| {
|
||||
match bytes {
|
||||
Ok(b) => {
|
||||
fn ArrayBuffer(&self, in_realm: InRealm, can_gc: CanGc) -> Rc<Promise> {
|
||||
let cx = GlobalScope::get_cx();
|
||||
let result = run_array_buffer_data_algorithm(cx, b, CanGc::note());
|
||||
let global = GlobalScope::from_safe_context(cx, in_realm);
|
||||
let promise = Promise::new_in_current_realm(in_realm, can_gc);
|
||||
|
||||
match result {
|
||||
Ok(FetchedData::ArrayBuffer(a)) => {
|
||||
promise.resolve_native(&a, CanGc::note())
|
||||
// 1. Let stream be the result of calling get stream on this.
|
||||
let stream = self.get_stream(can_gc);
|
||||
|
||||
// 2. Let reader be the result of getting a reader from stream.
|
||||
// If that threw an exception, return a new promise rejected with that exception.
|
||||
let reader = match stream.and_then(|s| s.acquire_default_reader(can_gc)) {
|
||||
Ok(reader) => reader,
|
||||
Err(error) => {
|
||||
promise.reject_error(error, can_gc);
|
||||
return promise;
|
||||
},
|
||||
Err(e) => promise.reject_error(e, CanGc::note()),
|
||||
_ => panic!("Unexpected result from run_array_buffer_data_algorithm"),
|
||||
}
|
||||
},
|
||||
Err(e) => promise.reject_error(e, CanGc::note()),
|
||||
};
|
||||
|
||||
// 3. Let promise be the result of reading all bytes from stream with reader.
|
||||
let success_promise = promise.clone();
|
||||
let failure_promise = promise.clone();
|
||||
reader.read_all_bytes(
|
||||
cx,
|
||||
&global,
|
||||
Rc::new(move |bytes| {
|
||||
rooted!(in(*cx) let mut js_object = ptr::null_mut::<JSObject>());
|
||||
// 4. Return the result of transforming promise by a fulfillment handler that returns a new
|
||||
// [ArrayBuffer]
|
||||
let array_buffer = create_buffer_source::<ArrayBufferU8>(
|
||||
cx,
|
||||
bytes,
|
||||
js_object.handle_mut(),
|
||||
can_gc,
|
||||
)
|
||||
.expect("Converting input to ArrayBufferU8 should never fail");
|
||||
success_promise.resolve_native(&array_buffer, can_gc);
|
||||
}),
|
||||
Rc::new(move |cx, value| {
|
||||
failure_promise.reject(cx, value, can_gc);
|
||||
}),
|
||||
in_realm,
|
||||
can_gc,
|
||||
);
|
||||
p
|
||||
|
||||
promise
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/FileAPI/#dom-blob-bytes>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1557,11 +1557,14 @@ impl Document {
|
|||
return;
|
||||
}
|
||||
|
||||
// For a node within a text input UA shadow DOM, delegate the focus target into its shadow host.
|
||||
// TODO: This focus delegation should be done with shadow DOM delegateFocus attribute.
|
||||
let target_el = el.find_focusable_shadow_host_if_necessary();
|
||||
|
||||
self.begin_focus_transaction();
|
||||
// Try to focus `el`. If it's not focusable, focus the document
|
||||
// instead.
|
||||
// Try to focus `el`. If it's not focusable, focus the document instead.
|
||||
self.request_focus(None, FocusInitiator::Local, can_gc);
|
||||
self.request_focus(Some(&*el), FocusInitiator::Local, can_gc);
|
||||
self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
|
||||
}
|
||||
|
||||
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
|
||||
|
@ -2375,6 +2378,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 &&
|
||||
|
@ -5060,6 +5066,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.
|
||||
|
@ -6756,6 +6791,21 @@ impl FakeRequestAnimationFrameCallback {
|
|||
}
|
||||
}
|
||||
|
||||
/// This is a temporary workaround to update animated images,
|
||||
/// we should get rid of this after we have refresh driver #3406
|
||||
#[derive(JSTraceable, MallocSizeOf)]
|
||||
pub(crate) struct ImageAnimationUpdateCallback {
|
||||
/// The document.
|
||||
#[ignore_malloc_size_of = "non-owning"]
|
||||
document: Trusted<Document>,
|
||||
}
|
||||
|
||||
impl ImageAnimationUpdateCallback {
|
||||
pub(crate) fn invoke(self, can_gc: CanGc) {
|
||||
with_script_thread(|script_thread| script_thread.update_the_rendering(true, can_gc))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(JSTraceable, MallocSizeOf)]
|
||||
pub(crate) enum AnimationFrameCallback {
|
||||
DevtoolsFramerateTick {
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::str::FromStr;
|
|||
use std::{fmt, mem};
|
||||
|
||||
use content_security_policy as csp;
|
||||
use cssparser::match_ignore_ascii_case;
|
||||
use cssparser::{Parser as CssParser, ParserInput as CssParserInput, match_ignore_ascii_case};
|
||||
use devtools_traits::AttrInfo;
|
||||
use dom_struct::dom_struct;
|
||||
use embedder_traits::InputMethodType;
|
||||
|
@ -36,6 +36,8 @@ use style::applicable_declarations::ApplicableDeclarationBlock;
|
|||
use style::attr::{AttrValue, LengthOrPercentageOrAuto};
|
||||
use style::context::QuirksMode;
|
||||
use style::invalidation::element::restyle_hints::RestyleHint;
|
||||
use style::media_queries::MediaList;
|
||||
use style::parser::ParserContext as CssParserContext;
|
||||
use style::properties::longhands::{
|
||||
self, background_image, border_spacing, font_family, font_size,
|
||||
};
|
||||
|
@ -50,13 +52,14 @@ use style::selector_parser::{
|
|||
};
|
||||
use style::shared_lock::{Locked, SharedRwLock};
|
||||
use style::stylesheets::layer_rule::LayerOrder;
|
||||
use style::stylesheets::{CssRuleType, UrlExtraData};
|
||||
use style::stylesheets::{CssRuleType, Origin as CssOrigin, UrlExtraData};
|
||||
use style::values::computed::Overflow;
|
||||
use style::values::generics::NonNegative;
|
||||
use style::values::generics::position::PreferredRatio;
|
||||
use style::values::generics::ratio::Ratio;
|
||||
use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified};
|
||||
use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state};
|
||||
use style_traits::ParsingMode as CssParsingMode;
|
||||
use stylo_atoms::Atom;
|
||||
use stylo_dom::ElementState;
|
||||
use xml5ever::serialize::TraversalScope::{
|
||||
|
@ -66,7 +69,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::{
|
||||
|
@ -781,6 +784,33 @@ impl Element {
|
|||
.registered_intersection_observers
|
||||
.retain(|reg_obs| *reg_obs.observer != *observer)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#matches-the-environment>
|
||||
pub(crate) fn matches_environment(&self, media_query: &str) -> bool {
|
||||
let document = self.owner_document();
|
||||
let quirks_mode = document.quirks_mode();
|
||||
let document_url_data = UrlExtraData(document.url().get_arc());
|
||||
// FIXME(emilio): This should do the same that we do for other media
|
||||
// lists regarding the rule type and such, though it doesn't really
|
||||
// matter right now...
|
||||
//
|
||||
// Also, ParsingMode::all() is wrong, and should be DEFAULT.
|
||||
let context = CssParserContext::new(
|
||||
CssOrigin::Author,
|
||||
&document_url_data,
|
||||
Some(CssRuleType::Style),
|
||||
CssParsingMode::all(),
|
||||
quirks_mode,
|
||||
/* namespaces = */ Default::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut parser_input = CssParserInput::new(media_query);
|
||||
let mut parser = CssParser::new(&mut parser_input);
|
||||
let media_list = MediaList::parse(&context, &mut parser);
|
||||
let result = media_list.evaluate(document.window().layout().device(), quirks_mode);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://dom.spec.whatwg.org/#valid-shadow-host-name>
|
||||
|
@ -1480,7 +1510,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 +1523,8 @@ impl Element {
|
|||
_ => false,
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Some(attr) = attr {
|
||||
return (**attr.value()).into();
|
||||
|
@ -1631,6 +1662,27 @@ impl Element {
|
|||
)
|
||||
}
|
||||
|
||||
/// Returns the focusable shadow host if this is a text control inner editor.
|
||||
/// This is a workaround for the focus delegation of shadow DOM and should be
|
||||
/// used only to delegate focusable inner editor of [HTMLInputElement] and
|
||||
/// [HTMLTextAreaElement].
|
||||
pub(crate) fn find_focusable_shadow_host_if_necessary(&self) -> Option<DomRoot<Element>> {
|
||||
if self.is_focusable_area() {
|
||||
Some(DomRoot::from_ref(self))
|
||||
} else if self.upcast::<Node>().is_text_control_inner_editor() {
|
||||
let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host());
|
||||
assert!(
|
||||
containing_shadow_host
|
||||
.as_ref()
|
||||
.is_some_and(|e| e.is_focusable_area()),
|
||||
"Containing shadow host is not focusable"
|
||||
);
|
||||
containing_shadow_host
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_actually_disabled(&self) -> bool {
|
||||
let node = self.upcast::<Node>();
|
||||
match node.type_id() {
|
||||
|
|
|
@ -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,10 +1440,13 @@ 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()) {
|
||||
// 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 port’s 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(
|
||||
CrossRealmTransform::Readable(readable) => {
|
||||
readable.handle_message(
|
||||
cx,
|
||||
self,
|
||||
&dom_port,
|
||||
|
@ -1458,15 +1454,14 @@ impl GlobalScope {
|
|||
comp,
|
||||
can_gc,
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
// Add a handler for port’s 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);
|
||||
CrossRealmTransform::Writable(writable) => {
|
||||
writable.handle_message(cx, self, message_clone.handle(), comp, can_gc);
|
||||
},
|
||||
}
|
||||
|
||||
if !has_cross_realm_tansform {
|
||||
} else {
|
||||
// Fire an event named message at messageEventTarget,
|
||||
// using MessageEvent,
|
||||
// with the data attribute initialized to messageClone
|
||||
|
@ -1481,20 +1476,20 @@ impl GlobalScope {
|
|||
can_gc,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} else if let Some(transform) = cross_realm_transform.as_ref() {
|
||||
match transform {
|
||||
// Add a handler for port’s 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);
|
||||
}
|
||||
|
||||
CrossRealmTransform::Readable(readable) => {
|
||||
readable.handle_error(cx, self, &dom_port, comp, can_gc);
|
||||
},
|
||||
// Add a handler for port’s 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);
|
||||
CrossRealmTransform::Writable(writable) => {
|
||||
writable.handle_error(cx, self, &dom_port, comp, can_gc);
|
||||
},
|
||||
}
|
||||
|
||||
if !has_cross_realm_tansform {
|
||||
} else {
|
||||
// If this throws an exception, catch it,
|
||||
// fire an event named messageerror at messageEventTarget,
|
||||
// using MessageEvent, and then return.
|
||||
|
@ -1502,7 +1497,6 @@ impl GlobalScope {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check all ports that have been transfer-received in the previous task,
|
||||
/// and complete their transfer if they haven't been re-transferred.
|
||||
|
@ -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(
|
||||
|
@ -2990,13 +2982,13 @@ impl GlobalScope {
|
|||
return p;
|
||||
}
|
||||
|
||||
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());
|
||||
match canvas.get_image_data() {
|
||||
Some(snapshot) => {
|
||||
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);
|
||||
},
|
||||
None => p.reject_error(Error::InvalidState, can_gc),
|
||||
}
|
||||
p
|
||||
},
|
||||
|
@ -3007,13 +2999,13 @@ impl GlobalScope {
|
|||
return p;
|
||||
}
|
||||
|
||||
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());
|
||||
match canvas.get_image_data() {
|
||||
Some(snapshot) => {
|
||||
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);
|
||||
},
|
||||
None => p.reject_error(Error::InvalidState, can_gc),
|
||||
}
|
||||
p
|
||||
},
|
||||
|
|
|
@ -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 headers’s guard is "request-no-cors":
|
||||
if self.guard.get() == Guard::RequestNoCors {
|
||||
// 3.1. Let temporaryValue be the result of getting name from headers’s 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 headers’s 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 headers’s 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 this’s 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 this’s header list does not contain name, then return.
|
||||
// 4. Delete name from this’s header list.
|
||||
self.header_list.borrow_mut().remove(valid_name);
|
||||
|
||||
// 5. If this’s 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 this’s 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 this’s header list does not contain `Set-Cookie`, then return « ».
|
||||
// 2. Return the values of all headers in this’s 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 this’s 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 this’s 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 this’s 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 this’s 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 headers’s guard is "immutable", then throw a TypeError.
|
||||
if self.guard.get() == Guard::Immutable {
|
||||
return Err(Error::Type("Guard is immutable".to_string()));
|
||||
}
|
||||
// 3. If headers’s 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 headers’s 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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use html5ever::{LocalName, Prefix, local_name, ns};
|
|||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::codecs::png::PngEncoder;
|
||||
use image::codecs::webp::WebPEncoder;
|
||||
use image::{ColorType, ImageEncoder};
|
||||
use image::{ColorType, ImageEncoder, ImageError};
|
||||
#[cfg(feature = "webgpu")]
|
||||
use ipc_channel::ipc::{self as ipcchan};
|
||||
use js::error::throw_type_error;
|
||||
|
@ -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>> {
|
||||
|
@ -361,7 +362,13 @@ impl HTMLCanvasElement {
|
|||
Some(context) => context.get_image_data(),
|
||||
None => {
|
||||
let size = self.get_size();
|
||||
if size.width == 0 || size.height == 0 {
|
||||
if size.is_empty() ||
|
||||
pixels::compute_rgba8_byte_length_if_within_limit(
|
||||
size.width as usize,
|
||||
size.height as usize,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Snapshot::cleared(size.cast()))
|
||||
|
@ -384,7 +391,7 @@ impl HTMLCanvasElement {
|
|||
quality: Option<f64>,
|
||||
snapshot: &Snapshot,
|
||||
encoder: &mut W,
|
||||
) {
|
||||
) -> Result<(), ImageError> {
|
||||
// We can't use self.Width() or self.Height() here, since the size of the canvas
|
||||
// may have changed since the snapshot was created. Truncating the dimensions to a
|
||||
// u32 can't panic, since the data comes from a canvas which is always smaller than
|
||||
|
@ -397,9 +404,7 @@ impl HTMLCanvasElement {
|
|||
EncodedImageType::Png => {
|
||||
// FIXME(nox): https://github.com/image-rs/image-png/issues/86
|
||||
// FIXME(nox): https://github.com/image-rs/image-png/issues/87
|
||||
PngEncoder::new(encoder)
|
||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||
.unwrap();
|
||||
PngEncoder::new(encoder).write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||
},
|
||||
EncodedImageType::Jpeg => {
|
||||
let jpeg_encoder = if let Some(quality) = quality {
|
||||
|
@ -417,16 +422,16 @@ impl HTMLCanvasElement {
|
|||
JpegEncoder::new(encoder)
|
||||
};
|
||||
|
||||
jpeg_encoder
|
||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||
.unwrap();
|
||||
jpeg_encoder.write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||
},
|
||||
|
||||
EncodedImageType::Webp => {
|
||||
// No quality support because of https://github.com/image-rs/image/issues/1984
|
||||
WebPEncoder::new_lossless(encoder)
|
||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||
.unwrap();
|
||||
WebPEncoder::new_lossless(encoder).write_image(
|
||||
canvas_data,
|
||||
width,
|
||||
height,
|
||||
ColorType::Rgba8,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -515,17 +520,22 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
|||
mime_type: DOMString,
|
||||
quality: HandleValue,
|
||||
) -> Fallible<USVString> {
|
||||
// Step 1.
|
||||
// Step 1: If this canvas element's bitmap's origin-clean flag is set to false,
|
||||
// then throw a "SecurityError" DOMException.
|
||||
if !self.origin_is_clean() {
|
||||
return Err(Error::Security);
|
||||
}
|
||||
|
||||
// Step 2.
|
||||
// Step 2: If this canvas element's bitmap has no pixels (i.e. either its
|
||||
// horizontal dimension or its vertical dimension is zero), then return the string
|
||||
// "data:,". (This is the shortest data: URL; it represents the empty string in a
|
||||
// text/plain resource.)
|
||||
if self.Width() == 0 || self.Height() == 0 {
|
||||
return Ok(USVString("data:,".into()));
|
||||
}
|
||||
|
||||
// Step 3.
|
||||
// Step 3: Let file be a serialization of this canvas element's bitmap as a file,
|
||||
// passing type and quality if given.
|
||||
let Some(mut snapshot) = self.get_image_data() else {
|
||||
return Ok(USVString("data:,".into()));
|
||||
};
|
||||
|
@ -550,12 +560,20 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
|||
&base64::engine::general_purpose::STANDARD,
|
||||
);
|
||||
|
||||
self.encode_for_mime_type(
|
||||
if self
|
||||
.encode_for_mime_type(
|
||||
&image_type,
|
||||
Self::maybe_quality(quality),
|
||||
&snapshot,
|
||||
&mut encoder,
|
||||
);
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
// Step 4. If file is null, then return "data:,".
|
||||
return Ok(USVString("data:,".into()));
|
||||
}
|
||||
|
||||
// Step 5. Return a data: URL representing file. [RFC2397]
|
||||
encoder.into_inner();
|
||||
Ok(USVString(url))
|
||||
}
|
||||
|
@ -603,26 +621,37 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
|||
return error!("Expected blob callback, but found none!");
|
||||
};
|
||||
|
||||
if let Some(mut snapshot) = result {
|
||||
let Some(mut snapshot) = result else {
|
||||
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
|
||||
return;
|
||||
};
|
||||
|
||||
snapshot.transform(
|
||||
snapshot::AlphaMode::Transparent{ premultiplied: false },
|
||||
snapshot::PixelFormat::RGBA
|
||||
);
|
||||
// Step 4.1
|
||||
// If result is non-null, then set result to a serialization of result as a file with
|
||||
// type and quality if given.
|
||||
|
||||
// Step 4.1: If result is non-null, then set result to a serialization of
|
||||
// result as a file with type and quality if given.
|
||||
// Step 4.2: Queue an element task on the canvas blob serialization task
|
||||
// source given the canvas element to run these steps:
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
|
||||
this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded);
|
||||
let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
|
||||
// Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element
|
||||
let blob = Blob::new(&this.global(), blob_impl, CanGc::note());
|
||||
|
||||
// Step 4.2.2 Invoke callback with « result » and "report".
|
||||
let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note());
|
||||
} else {
|
||||
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
|
||||
let blob_impl;
|
||||
let blob;
|
||||
let result = match this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded) {
|
||||
Ok(..) => {
|
||||
// Step 4.2.1: If result is non-null, then set result to a new Blob
|
||||
// object, created in the relevant realm of this canvas element,
|
||||
// representing result. [FILEAPI]
|
||||
blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
|
||||
blob = Blob::new(&this.global(), blob_impl, CanGc::note());
|
||||
Some(&*blob)
|
||||
}
|
||||
Err(..) => None,
|
||||
};
|
||||
|
||||
// Step 4.2.2: Invoke callback with « result » and "report".
|
||||
let _ = callback.Call__(result, ExceptionHandling::Report, CanGc::note());
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
|
@ -687,8 +716,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);
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,14 +29,13 @@ 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};
|
||||
use style::context::QuirksMode;
|
||||
use style::media_queries::MediaList;
|
||||
use style::parser::ParserContext;
|
||||
use style::stylesheets::{CssRuleType, Origin, UrlExtraData};
|
||||
use style::stylesheets::{CssRuleType, Origin};
|
||||
use style::values::specified::AbsoluteLength;
|
||||
use style::values::specified::length::{Length, NoCalcLength};
|
||||
use style::values::specified::source_size_list::SourceSizeList;
|
||||
|
@ -146,9 +145,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 +175,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 +190,7 @@ impl HTMLImageElement {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn image_data(&self) -> Option<Arc<Image>> {
|
||||
pub(crate) fn image_data(&self) -> Option<Image> {
|
||||
self.current_request.borrow().image.clone()
|
||||
}
|
||||
}
|
||||
|
@ -341,10 +340,12 @@ impl HTMLImageElement {
|
|||
is_placeholder,
|
||||
}) => {
|
||||
if is_placeholder {
|
||||
if let Some(raster_image) = image.as_raster_image() {
|
||||
self.process_image_response(
|
||||
ImageResponse::PlaceholderLoaded(image, url),
|
||||
ImageResponse::PlaceholderLoaded(raster_image, url),
|
||||
can_gc,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
self.process_image_response(ImageResponse::Loaded(image, url), can_gc)
|
||||
}
|
||||
|
@ -403,7 +404,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 +449,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 +469,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 +481,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 +534,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);
|
||||
|
@ -675,7 +677,7 @@ impl HTMLImageElement {
|
|||
|
||||
// Step 4.6
|
||||
if let Some(x) = element.get_attribute(&ns!(), &local_name!("media")) {
|
||||
if !self.matches_environment(x.value().to_string()) {
|
||||
if !elem.matches_environment(&x.value()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -719,33 +721,6 @@ impl HTMLImageElement {
|
|||
result
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#matches-the-environment>
|
||||
fn matches_environment(&self, media_query: String) -> bool {
|
||||
let document = self.owner_document();
|
||||
let quirks_mode = document.quirks_mode();
|
||||
let document_url_data = UrlExtraData(document.url().get_arc());
|
||||
// FIXME(emilio): This should do the same that we do for other media
|
||||
// lists regarding the rule type and such, though it doesn't really
|
||||
// matter right now...
|
||||
//
|
||||
// Also, ParsingMode::all() is wrong, and should be DEFAULT.
|
||||
let context = ParserContext::new(
|
||||
Origin::Author,
|
||||
&document_url_data,
|
||||
Some(CssRuleType::Style),
|
||||
ParsingMode::all(),
|
||||
quirks_mode,
|
||||
/* namespaces = */ Default::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut parserInput = ParserInput::new(&media_query);
|
||||
let mut parser = Parser::new(&mut parserInput);
|
||||
let media_list = MediaList::parse(&context, &mut parser);
|
||||
let result = media_list.evaluate(document.window().layout().device(), quirks_mode);
|
||||
result
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities>
|
||||
fn normalise_source_densities(&self, source_set: &mut SourceSet, width: Option<Length>) {
|
||||
// Step 1
|
||||
|
@ -1020,10 +995,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 +1005,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 +1332,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 +1404,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 +1421,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> {
|
||||
|
|
|
@ -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,96 @@ 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 reference to text control inner editor and placeholder container element in the UA
|
||||
/// shadow tree for `<input type=text>`. The following is the structure of the shadow tree.
|
||||
///
|
||||
/// ```
|
||||
/// <input type="text">
|
||||
/// #shadow-root
|
||||
/// <div id="inner-container">
|
||||
/// <div id="input-editor"></div>
|
||||
/// <div id="input-placeholder"></div>
|
||||
/// </div>
|
||||
/// </input>
|
||||
/// ```
|
||||
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
|
||||
// But, this could be slower in performance and does have some discrepancies. For example,
|
||||
// they would try to vertically align <input> text baseline with the baseline of other
|
||||
// TextNode within an inline flow. Another example is the horizontal scroll.
|
||||
struct InputTypeTextShadowTree {
|
||||
text_container: Dom<HTMLDivElement>,
|
||||
placeholder_container: Dom<HTMLDivElement>,
|
||||
}
|
||||
|
||||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||||
/// Contains references to the elements in the shadow tree for `<input type=range>`.
|
||||
///
|
||||
/// The shadow tree consists of a single div with the currently selected color as
|
||||
/// the background.
|
||||
struct InputTypeColorShadowTree {
|
||||
color_value: Dom<HTMLDivElement>,
|
||||
}
|
||||
|
||||
// FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support.
|
||||
const TEXT_TREE_STYLE: &str = "
|
||||
#input-editor::selection {
|
||||
background: rgba(176, 214, 255, 1.0);
|
||||
color: black;
|
||||
}
|
||||
|
||||
:host:not(:placeholder-shown) #input-placeholder {
|
||||
visibility: hidden !important
|
||||
}
|
||||
|
||||
#input-editor {
|
||||
overflow-wrap: normal;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#input-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#input-editor, #input-placeholder {
|
||||
white-space: pre;
|
||||
margin-block: auto !important;
|
||||
inset-block: 0 !important;
|
||||
block-size: fit-content !important;
|
||||
}
|
||||
|
||||
#input-placeholder {
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
color: grey;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
";
|
||||
|
||||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||||
#[non_exhaustive]
|
||||
enum ShadowTree {
|
||||
Text(InputTypeTextShadowTree),
|
||||
Color(InputTypeColorShadowTree),
|
||||
// TODO: Add shadow trees for other input types (range etc) here
|
||||
}
|
||||
|
||||
const COLOR_TREE_STYLE: &str = "
|
||||
#color-value {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid gray;
|
||||
border-radius: 2px;
|
||||
}
|
||||
";
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#attr-input-type>
|
||||
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
|
@ -172,7 +271,6 @@ impl InputType {
|
|||
fn is_textual(&self) -> bool {
|
||||
matches!(
|
||||
*self,
|
||||
InputType::Color |
|
||||
InputType::Date |
|
||||
InputType::DatetimeLocal |
|
||||
InputType::Email |
|
||||
|
@ -277,9 +375,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 +419,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 +478,7 @@ impl HTMLInputElement {
|
|||
form_owner: Default::default(),
|
||||
labels_node_list: MutNullableDom::new(None),
|
||||
validity_state: Default::default(),
|
||||
shadow_tree: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -475,6 +582,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 +911,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 +1066,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 +1122,221 @@ 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,
|
||||
true,
|
||||
SlotAssignmentMode::Manual,
|
||||
can_gc,
|
||||
)
|
||||
.expect("Attaching UA shadow root failed")
|
||||
})
|
||||
}
|
||||
|
||||
fn create_text_shadow_tree(&self, can_gc: CanGc) {
|
||||
let document = self.owner_document();
|
||||
let shadow_root = self.shadow_root(can_gc);
|
||||
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
|
||||
|
||||
let inner_container =
|
||||
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
|
||||
inner_container
|
||||
.upcast::<Element>()
|
||||
.SetId(DOMString::from("input-container"), can_gc);
|
||||
shadow_root
|
||||
.upcast::<Node>()
|
||||
.AppendChild(inner_container.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let placeholder_container =
|
||||
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
|
||||
placeholder_container
|
||||
.upcast::<Element>()
|
||||
.SetId(DOMString::from("input-placeholder"), can_gc);
|
||||
inner_container
|
||||
.upcast::<Node>()
|
||||
.AppendChild(placeholder_container.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
|
||||
text_container
|
||||
.upcast::<Element>()
|
||||
.SetId(DOMString::from("input-editor"), can_gc);
|
||||
text_container
|
||||
.upcast::<Node>()
|
||||
.set_text_control_inner_editor();
|
||||
inner_container
|
||||
.upcast::<Node>()
|
||||
.AppendChild(text_container.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let style = HTMLStyleElement::new(
|
||||
local_name!("style"),
|
||||
None,
|
||||
&document,
|
||||
None,
|
||||
ElementCreator::ScriptCreated,
|
||||
can_gc,
|
||||
);
|
||||
// TODO(stevennovaryo): Either use UA stylesheet with internal pseudo element or preemptively parse
|
||||
// the stylesheet to reduce the costly operation and avoid CSP related error.
|
||||
style
|
||||
.upcast::<Node>()
|
||||
.SetTextContent(Some(DOMString::from(TEXT_TREE_STYLE)), can_gc);
|
||||
shadow_root
|
||||
.upcast::<Node>()
|
||||
.AppendChild(style.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let _ = self
|
||||
.shadow_tree
|
||||
.borrow_mut()
|
||||
.insert(ShadowTree::Text(InputTypeTextShadowTree {
|
||||
text_container: text_container.as_traced(),
|
||||
placeholder_container: placeholder_container.as_traced(),
|
||||
}));
|
||||
}
|
||||
|
||||
fn text_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeTextShadowTree> {
|
||||
let has_text_shadow_tree = self
|
||||
.shadow_tree
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Text(_)));
|
||||
if !has_text_shadow_tree {
|
||||
self.create_text_shadow_tree(can_gc);
|
||||
}
|
||||
|
||||
let shadow_tree = self.shadow_tree.borrow();
|
||||
Ref::filter_map(shadow_tree, |shadow_tree| {
|
||||
let shadow_tree = shadow_tree.as_ref()?;
|
||||
match shadow_tree {
|
||||
ShadowTree::Text(text_tree) => Some(text_tree),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.expect("UA shadow tree was not created")
|
||||
}
|
||||
|
||||
fn create_color_shadow_tree(&self, can_gc: CanGc) {
|
||||
let document = self.owner_document();
|
||||
let shadow_root = self.shadow_root(can_gc);
|
||||
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
|
||||
|
||||
let color_value = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
|
||||
color_value
|
||||
.upcast::<Element>()
|
||||
.SetId(DOMString::from("color-value"), can_gc);
|
||||
shadow_root
|
||||
.upcast::<Node>()
|
||||
.AppendChild(color_value.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let style = HTMLStyleElement::new(
|
||||
local_name!("style"),
|
||||
None,
|
||||
&document,
|
||||
None,
|
||||
ElementCreator::ScriptCreated,
|
||||
can_gc,
|
||||
);
|
||||
style
|
||||
.upcast::<Node>()
|
||||
.SetTextContent(Some(DOMString::from(COLOR_TREE_STYLE)), can_gc);
|
||||
shadow_root
|
||||
.upcast::<Node>()
|
||||
.AppendChild(style.upcast::<Node>(), can_gc)
|
||||
.unwrap();
|
||||
|
||||
let _ = self
|
||||
.shadow_tree
|
||||
.borrow_mut()
|
||||
.insert(ShadowTree::Color(InputTypeColorShadowTree {
|
||||
color_value: color_value.as_traced(),
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get a handle to the shadow tree for this input, assuming it's [InputType] is `Color`.
|
||||
///
|
||||
/// If the input is not currently a shadow host, a new shadow tree will be created.
|
||||
///
|
||||
/// If the input is a shadow host for a different kind of shadow tree then the old
|
||||
/// tree will be removed and a new one will be created.
|
||||
fn color_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeColorShadowTree> {
|
||||
let has_color_shadow_tree = self
|
||||
.shadow_tree
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Color(_)));
|
||||
if !has_color_shadow_tree {
|
||||
self.create_color_shadow_tree(can_gc);
|
||||
}
|
||||
|
||||
let shadow_tree = self.shadow_tree.borrow();
|
||||
Ref::filter_map(shadow_tree, |shadow_tree| {
|
||||
let shadow_tree = shadow_tree.as_ref()?;
|
||||
match shadow_tree {
|
||||
ShadowTree::Color(color_tree) => Some(color_tree),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.expect("UA shadow tree was not created")
|
||||
}
|
||||
|
||||
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
|
||||
match self.input_type() {
|
||||
InputType::Text => {
|
||||
let text_shadow_tree = self.text_shadow_tree(can_gc);
|
||||
let value = self.Value();
|
||||
|
||||
// The addition of zero-width space here forces the text input to have an inline formatting
|
||||
// context that might otherwise be trimmed if there's no text. This is important to ensure
|
||||
// that the input element is at least as tall as the line gap of the caret:
|
||||
// <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
|
||||
//
|
||||
// This is also used to ensure that the caret will still be rendered when the input is empty.
|
||||
// TODO: Is there a less hacky way to do this?
|
||||
let value_text = match value.is_empty() {
|
||||
false => value,
|
||||
true => "\u{200B}".into(),
|
||||
};
|
||||
|
||||
text_shadow_tree
|
||||
.text_container
|
||||
.upcast::<Node>()
|
||||
.SetTextContent(Some(value_text), can_gc);
|
||||
},
|
||||
InputType::Color => {
|
||||
let color_shadow_tree = self.color_shadow_tree(can_gc);
|
||||
let mut value = self.Value();
|
||||
if value.str().is_valid_simple_color_string() {
|
||||
value.make_ascii_lowercase();
|
||||
} else {
|
||||
value = DOMString::from("#000000");
|
||||
}
|
||||
let style = format!("background-color: {value}");
|
||||
color_shadow_tree
|
||||
.color_value
|
||||
.upcast::<Element>()
|
||||
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait LayoutHTMLInputElementHelpers<'dom> {
|
||||
/// 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 +1395,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()
|
||||
}
|
||||
},
|
||||
None => DEFAULT_FILE_INPUT_VALUE.into(),
|
||||
}
|
||||
|
@ -1103,6 +1422,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 +1500,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 +1579,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
|
||||
|
@ -1317,6 +1639,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
|
|||
fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult {
|
||||
match self.value_mode() {
|
||||
ValueMode::Value => {
|
||||
{
|
||||
// Step 3.
|
||||
self.value_dirty.set(true);
|
||||
|
||||
|
@ -1333,6 +1656,12 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
|
|||
// Step 5.
|
||||
textinput.clear_selection_to_limit(Direction::Forward);
|
||||
}
|
||||
}
|
||||
|
||||
// Additionaly, update the placeholder shown state. This is
|
||||
// normally being done in the attributed mutated. And, being
|
||||
// done in another scope to prevent borrow checker issues.
|
||||
self.update_placeholder_shown_state();
|
||||
},
|
||||
ValueMode::Default | ValueMode::DefaultOn => {
|
||||
self.upcast::<Element>()
|
||||
|
@ -1349,7 +1678,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
|
|||
},
|
||||
}
|
||||
|
||||
update_related_validity_states(self, can_gc);
|
||||
self.value_changed(can_gc);
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1724,18 +2053,6 @@ fn perform_radio_group_validation(elem: &HTMLInputElement, group: Option<&Atom>,
|
|||
}
|
||||
}
|
||||
|
||||
fn update_related_validity_states(elem: &HTMLInputElement, can_gc: CanGc) {
|
||||
match elem.input_type() {
|
||||
InputType::Radio => {
|
||||
perform_radio_group_validation(elem, elem.radio_group_name().as_ref(), can_gc)
|
||||
},
|
||||
_ => {
|
||||
elem.validity_state()
|
||||
.perform_validation_and_update(ValidationFlags::all(), can_gc);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#radio-button-group
|
||||
fn in_same_group(
|
||||
other: &HTMLInputElement,
|
||||
|
@ -1905,7 +2222,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 => (),
|
||||
_ => (),
|
||||
|
@ -1927,6 +2244,19 @@ impl HTMLInputElement {
|
|||
el.set_placeholder_shown_state(has_placeholder && !has_value);
|
||||
}
|
||||
|
||||
// Update the placeholder text in the text shadow tree.
|
||||
// To increase the performance, we would only do this when it is necessary.
|
||||
fn update_text_shadow_tree_placeholder(&self, can_gc: CanGc) {
|
||||
if self.input_type() != InputType::Text {
|
||||
return;
|
||||
}
|
||||
|
||||
self.text_shadow_tree(can_gc)
|
||||
.placeholder_container
|
||||
.upcast::<Node>()
|
||||
.SetTextContent(Some(self.placeholder.borrow().clone()), can_gc);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file)
|
||||
// Select files by invoking UI or by passed in argument
|
||||
fn select_files(&self, opt_test_paths: Option<Vec<DOMString>>, can_gc: CanGc) {
|
||||
|
@ -1949,7 +2279,8 @@ impl HTMLInputElement {
|
|||
.collect()
|
||||
});
|
||||
|
||||
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
|
||||
let (chan, recv) =
|
||||
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
|
||||
.expect("Error initializing channel");
|
||||
let msg =
|
||||
FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths);
|
||||
|
@ -1977,7 +2308,8 @@ impl HTMLInputElement {
|
|||
None => None,
|
||||
};
|
||||
|
||||
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
|
||||
let (chan, recv) =
|
||||
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
|
||||
.expect("Error initializing channel");
|
||||
let msg =
|
||||
FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path);
|
||||
|
@ -2348,6 +2680,70 @@ impl HTMLInputElement {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn update_related_validity_states(&self, can_gc: CanGc) {
|
||||
match self.input_type() {
|
||||
InputType::Radio => {
|
||||
perform_radio_group_validation(self, self.radio_group_name().as_ref(), can_gc)
|
||||
},
|
||||
_ => {
|
||||
self.validity_state()
|
||||
.perform_validation_and_update(ValidationFlags::all(), can_gc);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn value_changed(&self, can_gc: CanGc) {
|
||||
self.update_related_validity_states(can_gc);
|
||||
self.update_shadow_tree_if_needed(can_gc);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#show-the-picker,-if-applicable>
|
||||
fn show_the_picker_if_applicable(&self, can_gc: CanGc) {
|
||||
// FIXME: Implement most of this algorithm
|
||||
|
||||
// Step 2. If element is not mutable, then return.
|
||||
if !self.is_mutable() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 6. Otherwise, the user agent should show the relevant user interface for selecting a value for element,
|
||||
// in the way it normally would when the user interacts with the control.
|
||||
if self.input_type() == InputType::Color {
|
||||
let (ipc_sender, ipc_receiver) =
|
||||
ipc::channel::<Option<RgbColor>>().expect("Failed to create IPC channel!");
|
||||
let document = self.owner_document();
|
||||
let rect = self.upcast::<Node>().bounding_content_box_or_zero(can_gc);
|
||||
let rect = Rect::new(
|
||||
Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
|
||||
Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
|
||||
);
|
||||
let current_value = self.Value();
|
||||
let current_color = RgbColor {
|
||||
red: u8::from_str_radix(¤t_value[1..3], 16).unwrap(),
|
||||
green: u8::from_str_radix(¤t_value[3..5], 16).unwrap(),
|
||||
blue: u8::from_str_radix(¤t_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 +2755,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 +2863,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() {
|
||||
|
@ -2484,8 +2882,11 @@ impl VirtualMethods for HTMLInputElement {
|
|||
},
|
||||
}
|
||||
|
||||
self.update_text_shadow_tree_placeholder(can_gc);
|
||||
self.update_placeholder_shown_state();
|
||||
},
|
||||
// FIXME(stevennovaryo): This is only reachable by Default and DefaultOn value mode. While others
|
||||
// are being handled in [Self::SetValue]. Should we merge this two together?
|
||||
local_name!("value") if !self.value_dirty.get() => {
|
||||
let value = mutation.new_value(attr).map(|value| (**value).to_owned());
|
||||
let mut value = value.map_or(DOMString::new(), DOMString::from);
|
||||
|
@ -2493,6 +2894,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(
|
||||
|
@ -2532,6 +2935,7 @@ impl VirtualMethods for HTMLInputElement {
|
|||
.extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r'));
|
||||
}
|
||||
}
|
||||
self.update_text_shadow_tree_placeholder(can_gc);
|
||||
self.update_placeholder_shown_state();
|
||||
},
|
||||
local_name!("readonly") => {
|
||||
|
@ -2553,7 +2957,7 @@ impl VirtualMethods for HTMLInputElement {
|
|||
_ => {},
|
||||
}
|
||||
|
||||
update_related_validity_states(self, can_gc);
|
||||
self.value_changed(can_gc);
|
||||
}
|
||||
|
||||
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
|
||||
|
@ -2584,7 +2988,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 +3081,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 +3108,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 +3138,7 @@ impl VirtualMethods for HTMLInputElement {
|
|||
}
|
||||
}
|
||||
|
||||
update_related_validity_states(self, can_gc);
|
||||
self.value_changed(can_gc);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext
|
||||
|
@ -2752,7 +3160,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 +3269,8 @@ impl Activatable for HTMLInputElement {
|
|||
},
|
||||
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):input-activation-behavior
|
||||
// https://html.spec.whatwg.org/multipage/#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 +3316,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 +3375,7 @@ impl Activatable for HTMLInputElement {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
update_related_validity_states(self, can_gc);
|
||||
self.value_changed(can_gc);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#input-activation-behavior>
|
||||
|
@ -3028,6 +3437,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);
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use std::borrow::{Borrow, ToOwned};
|
||||
use std::cell::Cell;
|
||||
use std::default::Default;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base::id::WebViewId;
|
||||
use content_security_policy as csp;
|
||||
|
@ -12,6 +13,8 @@ use dom_struct::dom_struct;
|
|||
use embedder_traits::EmbedderMsg;
|
||||
use html5ever::{LocalName, Prefix, local_name, ns};
|
||||
use js::rust::HandleObject;
|
||||
use mime::Mime;
|
||||
use net_traits::mime_classifier::{MediaType, MimeClassifier};
|
||||
use net_traits::policy_container::PolicyContainer;
|
||||
use net_traits::request::{
|
||||
CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder,
|
||||
|
@ -22,7 +25,7 @@ use net_traits::{
|
|||
ResourceTimingType,
|
||||
};
|
||||
use servo_arc::Arc;
|
||||
use servo_url::ServoUrl;
|
||||
use servo_url::{ImmutableOrigin, ServoUrl};
|
||||
use style::attr::AttrValue;
|
||||
use style::stylesheets::Stylesheet;
|
||||
use stylo_atoms::Atom;
|
||||
|
@ -78,6 +81,7 @@ struct LinkProcessingOptions {
|
|||
policy_container: PolicyContainer,
|
||||
source_set: Option<()>,
|
||||
base_url: ServoUrl,
|
||||
origin: ImmutableOrigin,
|
||||
insecure_requests_policy: InsecureRequestsPolicy,
|
||||
has_trustworthy_ancestor_origin: bool,
|
||||
// Some fields that we don't need yet are missing
|
||||
|
@ -113,6 +117,10 @@ pub(crate) struct HTMLLinkElement {
|
|||
request_generation_id: Cell<RequestGenerationId>,
|
||||
/// <https://html.spec.whatwg.org/multipage/#explicitly-enabled>
|
||||
is_explicitly_enabled: Cell<bool>,
|
||||
/// Whether the previous type matched with the destination
|
||||
previous_type_matched: Cell<bool>,
|
||||
/// Whether the previous media environment matched with the media query
|
||||
previous_media_environment_matched: Cell<bool>,
|
||||
}
|
||||
|
||||
impl HTMLLinkElement {
|
||||
|
@ -133,6 +141,8 @@ impl HTMLLinkElement {
|
|||
any_failed_load: Cell::new(false),
|
||||
request_generation_id: Cell::new(RequestGenerationId(0)),
|
||||
is_explicitly_enabled: Cell::new(false),
|
||||
previous_type_matched: Cell::new(true),
|
||||
previous_media_environment_matched: Cell::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,7 +246,7 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
return;
|
||||
}
|
||||
|
||||
if !self.upcast::<Node>().is_connected() || is_removal {
|
||||
if !self.upcast::<Node>().is_connected() {
|
||||
return;
|
||||
}
|
||||
match *local_name {
|
||||
|
@ -245,6 +255,12 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
.set(LinkRelations::for_element(self.upcast()));
|
||||
},
|
||||
local_name!("href") => {
|
||||
if is_removal {
|
||||
return;
|
||||
}
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
|
||||
// When the href attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is changed.
|
||||
if self.relations.get().contains(LinkRelations::STYLESHEET) {
|
||||
self.handle_stylesheet_url(&attr.value());
|
||||
}
|
||||
|
@ -254,9 +270,19 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
self.handle_favicon_url(&attr.value(), &sizes);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
|
||||
// When the href attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is changed.
|
||||
if self.relations.get().contains(LinkRelations::PREFETCH) {
|
||||
self.fetch_and_process_prefetch_link(&attr.value());
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-preload
|
||||
// When the href attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is changed.
|
||||
if self.relations.get().contains(LinkRelations::PRELOAD) {
|
||||
self.handle_preload_url();
|
||||
}
|
||||
},
|
||||
local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => {
|
||||
if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) {
|
||||
|
@ -264,9 +290,73 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
}
|
||||
},
|
||||
local_name!("crossorigin") => {
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
|
||||
// When the crossorigin attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is set, changed, or removed.
|
||||
if self.relations.get().contains(LinkRelations::PREFETCH) {
|
||||
self.fetch_and_process_prefetch_link(&attr.value());
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
|
||||
// When the crossorigin attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is set, changed, or removed.
|
||||
if self.relations.get().contains(LinkRelations::STYLESHEET) {
|
||||
self.handle_stylesheet_url(&attr.value());
|
||||
}
|
||||
},
|
||||
local_name!("as") => {
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-preload
|
||||
// When the as attribute of the link element of an external resource link
|
||||
// that is already browsing-context connected is changed.
|
||||
if self.relations.get().contains(LinkRelations::PRELOAD) {
|
||||
if let AttributeMutation::Set(Some(_)) = mutation {
|
||||
self.handle_preload_url();
|
||||
}
|
||||
}
|
||||
},
|
||||
local_name!("type") => {
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-stylesheet
|
||||
// When the type attribute of the link element of an external resource link that
|
||||
// is already browsing-context connected is set or changed to a value that does
|
||||
// not or no longer matches the Content-Type metadata of the previous obtained
|
||||
// external resource, if any.
|
||||
//
|
||||
// TODO: Match Content-Type metadata to check if it needs to be updated
|
||||
if self.relations.get().contains(LinkRelations::STYLESHEET) {
|
||||
self.handle_stylesheet_url(&attr.value());
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-preload
|
||||
// When the type attribute of the link element of an external resource link that
|
||||
// is already browsing-context connected, but was previously not obtained due to
|
||||
// the type attribute specifying an unsupported type for the request destination,
|
||||
// is set, removed, or changed.
|
||||
if self.relations.get().contains(LinkRelations::PRELOAD) &&
|
||||
!self.previous_type_matched.get()
|
||||
{
|
||||
self.handle_preload_url();
|
||||
}
|
||||
},
|
||||
local_name!("media") => {
|
||||
// https://html.spec.whatwg.org/multipage/#link-type-preload
|
||||
// When the media attribute of the link element of an external resource link that
|
||||
// is already browsing-context connected, but was previously not obtained due to
|
||||
// the media attribute not matching the environment, is changed or removed.
|
||||
if self.relations.get().contains(LinkRelations::PRELOAD) &&
|
||||
!self.previous_media_environment_matched.get()
|
||||
{
|
||||
match mutation {
|
||||
AttributeMutation::Removed | AttributeMutation::Set(Some(_)) => {
|
||||
self.handle_preload_url()
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
|
||||
let matches_media_environment =
|
||||
self.upcast::<Element>().matches_environment(&attr.value());
|
||||
self.previous_media_environment_matched
|
||||
.set(matches_media_environment);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
@ -307,6 +397,10 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
if relations.contains(LinkRelations::PREFETCH) {
|
||||
self.fetch_and_process_prefetch_link(&href);
|
||||
}
|
||||
|
||||
if relations.contains(LinkRelations::PRELOAD) {
|
||||
self.handle_preload_url();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -325,6 +419,14 @@ impl VirtualMethods for HTMLLinkElement {
|
|||
}
|
||||
|
||||
impl HTMLLinkElement {
|
||||
fn compute_destination_for_attribute(&self) -> Destination {
|
||||
let element = self.upcast::<Element>();
|
||||
element
|
||||
.get_attribute(&ns!(), &local_name!("as"))
|
||||
.map(|attr| translate_a_preload_destination(&attr.value()))
|
||||
.unwrap_or(Destination::None)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element>
|
||||
fn processing_options(&self) -> LinkProcessingOptions {
|
||||
let element = self.upcast::<Element>();
|
||||
|
@ -333,10 +435,7 @@ impl HTMLLinkElement {
|
|||
let document = self.upcast::<Node>().owner_doc();
|
||||
|
||||
// Step 2. Let options be a new link processing options
|
||||
let destination = element
|
||||
.get_attribute(&ns!(), &local_name!("as"))
|
||||
.map(|attr| translate_a_preload_destination(&attr.value()))
|
||||
.unwrap_or(Destination::None);
|
||||
let destination = self.compute_destination_for_attribute();
|
||||
|
||||
let mut options = LinkProcessingOptions {
|
||||
href: String::new(),
|
||||
|
@ -348,6 +447,7 @@ impl HTMLLinkElement {
|
|||
referrer_policy: referrer_policy_for_element(element),
|
||||
policy_container: document.policy_container().to_owned(),
|
||||
source_set: None, // FIXME
|
||||
origin: document.borrow().origin().immutable().to_owned(),
|
||||
base_url: document.borrow().base_url(),
|
||||
insecure_requests_policy: document.insecure_requests_policy(),
|
||||
has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(),
|
||||
|
@ -446,6 +546,10 @@ impl HTMLLinkElement {
|
|||
None => "",
|
||||
};
|
||||
|
||||
if !element.matches_environment(mq_str) {
|
||||
return;
|
||||
}
|
||||
|
||||
let media = MediaList::parse_media_list(mq_str, document.window());
|
||||
|
||||
let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity"));
|
||||
|
@ -458,8 +562,6 @@ impl HTMLLinkElement {
|
|||
self.request_generation_id
|
||||
.set(self.request_generation_id.get().increment());
|
||||
|
||||
// TODO: #8085 - Don't load external stylesheets if the node's mq
|
||||
// doesn't match.
|
||||
let loader = StylesheetLoader::for_element(self.upcast());
|
||||
loader.load(
|
||||
StylesheetContextSource::LinkElement { media: Some(media) },
|
||||
|
@ -494,6 +596,133 @@ impl HTMLLinkElement {
|
|||
Err(e) => debug!("Parsing url {} failed: {}", href, e),
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
|
||||
fn handle_preload_url(&self) {
|
||||
// Step 1. Update the source set for el.
|
||||
// TODO
|
||||
// Step 2. Let options be the result of creating link options from el.
|
||||
let options = self.processing_options();
|
||||
// Step 3. Preload options, with the following steps given a response response:
|
||||
// Step 3.1 If response is a network error, fire an event named error at el.
|
||||
// Otherwise, fire an event named load at el.
|
||||
self.preload(options);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#preload>
|
||||
fn preload(&self, options: LinkProcessingOptions) {
|
||||
// Step 1. If options's type doesn't match options's destination, then return.
|
||||
let type_matches_destination: bool =
|
||||
HTMLLinkElement::type_matches_destination(&options.link_type, options.destination);
|
||||
self.previous_type_matched.set(type_matches_destination);
|
||||
if !type_matches_destination {
|
||||
return;
|
||||
}
|
||||
// Step 2. If options's destination is "image" and options's source set is not null,
|
||||
// then set options's href to the result of selecting an image source from options's source set.
|
||||
// TODO
|
||||
// Step 3. Let request be the result of creating a link request given options.
|
||||
let url = options.base_url.clone();
|
||||
let Some(request) = options.create_link_request(self.owner_window().webview_id()) else {
|
||||
// Step 4. If request is null, then return.
|
||||
return;
|
||||
};
|
||||
let document = self.upcast::<Node>().owner_doc();
|
||||
// Step 5. Let unsafeEndTime be 0.
|
||||
// TODO
|
||||
// Step 6. Let entry be a new preload entry whose integrity metadata is options's integrity.
|
||||
// TODO
|
||||
// Step 7. Let key be the result of creating a preload key given request.
|
||||
// TODO
|
||||
// Step 8. If options's document is "pending", then set request's initiator type to "early hint".
|
||||
// TODO
|
||||
// Step 9. Let controller be null.
|
||||
// Step 10. Let reportTiming given a Document document be to report timing for controller
|
||||
// given document's relevant global object.
|
||||
// Step 11. Set controller to the result of fetching request, with processResponseConsumeBody
|
||||
// set to the following steps given a response response and null, failure, or a byte sequence bodyBytes:
|
||||
let fetch_context = PreloadContext {
|
||||
url,
|
||||
link: Trusted::new(self),
|
||||
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
|
||||
};
|
||||
document.fetch_background(request.clone(), fetch_context);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#match-preload-type>
|
||||
fn type_matches_destination(mime_type: &str, destination: Option<Destination>) -> bool {
|
||||
// Step 1. If type is an empty string, then return true.
|
||||
if mime_type.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Step 2. If destination is "fetch", then return true.
|
||||
//
|
||||
// Fetch is handled as an empty string destination in the spec:
|
||||
// https://fetch.spec.whatwg.org/#concept-potential-destination-translate
|
||||
let Some(destination) = destination else {
|
||||
return false;
|
||||
};
|
||||
if destination == Destination::None {
|
||||
return true;
|
||||
}
|
||||
// Step 3. Let mimeTypeRecord be the result of parsing type.
|
||||
let Ok(mime_type_record) = Mime::from_str(mime_type) else {
|
||||
// Step 4. If mimeTypeRecord is failure, then return false.
|
||||
return false;
|
||||
};
|
||||
// Step 5. If mimeTypeRecord is not supported by the user agent, then return false.
|
||||
//
|
||||
// We currently don't check if we actually support the mime type. Only if we can classify
|
||||
// it according to the spec.
|
||||
let Some(mime_type) = MimeClassifier::get_media_type(&mime_type_record) else {
|
||||
return false;
|
||||
};
|
||||
// Step 6. If any of the following are true:
|
||||
if
|
||||
// destination is "audio" or "video", and mimeTypeRecord is an audio or video MIME type;
|
||||
((destination == Destination::Audio || destination == Destination::Video) &&
|
||||
mime_type == MediaType::AudioVideo)
|
||||
// destination is a script-like destination and mimeTypeRecord is a JavaScript MIME type;
|
||||
|| (destination.is_script_like() && mime_type == MediaType::JavaScript)
|
||||
// destination is "image" and mimeTypeRecord is an image MIME type;
|
||||
|| (destination == Destination::Image && mime_type == MediaType::Image)
|
||||
// destination is "font" and mimeTypeRecord is a font MIME type;
|
||||
|| (destination == Destination::Font && mime_type == MediaType::Font)
|
||||
// destination is "json" and mimeTypeRecord is a JSON MIME type;
|
||||
|| (destination == Destination::Json && mime_type == MediaType::Json)
|
||||
// destination is "style" and mimeTypeRecord's essence is text/css; or
|
||||
|| (destination == Destination::Style && mime_type_record == mime::TEXT_CSS)
|
||||
// destination is "track" and mimeTypeRecord's essence is text/vtt,
|
||||
|| (destination == Destination::Track && mime_type_record.essence_str() == "text/vtt")
|
||||
{
|
||||
// then return true.
|
||||
return true;
|
||||
}
|
||||
// Step 7. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
fn fire_event_after_response(&self, response: Result<ResourceFetchTiming, NetworkError>) {
|
||||
if response.is_err() {
|
||||
self.upcast::<EventTarget>()
|
||||
.fire_event(atom!("error"), CanGc::note());
|
||||
} else {
|
||||
// TODO(35035): Figure out why we need to queue a task for the load event. Otherwise
|
||||
// the performance timing data hasn't been saved yet, which fails several preload
|
||||
// WPT tests that assume that performance timing information is available when
|
||||
// the load event is fired.
|
||||
let this = Trusted::new(self);
|
||||
self.owner_global()
|
||||
.task_manager()
|
||||
.performance_timeline_task_source()
|
||||
.queue(task!(preload_load_event: move || {
|
||||
let this = this.root();
|
||||
this
|
||||
.upcast::<EventTarget>()
|
||||
.fire_event(atom!("load"), CanGc::note());
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StylesheetOwner for HTMLLinkElement {
|
||||
|
@ -552,6 +781,21 @@ impl HTMLLinkElementMethods<crate::DomTypeHolder> for HTMLLinkElement {
|
|||
.set_tokenlist_attribute(&local_name!("rel"), rel, can_gc);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-link-as
|
||||
make_enumerated_getter!(
|
||||
As,
|
||||
"as",
|
||||
"fetch" | "audio" | "audioworklet" | "document" | "embed" | "font" | "frame"
|
||||
| "iframe" | "image" | "json" | "manifest" | "object" | "paintworklet"
|
||||
| "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track"
|
||||
| "video" | "webidentity" | "worker" | "xslt",
|
||||
missing => "",
|
||||
invalid => ""
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-link-as
|
||||
make_setter!(SetAs, "as");
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-link-media
|
||||
make_getter!(Media, "media");
|
||||
|
||||
|
@ -689,6 +933,8 @@ impl LinkProcessingOptions {
|
|||
self.has_trustworthy_ancestor_origin,
|
||||
self.policy_container,
|
||||
)
|
||||
.initiator(Initiator::Link)
|
||||
.origin(self.origin)
|
||||
.integrity_metadata(self.integrity)
|
||||
.cryptographic_nonce_metadata(self.cryptographic_nonce_metadata)
|
||||
.referrer_policy(self.referrer_policy);
|
||||
|
@ -795,3 +1041,77 @@ impl PreInvoke for PrefetchContext {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct PreloadContext {
|
||||
/// The `<link>` element that caused this preload operation
|
||||
link: Trusted<HTMLLinkElement>,
|
||||
|
||||
resource_timing: ResourceFetchTiming,
|
||||
|
||||
/// The url being preloaded
|
||||
url: ServoUrl,
|
||||
}
|
||||
|
||||
impl FetchResponseListener for PreloadContext {
|
||||
fn process_request_body(&mut self, _: RequestId) {}
|
||||
|
||||
fn process_request_eof(&mut self, _: RequestId) {}
|
||||
|
||||
fn process_response(
|
||||
&mut self,
|
||||
_: RequestId,
|
||||
fetch_metadata: Result<FetchMetadata, NetworkError>,
|
||||
) {
|
||||
_ = fetch_metadata;
|
||||
}
|
||||
|
||||
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
|
||||
_ = chunk;
|
||||
}
|
||||
|
||||
/// Step 3.1 of <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
|
||||
fn process_response_eof(
|
||||
&mut self,
|
||||
_: RequestId,
|
||||
response: Result<ResourceFetchTiming, NetworkError>,
|
||||
) {
|
||||
self.link.root().fire_event_after_response(response);
|
||||
}
|
||||
|
||||
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
|
||||
&mut self.resource_timing
|
||||
}
|
||||
|
||||
fn resource_timing(&self) -> &ResourceFetchTiming {
|
||||
&self.resource_timing
|
||||
}
|
||||
|
||||
fn submit_resource_timing(&mut self) {
|
||||
submit_timing(self, CanGc::note())
|
||||
}
|
||||
|
||||
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) {
|
||||
let global = &self.resource_timing_global();
|
||||
global.report_csp_violations(violations, None);
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceTimingListener for PreloadContext {
|
||||
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
|
||||
(
|
||||
InitiatorType::LocalName(self.url.clone().into_string()),
|
||||
self.url.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
|
||||
self.link.root().upcast::<Node>().owner_doc().global()
|
||||
}
|
||||
}
|
||||
|
||||
impl PreInvoke for PreloadContext {
|
||||
fn should_invoke(&self) -> bool {
|
||||
// Preload requests are never aborted.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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_,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue