mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(print): isolate PDF style and font environments from screen state
This commit is contained in:
@@ -25,6 +25,7 @@ pub(crate) fn build_native_layout_pass(
|
||||
services: &mut DocumentLayoutServices,
|
||||
embedded_document_services: &mut HashMap<DomHandle, DocumentLayoutServices>,
|
||||
request: LayoutPassRequest,
|
||||
environment_override: Option<crate::style_engine::StyloStyleEnvironment>,
|
||||
) -> Result<LayoutPassResult<DomHandle>, moli_layout::LayoutError> {
|
||||
let mut document_stack = Vec::new();
|
||||
build_native_layout_pass_recursive(
|
||||
@@ -33,6 +34,7 @@ pub(crate) fn build_native_layout_pass(
|
||||
services,
|
||||
embedded_document_services,
|
||||
request,
|
||||
environment_override,
|
||||
&mut document_stack,
|
||||
)
|
||||
}
|
||||
@@ -43,6 +45,7 @@ fn build_native_layout_pass_recursive(
|
||||
services: &mut DocumentLayoutServices,
|
||||
embedded_document_services: &mut HashMap<DomHandle, DocumentLayoutServices>,
|
||||
request: LayoutPassRequest,
|
||||
environment_override: Option<crate::style_engine::StyloStyleEnvironment>,
|
||||
document_stack: &mut Vec<DomHandle>,
|
||||
) -> Result<LayoutPassResult<DomHandle>, moli_layout::LayoutError> {
|
||||
let profile_started = moli_trace::cpu_profile_enabled().then(std::time::Instant::now);
|
||||
@@ -60,8 +63,13 @@ fn build_native_layout_pass_recursive(
|
||||
request.requests_paint(),
|
||||
);
|
||||
let source_ready = profile_started.map(|_| std::time::Instant::now());
|
||||
let mut styles =
|
||||
style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, request.viewport);
|
||||
let mut styles = style_resolver::NativeLayoutStyleResolver::new(
|
||||
runtime,
|
||||
root,
|
||||
document,
|
||||
request.viewport,
|
||||
environment_override,
|
||||
);
|
||||
let styles_ready = profile_started.map(|_| std::time::Instant::now());
|
||||
let result = {
|
||||
let mut frames = NativeEmbeddedFrameRenderer {
|
||||
@@ -69,6 +77,7 @@ fn build_native_layout_pass_recursive(
|
||||
reason: request.reason,
|
||||
capture_paint: request.requests_paint(),
|
||||
include_backgrounds: request.includes_backgrounds(),
|
||||
environment_override,
|
||||
document_stack,
|
||||
embedded_document_services,
|
||||
};
|
||||
@@ -135,6 +144,7 @@ struct NativeEmbeddedFrameRenderer<'a> {
|
||||
reason: moli_layout::LayoutFlushReason,
|
||||
capture_paint: bool,
|
||||
include_backgrounds: bool,
|
||||
environment_override: Option<crate::style_engine::StyloStyleEnvironment>,
|
||||
document_stack: &'a mut Vec<DomHandle>,
|
||||
embedded_document_services: &'a mut HashMap<DomHandle, DocumentLayoutServices>,
|
||||
}
|
||||
@@ -182,6 +192,7 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
|
||||
&mut services,
|
||||
self.embedded_document_services,
|
||||
request,
|
||||
self.environment_override,
|
||||
self.document_stack,
|
||||
);
|
||||
self.embedded_document_services.insert(document, services);
|
||||
@@ -207,6 +218,6 @@ pub(crate) fn build_normalized_native_box_tree_for_test(
|
||||
let viewport = runtime.layout_viewport_for_document(document);
|
||||
let source = source_view::NativeLayoutSourceView::new(runtime, root);
|
||||
let mut styles =
|
||||
style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, viewport);
|
||||
style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, viewport, None);
|
||||
moli_layout::build_layout_world(&source, &mut styles).map(|world| world.normalized_tree())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
JsContextHost,
|
||||
element::{PreparedAnonymousStyleContext, StyleObservation},
|
||||
},
|
||||
style_engine::{StyleViewport, StyloAnonymousBoxKind},
|
||||
style_engine::{StyleViewport, StyloAnonymousBoxKind, StyloStyleEnvironment},
|
||||
};
|
||||
|
||||
pub(super) struct NativeLayoutStyleResolver<'a> {
|
||||
@@ -43,8 +43,9 @@ impl<'a> NativeLayoutStyleResolver<'a> {
|
||||
root: DomHandle,
|
||||
document: DomHandle,
|
||||
viewport: LayoutViewport,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> Self {
|
||||
let mut reads = layout_style_read_scope(runtime, document, viewport);
|
||||
let mut reads = layout_style_read_scope(runtime, document, viewport, environment_override);
|
||||
// Drain pending invalidations and update the retained world before
|
||||
// freezing the anonymous-style context for this synchronous pass.
|
||||
let anonymous_context = reads.read(root).prepare_anonymous_style_context();
|
||||
@@ -93,15 +94,24 @@ fn layout_style_read_scope<'a>(
|
||||
runtime: &'a JsContextHost,
|
||||
document: DomHandle,
|
||||
viewport: LayoutViewport,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> StyleObservation<'a> {
|
||||
let viewport = layout_style_viewport(runtime, viewport);
|
||||
if document == runtime.document_handle() && viewport == runtime.style_viewport() {
|
||||
if environment_override.is_none()
|
||||
&& document == runtime.document_handle()
|
||||
&& viewport == runtime.style_viewport()
|
||||
{
|
||||
// Keep the normal main-document observation environment so layout and
|
||||
// rendered-text collection read the same persistent style world. Only
|
||||
// an override viewport or embedded Document needs an explicit context.
|
||||
// an override viewport/media or embedded Document needs an explicit context.
|
||||
StyleObservation::new(runtime)
|
||||
} else {
|
||||
StyleObservation::new_for_document_viewport(runtime, document, viewport)
|
||||
StyleObservation::new_for_document_viewport(
|
||||
runtime,
|
||||
document,
|
||||
viewport,
|
||||
environment_override,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +378,7 @@ mod tests {
|
||||
let root = host.dom_host().document_element_handle().unwrap();
|
||||
let owner = host.dom_host().element_handle_by_id("owner").unwrap();
|
||||
let viewport = host.layout_viewport_for_document(document);
|
||||
let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport);
|
||||
let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport, None);
|
||||
let world = layout_world(&mut resolver, root);
|
||||
let parent = parent_context(&world, owner);
|
||||
let reads = resolver.reads.read_count_for_test();
|
||||
@@ -406,7 +416,7 @@ mod tests {
|
||||
let root = host.dom_host().document_element_handle().unwrap();
|
||||
let owner = host.dom_host().element_handle_by_id("owner").unwrap();
|
||||
let viewport = host.layout_viewport_for_document(document);
|
||||
let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport);
|
||||
let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport, None);
|
||||
let world = layout_world(&mut resolver, root);
|
||||
let parent = parent_context(&world, owner);
|
||||
|
||||
@@ -418,7 +428,7 @@ mod tests {
|
||||
.document_element_handle_for_document(foreign_document)
|
||||
.unwrap();
|
||||
let mut foreign =
|
||||
NativeLayoutStyleResolver::new(&host, foreign_root, foreign_document, viewport);
|
||||
NativeLayoutStyleResolver::new(&host, foreign_root, foreign_document, viewport, None);
|
||||
let foreign_world = layout_world(&mut foreign, foreign_root);
|
||||
let foreign_parent = parent_context(&foreign_world, foreign_root);
|
||||
assert!(
|
||||
@@ -462,6 +472,7 @@ mod tests {
|
||||
root,
|
||||
document,
|
||||
host.layout_viewport_for_document(document),
|
||||
None,
|
||||
);
|
||||
let world = layout_world(&mut resolver, root);
|
||||
let reads = resolver.reads.read_count_for_test();
|
||||
@@ -511,6 +522,7 @@ mod tests {
|
||||
root,
|
||||
document,
|
||||
host.layout_viewport_for_document(document),
|
||||
None,
|
||||
);
|
||||
let world = layout_world(&mut resolver, root);
|
||||
let parent = parent_context(&world, owner);
|
||||
@@ -545,6 +557,7 @@ mod tests {
|
||||
root,
|
||||
document,
|
||||
host.layout_viewport_for_document(document),
|
||||
None,
|
||||
);
|
||||
let world = layout_world(&mut resolver, root);
|
||||
let parent = parent_context(&world, owner);
|
||||
|
||||
@@ -123,7 +123,7 @@ impl JsContextHost {
|
||||
document,
|
||||
self.dom_host().dom_version(),
|
||||
self.style_engine
|
||||
.computed_style_observation_generations(self.active_layout_document_handles()),
|
||||
.visual_style_input_generations(self.active_layout_document_handles()),
|
||||
self.visual_state_generation(),
|
||||
self.visual_resource_generation(),
|
||||
viewport,
|
||||
@@ -213,7 +213,12 @@ impl JsContextHost {
|
||||
/// after the recursive pass succeeds. Advancing this input generation
|
||||
/// first also distinguishes an explicit main-Document capture viewport
|
||||
/// from ordinary DOM APIs selecting conflicting implicit viewports.
|
||||
pub(crate) fn note_layout_style_viewport(&self, document: DomHandle, viewport: StyleViewport) {
|
||||
pub(crate) fn note_layout_style_environment(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
viewport: StyleViewport,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) {
|
||||
let matches_cached = if document == self.document_handle() {
|
||||
viewport == self.style_viewport()
|
||||
} else {
|
||||
@@ -224,7 +229,10 @@ impl JsContextHost {
|
||||
&& viewport.height == Some(f64::from(cached.css_height))
|
||||
})
|
||||
};
|
||||
if !matches_cached {
|
||||
let different_media = environment_override.is_some_and(|environment| {
|
||||
environment != StyloStyleEnvironment::from_emulated_media(self.emulated_media())
|
||||
});
|
||||
if !matches_cached || different_media {
|
||||
self.style_viewport_generation
|
||||
.set(self.style_viewport_generation.get().saturating_add(1));
|
||||
}
|
||||
@@ -299,6 +307,15 @@ impl JsContextHost {
|
||||
&self,
|
||||
document: DomHandle,
|
||||
request: LayoutPassRequest,
|
||||
) -> Result<Option<LayoutPassResult<DomHandle>>, LayoutError> {
|
||||
self.build_layout_pass_for_document_in_environment(document, request, None)
|
||||
}
|
||||
|
||||
pub(crate) fn build_layout_pass_for_document_in_environment(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
request: LayoutPassRequest,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> Result<Option<LayoutPassResult<DomHandle>>, LayoutError> {
|
||||
let Some(root) = self
|
||||
.dom_host()
|
||||
@@ -324,6 +341,7 @@ impl JsContextHost {
|
||||
services,
|
||||
embedded_document_services,
|
||||
request,
|
||||
environment_override,
|
||||
)
|
||||
},
|
||||
)?
|
||||
|
||||
@@ -74,6 +74,9 @@ pub(crate) struct StyleObservation<'a> {
|
||||
/// detached CSSOM snapshots use `Some` to pin an exact caller-owned
|
||||
/// viewport instead.
|
||||
fixed_context: Option<StyleComputationContext>,
|
||||
/// A capture may observe a temporary media environment without changing
|
||||
/// the Window's live environment or scheduling environment events.
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
/// Explicit layout observations may intentionally use a caller-owned
|
||||
/// viewport that differs from the Window surface. They advance the style
|
||||
/// viewport input epoch, but do not become the baseline for ordinary DOM
|
||||
@@ -110,8 +113,9 @@ impl<'a> StyleObservationInputs<'a> {
|
||||
context: StyleComputationContext,
|
||||
_tracks_persistent_world: bool,
|
||||
sizing_target: Option<DomHandle>,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> Self {
|
||||
let environment = stylo_style_environment(runtime, source_document);
|
||||
let environment = stylo_style_environment(runtime, source_document, environment_override);
|
||||
Self {
|
||||
runtime,
|
||||
key: StyleObservationKey::for_document(runtime, source_document),
|
||||
@@ -246,14 +250,17 @@ impl<'a> StyleObservation<'a> {
|
||||
runtime: &'a JsContextHost,
|
||||
document: DomHandle,
|
||||
viewport: StyleViewport,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> Self {
|
||||
runtime.note_layout_style_viewport(document, viewport);
|
||||
Self::new_with_fixed_context(
|
||||
runtime.note_layout_style_environment(document, viewport, environment_override);
|
||||
let mut observation = Self::new_with_fixed_context(
|
||||
runtime,
|
||||
Some(StyleComputationContext::new(viewport).with_read_document(Some(document))),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
);
|
||||
observation.environment_override = environment_override;
|
||||
observation
|
||||
}
|
||||
|
||||
pub(super) fn new_for_element(
|
||||
@@ -275,6 +282,7 @@ impl<'a> StyleObservation<'a> {
|
||||
#[cfg(test)]
|
||||
read_count: 0,
|
||||
fixed_context,
|
||||
environment_override: None,
|
||||
tracks_persistent_world,
|
||||
sizing_target,
|
||||
drained_document: None,
|
||||
@@ -409,6 +417,7 @@ impl<'a> StyleObservation<'a> {
|
||||
context,
|
||||
self.tracks_persistent_world,
|
||||
self.sizing_target,
|
||||
self.environment_override,
|
||||
));
|
||||
if self.primary_document.is_none() {
|
||||
self.primary_document = Some(Rc::clone(&observation));
|
||||
|
||||
@@ -159,19 +159,22 @@ pub(super) fn stylesheet_query_fallback(
|
||||
pub(super) fn stylo_style_environment(
|
||||
runtime: &JsContextHost,
|
||||
document: Option<DomHandle>,
|
||||
environment_override: Option<StyloStyleEnvironment>,
|
||||
) -> StyloStyleEnvironment {
|
||||
#[cfg(test)]
|
||||
runtime.note_style_observation_environment_resolution_for_test();
|
||||
StyloStyleEnvironment::from_emulated_media(runtime.emulated_media()).with_page_color_schemes(
|
||||
document
|
||||
.map(|document| {
|
||||
crate::document_color_scheme::document_page_color_schemes(
|
||||
runtime.dom_host(),
|
||||
document,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
environment_override
|
||||
.unwrap_or_else(|| StyloStyleEnvironment::from_emulated_media(runtime.emulated_media()))
|
||||
.with_page_color_schemes(
|
||||
document
|
||||
.map(|document| {
|
||||
crate::document_color_scheme::document_page_color_schemes(
|
||||
runtime.dom_host(),
|
||||
document,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn complete_style_world_snapshot(
|
||||
|
||||
@@ -33,30 +33,30 @@ impl PageVm {
|
||||
if self.layout_configuration.policy == LayoutPolicy::Mock {
|
||||
return Ok(Reply::LayoutDisabled);
|
||||
}
|
||||
let previous = self.document_settings.emulated_media.clone();
|
||||
let restore = previous.media.is_none();
|
||||
if restore {
|
||||
let mut print = previous.clone();
|
||||
let environment_override = if self.document_settings.emulated_media.media.is_none() {
|
||||
let mut print = self.document_settings.emulated_media.clone();
|
||||
print.media = Some("print".to_owned());
|
||||
self.set_emulated_media(&print)?;
|
||||
}
|
||||
Some(crate::style_engine::StyloStyleEnvironment::from_emulated_media(&print))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let surface = self.document_settings.viewport_surface.unwrap_or_default();
|
||||
let viewport = PaintViewport::new(
|
||||
surface.inner_width,
|
||||
surface.inner_height,
|
||||
surface.device_pixel_ratio as f32,
|
||||
);
|
||||
let result = self.vm_mut().paint_layout_snapshot_with_capture(
|
||||
viewport,
|
||||
moli_layout::LayoutFlushReason::Print,
|
||||
PaintCaptureRequest {
|
||||
include_backgrounds: request.print_background,
|
||||
..PaintCaptureRequest::print_document()
|
||||
},
|
||||
);
|
||||
if restore {
|
||||
self.set_emulated_media(&previous)?;
|
||||
}
|
||||
let result = self
|
||||
.vm_mut()
|
||||
.paint_layout_snapshot_with_capture_in_environment(
|
||||
viewport,
|
||||
moli_layout::LayoutFlushReason::Print,
|
||||
PaintCaptureRequest {
|
||||
include_backgrounds: request.print_background,
|
||||
..PaintCaptureRequest::print_document()
|
||||
},
|
||||
environment_override,
|
||||
);
|
||||
Ok(match result? {
|
||||
Some(snapshot) => match moli_paint::pdf::pdf_snapshot(&snapshot, &request.options) {
|
||||
Ok(bytes) => Reply::Printed(bytes),
|
||||
|
||||
@@ -67,7 +67,7 @@ pub struct RendererVisualStateToken(Arc<RendererVisualState>);
|
||||
struct RendererVisualState {
|
||||
document: RendererDocumentLifecycleIdentity,
|
||||
dom_generation: u64,
|
||||
style_generations: Vec<(u32, u64, u64, u64)>,
|
||||
style_generations: Vec<(u32, u64, u64)>,
|
||||
interaction_generation: u64,
|
||||
resource_generation: u64,
|
||||
viewport_width: u32,
|
||||
@@ -80,7 +80,7 @@ impl RendererVisualStateToken {
|
||||
pub(crate) fn new(
|
||||
document: RendererDocumentLifecycleIdentity,
|
||||
dom_generation: u64,
|
||||
style_generations: Vec<(crate::document_runtime::DomHandle, u64, u64, u64)>,
|
||||
style_generations: Vec<(crate::document_runtime::DomHandle, u64, u64)>,
|
||||
interaction_generation: u64,
|
||||
resource_generation: u64,
|
||||
viewport: PaintViewport,
|
||||
@@ -91,9 +91,7 @@ impl RendererVisualStateToken {
|
||||
dom_generation,
|
||||
style_generations: style_generations
|
||||
.into_iter()
|
||||
.map(|(document, source, computed, context)| {
|
||||
(document.index_u32(), source, computed, context)
|
||||
})
|
||||
.map(|(document, source, input)| (document.index_u32(), source, input))
|
||||
.collect(),
|
||||
interaction_generation,
|
||||
resource_generation,
|
||||
|
||||
@@ -3,6 +3,7 @@ use super::*;
|
||||
mod image_repeat;
|
||||
mod planar_transforms;
|
||||
mod print;
|
||||
mod print_isolation;
|
||||
mod transform_precision;
|
||||
|
||||
use base64::Engine as _;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use super::*;
|
||||
use crate::runtime::{
|
||||
RendererCaptureScreencastFrameReply, RendererCaptureScreenshotRequest, RendererPrintToPdfReply,
|
||||
RendererPrintToPdfRequest,
|
||||
};
|
||||
|
||||
fn screen_token(page: &PageVm) -> crate::runtime::RendererVisualStateToken {
|
||||
page.vm().visual_state_token(
|
||||
page.document_lifecycle.identity(),
|
||||
moli_layout::PaintViewport::new(320, 100, 1.0),
|
||||
[255; 4],
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn native_pdf_preserves_live_fonts_and_screencast_input_identity() {
|
||||
run_page_vm_async_test(async move {
|
||||
use base64::Engine;
|
||||
|
||||
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
|
||||
loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT);
|
||||
let mut page = test_page_vm_with_loader_and_document_url(
|
||||
&loader,
|
||||
Vec::new(),
|
||||
Url::parse("https://print-isolation.test/")?,
|
||||
);
|
||||
page.set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
|
||||
inner_width: 320,
|
||||
inner_height: 100,
|
||||
..Default::default()
|
||||
}))?;
|
||||
let font = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
|
||||
"../../../../../../moli-layout/tests/fixtures/moli-ahem.woff2"
|
||||
));
|
||||
page.vm_mut().eval(&format!(r#"
|
||||
document.head.innerHTML=`<style>
|
||||
body{{margin:0}} #target{{width:40px;height:40px}}
|
||||
.print{{display:none}}
|
||||
@media screen{{@font-face{{font-family:ScreenOnly;src:url(data:font/woff2;base64,{font})}}}}
|
||||
@media print{{
|
||||
@font-face{{font-family:PrintOnly;src:url(data:font/woff2;base64,{font})}}
|
||||
#target{{width:80px}} .screen{{display:none}} .print{{display:block}}
|
||||
}}
|
||||
</style>`;
|
||||
document.body.innerHTML='<div id=target><span class=screen>SCREEN</span><span class=print>PRINT</span></div>';
|
||||
"#))?;
|
||||
page.capture_screenshot(RendererCaptureScreenshotRequest::viewport_png())?;
|
||||
page.vm_mut().eval(r#"
|
||||
window.liveFonts=document.fonts;
|
||||
window.liveReady=liveFonts.ready;
|
||||
window.screenFace=[...liveFonts].find(face=>face.family==='ScreenOnly');
|
||||
"#)?;
|
||||
assert_eq!(page.vm_mut().eval("String(!!screenFace)")?, "true");
|
||||
let RendererCaptureScreencastFrameReply::Captured(first) = page
|
||||
.capture_screencast_frame(viewport_screencast_request(None))? else {
|
||||
anyhow::bail!("expected initial screen capture");
|
||||
};
|
||||
let original = screen_token(&page);
|
||||
assert_eq!(original, first.visual_state);
|
||||
let fonts = page.vm_mut().document_web_font_counts_for_test();
|
||||
let publishes = page.vm().layout_snapshot_cache_observability_for_test().2;
|
||||
let media = page.document_settings.emulated_media.clone();
|
||||
|
||||
let RendererPrintToPdfReply::Printed(bytes) = page.print_to_pdf(Default::default())? else {
|
||||
anyhow::bail!("expected PDF");
|
||||
};
|
||||
let document = lopdf::Document::load_mem(&bytes)?;
|
||||
assert_eq!(
|
||||
document.extract_text(&document.get_pages().keys().copied().collect::<Vec<_>>())?
|
||||
.split_whitespace().collect::<Vec<_>>().join(" "),
|
||||
"PRINT"
|
||||
);
|
||||
assert_eq!(page.document_settings.emulated_media, media);
|
||||
assert_eq!(screen_token(&page), original);
|
||||
assert_eq!(page.vm_mut().document_web_font_counts_for_test(), fonts);
|
||||
assert_eq!(page.vm().layout_snapshot_cache_observability_for_test().2, publishes);
|
||||
assert_eq!(page.vm_mut().eval("JSON.stringify([document.fonts===liveFonts,liveFonts.has(screenFace),liveFonts.ready===liveReady,target.offsetWidth,matchMedia('print').matches])")?, "[true,true,true,40,false]");
|
||||
assert_eq!(screen_token(&page), original);
|
||||
assert!(matches!(
|
||||
page.capture_screencast_frame(viewport_screencast_request(Some(first.visual_state)))?,
|
||||
RendererCaptureScreencastFrameReply::Unchanged
|
||||
));
|
||||
|
||||
let mut invalid = RendererPrintToPdfRequest::default();
|
||||
invalid.options.width = 0.0;
|
||||
assert!(matches!(page.print_to_pdf(invalid)?, RendererPrintToPdfReply::Failed(_)));
|
||||
assert_eq!(screen_token(&page), original);
|
||||
assert_eq!(page.vm_mut().document_web_font_counts_for_test(), fonts);
|
||||
assert_eq!(page.document_settings.emulated_media, media);
|
||||
|
||||
page.vm_mut().eval("target.style.width='60px'")?;
|
||||
let RendererCaptureScreencastFrameReply::Captured(changed) = page
|
||||
.capture_screencast_frame(viewport_screencast_request(Some(original.clone())))? else {
|
||||
anyhow::bail!("a real source mutation must invalidate screencast");
|
||||
};
|
||||
assert_ne!(changed.visual_state, original);
|
||||
assert_eq!(page.vm_mut().eval("String(target.offsetWidth)")?, "60");
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("temporary print styles must preserve live screen inputs");
|
||||
}
|
||||
@@ -154,8 +154,19 @@ impl ScriptVm {
|
||||
reason: moli_layout::LayoutFlushReason,
|
||||
capture: moli_layout::PaintCaptureRequest,
|
||||
) -> anyhow::Result<Option<moli_layout::PaintSnapshot>> {
|
||||
self.with_fresh_layout_pass(
|
||||
self.paint_layout_snapshot_with_capture_in_environment(viewport, reason, capture, None)
|
||||
}
|
||||
|
||||
pub(crate) fn paint_layout_snapshot_with_capture_in_environment(
|
||||
&mut self,
|
||||
viewport: moli_layout::PaintViewport,
|
||||
reason: moli_layout::LayoutFlushReason,
|
||||
capture: moli_layout::PaintCaptureRequest,
|
||||
environment_override: Option<crate::style_engine::StyloStyleEnvironment>,
|
||||
) -> anyhow::Result<Option<moli_layout::PaintSnapshot>> {
|
||||
self.with_fresh_layout_pass_in_environment(
|
||||
moli_layout::LayoutPassRequest::with_capture(viewport, reason, capture),
|
||||
environment_override,
|
||||
moli_layout::LayoutPassResult::take_paint_snapshot,
|
||||
)
|
||||
.map_err(anyhow::Error::new)
|
||||
@@ -187,13 +198,26 @@ impl ScriptVm {
|
||||
consume: impl FnOnce(
|
||||
&mut moli_layout::LayoutPassResult<DomHandle>,
|
||||
) -> Result<T, moli_layout::LayoutError>,
|
||||
) -> Result<Option<T>, moli_layout::LayoutError> {
|
||||
self.with_fresh_layout_pass_in_environment(request, None, consume)
|
||||
}
|
||||
|
||||
fn with_fresh_layout_pass_in_environment<T>(
|
||||
&mut self,
|
||||
request: moli_layout::LayoutPassRequest,
|
||||
environment_override: Option<crate::style_engine::StyloStyleEnvironment>,
|
||||
consume: impl FnOnce(
|
||||
&mut moli_layout::LayoutPassResult<DomHandle>,
|
||||
) -> Result<T, moli_layout::LayoutError>,
|
||||
) -> Result<Option<T>, moli_layout::LayoutError> {
|
||||
// Font-source reconciliation is a pre-pass lifecycle step. CSS image
|
||||
// URLs come back from the actual box-construction traversal below.
|
||||
// Once the guard is entered, layout performs no JS, event-loop,
|
||||
// observer, or resource completion work and owns no state beyond this
|
||||
// call.
|
||||
self.reconcile_document_web_fonts_for_layout();
|
||||
if environment_override.is_none() {
|
||||
self.reconcile_document_web_fonts_for_layout();
|
||||
}
|
||||
let requests_paint = request.requests_paint();
|
||||
// Printing consumes a temporary projection. Screenshots, screencast
|
||||
// frames and explicit refreshes publish geometry for subsequent reads.
|
||||
@@ -202,7 +226,11 @@ impl ScriptVm {
|
||||
let context_host = self._context_host.borrow();
|
||||
let document = context_host.document_handle();
|
||||
let result = context_host
|
||||
.build_layout_pass_for_document(document, request)
|
||||
.build_layout_pass_for_document_in_environment(
|
||||
document,
|
||||
request,
|
||||
environment_override,
|
||||
)
|
||||
.and_then(|pass| {
|
||||
let Some(mut pass) = pass else {
|
||||
return Ok(None);
|
||||
@@ -499,6 +527,7 @@ impl ScriptVm {
|
||||
&mut services,
|
||||
&mut embedded_document_services,
|
||||
request,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ impl<'a> StyleInvalidationCleanup<'a> {
|
||||
self.pseudo_style_cache.clear();
|
||||
self.document_state.clear_selector_caches();
|
||||
self.document_state.lazy_invalidation_roots.clear();
|
||||
self.document_state.bump_target_context_epoch();
|
||||
self.document_state.bump_computed_context_epoch();
|
||||
let generations_after = self.document_state.generation_snapshot();
|
||||
context.trace_clear_all_fallback(self.document, generations_before, generations_after);
|
||||
}
|
||||
@@ -278,7 +278,7 @@ impl<'a> StyleInvalidationCleanup<'a> {
|
||||
self.pseudo_style_cache
|
||||
.invalidate_handles(roots.iter().copied());
|
||||
self.document_state.clear_selector_caches();
|
||||
self.document_state.bump_target_context_epoch();
|
||||
self.document_state.bump_computed_context_epoch();
|
||||
Some(immediately_invalidated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ impl DocumentStyleWorld {
|
||||
self.document_state.clear_retained_style_system();
|
||||
self.document_state.bump_source_set_generation();
|
||||
self.document_state.bump_computed_cache_generation();
|
||||
self.document_state.bump_target_context_epoch();
|
||||
self.document_state.note_style_input_change();
|
||||
self.owner_style_sheet_sources.borrow_mut().clear_all();
|
||||
*self.linked_stylesheet_sources.borrow_mut() = LinkedStylesheetSources::default();
|
||||
self.adopted_style_sheet_sources.borrow_mut().clear_all();
|
||||
@@ -169,6 +169,27 @@ impl DocumentStyleWorlds {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn visual_input_generations(
|
||||
&self,
|
||||
documents: impl IntoIterator<Item = DomHandle>,
|
||||
) -> Vec<(DomHandle, u64, u64)> {
|
||||
let mut documents = documents.into_iter().collect::<Vec<_>>();
|
||||
documents.sort_by_key(|document| document.index());
|
||||
documents.dedup();
|
||||
documents
|
||||
.into_iter()
|
||||
.map(|document| {
|
||||
let world = self.for_document(document);
|
||||
(
|
||||
document,
|
||||
world.document_state.source_set_generation(),
|
||||
world.document_state.visual_input_generation(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) fn observation_generations(
|
||||
&self,
|
||||
documents: impl IntoIterator<Item = DomHandle>,
|
||||
|
||||
@@ -262,11 +262,13 @@ impl MoliStyleEngine {
|
||||
.computed_cache_generation
|
||||
}
|
||||
|
||||
pub(crate) fn computed_style_observation_generations(
|
||||
/// Visual output changes with source inputs, not with materialization of
|
||||
/// the computed cache (for example while observing temporary print media).
|
||||
pub(crate) fn visual_style_input_generations(
|
||||
&self,
|
||||
documents: impl IntoIterator<Item = DomHandle>,
|
||||
) -> Vec<(DomHandle, u64, u64, u64)> {
|
||||
self.document_worlds.observation_generations(documents)
|
||||
) -> Vec<(DomHandle, u64, u64)> {
|
||||
self.document_worlds.visual_input_generations(documents)
|
||||
}
|
||||
|
||||
pub(crate) fn stylesheet_resource_snapshot_for_document(
|
||||
@@ -414,7 +416,7 @@ impl MoliStyleEngine {
|
||||
pub(crate) fn bump_target_context_epoch_for_document(&self, document: DomHandle) {
|
||||
self.world_for_document(document)
|
||||
.document_state
|
||||
.bump_target_context_epoch();
|
||||
.note_style_input_change();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -96,7 +96,7 @@ impl MoliStyleEngine {
|
||||
world
|
||||
.registered_custom_properties
|
||||
.register(registration, base_url)?;
|
||||
world.document_state.bump_target_context_epoch();
|
||||
world.document_state.note_style_input_change();
|
||||
world.document_state.record_source_dirty_scope(
|
||||
StyleScopeId::Document(document),
|
||||
StyleSourceDirtyReason::CustomPropertyRegistration,
|
||||
|
||||
@@ -91,7 +91,7 @@ impl MoliStyleEngine {
|
||||
.unwrap_or_default();
|
||||
if should_defer_structural_mutations(document_effects) {
|
||||
let deferred_started = profile_enabled.then(std::time::Instant::now);
|
||||
world.document_state.bump_target_context_epoch();
|
||||
world.document_state.note_style_input_change();
|
||||
world
|
||||
.pending_structural_mutations
|
||||
.push(document_effects, emulated_media, viewport);
|
||||
@@ -146,7 +146,7 @@ impl MoliStyleEngine {
|
||||
return false;
|
||||
}
|
||||
if mutation_effects_have_source_scope(effects) {
|
||||
world.document_state.bump_target_context_epoch();
|
||||
world.document_state.note_style_input_change();
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -525,6 +525,7 @@ impl MoliStyleEngine {
|
||||
let Some(world) = self.owner_document_world(host, root) else {
|
||||
return;
|
||||
};
|
||||
world.document_state.note_visual_input_change();
|
||||
self.invalidation_cleanup_for_world(&world)
|
||||
.invalidate_inline_style_subtree(host, root);
|
||||
}
|
||||
@@ -533,6 +534,7 @@ impl MoliStyleEngine {
|
||||
let Some(world) = self.owner_document_world(host, root) else {
|
||||
return;
|
||||
};
|
||||
world.document_state.note_visual_input_change();
|
||||
self.invalidation_cleanup_for_world(&world)
|
||||
.invalidate_subtrees_and_shadow_cascade_data(host, [root]);
|
||||
}
|
||||
@@ -552,7 +554,7 @@ impl MoliStyleEngine {
|
||||
};
|
||||
let source_stores = world.borrow_source_stores();
|
||||
if !world_has_style_state(self, world) {
|
||||
world.document_state.bump_target_context_epoch();
|
||||
world.document_state.note_style_input_change();
|
||||
return;
|
||||
}
|
||||
queue_style_invalidation_for_scope(
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(super) fn queue_style_invalidation_for_scope(
|
||||
let Some(source_scope) = source_scope else {
|
||||
return;
|
||||
};
|
||||
document_state.bump_target_context_epoch();
|
||||
document_state.note_style_input_change();
|
||||
let plan_started = profile_enabled.then(std::time::Instant::now);
|
||||
let work = pending_work_for_pending_cause(
|
||||
host,
|
||||
|
||||
@@ -87,6 +87,9 @@ pub(super) struct StyleDocumentState {
|
||||
computed_cache_generation: Cell<u64>,
|
||||
retained_style_system_generation: Cell<u64>,
|
||||
target_context_epoch: Cell<u64>,
|
||||
/// Source-side invalidation only. The target context also advances when
|
||||
/// derived styles are cleared, including temporary capture environments.
|
||||
visual_input_generation: Cell<u64>,
|
||||
pub(super) lazy_invalidation_roots: LazyStyleInvalidationRoots,
|
||||
#[cfg(debug_assertions)]
|
||||
completed_style_observation: RefCell<Option<CompletedStyleObservation>>,
|
||||
@@ -112,6 +115,7 @@ impl StyleDocumentState {
|
||||
computed_cache_generation: Cell::new(0),
|
||||
retained_style_system_generation: Cell::new(0),
|
||||
target_context_epoch: Cell::new(0),
|
||||
visual_input_generation: Cell::new(0),
|
||||
lazy_invalidation_roots: LazyStyleInvalidationRoots::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
completed_style_observation: RefCell::new(None),
|
||||
@@ -164,7 +168,22 @@ impl StyleDocumentState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn bump_target_context_epoch(&self) {
|
||||
pub(super) fn note_style_input_change(&self) {
|
||||
self.note_visual_input_change();
|
||||
self.bump_computed_context_epoch();
|
||||
}
|
||||
|
||||
pub(super) fn visual_input_generation(&self) -> u64 {
|
||||
self.visual_input_generation.get()
|
||||
}
|
||||
|
||||
pub(super) fn note_visual_input_change(&self) {
|
||||
self.visual_input_generation
|
||||
.set(self.visual_input_generation.get().saturating_add(1));
|
||||
}
|
||||
|
||||
/// Cache invalidation during style computation is not a new source input.
|
||||
pub(super) fn bump_computed_context_epoch(&self) {
|
||||
self.target_context_epoch
|
||||
.set(self.target_context_epoch().saturating_add(1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user