fix(print): isolate PDF style and font environments from screen state

This commit is contained in:
ldm0
2026-10-21 02:18:52 +08:00
committed by Donough Liu
parent 41e608f91c
commit f3a4e64995
17 changed files with 294 additions and 66 deletions
+14 -3
View File
@@ -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(
+17 -17
View File
@@ -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");
}
+32 -3
View File
@@ -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,
)
}
+2 -2
View File
@@ -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>,
+6 -4
View File
@@ -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,
+20 -1
View File
@@ -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));
}