diff --git a/moli-layout/src/builder.rs b/moli-layout/src/builder.rs index 9311dcf4f2..5e5c3c0fc3 100644 --- a/moli-layout/src/builder.rs +++ b/moli-layout/src/builder.rs @@ -377,8 +377,7 @@ where root_style.scrollbar_colors(), root_style.uses_horizontal_writing_mode(), self.source.disables_viewport_user_scrolling(), - ) - .with_scrollbars_hidden(root_style.viewport_scrollbars_hidden()); + ); } fn principal_box( diff --git a/moli-layout/src/environment.rs b/moli-layout/src/environment.rs new file mode 100644 index 0000000000..c7bba1cbcc --- /dev/null +++ b/moli-layout/src/environment.rs @@ -0,0 +1,11 @@ +/// Document-wide runtime inputs for one layout demand. +/// +/// These values are independent of authored CSS and apply to every generated +/// box, including pseudo elements and anonymous boxes. The renderer supplies +/// the same target policy when composing embedded documents. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct LayoutEnvironment { + /// Removes scrollbar controls and automatic gutters. Authored stable + /// gutters still reserve space using the CSS scrollbar width. + pub scrollbars_hidden: bool, +} diff --git a/moli-layout/src/lib.rs b/moli-layout/src/lib.rs index 34384938c7..72db0713ac 100644 --- a/moli-layout/src/lib.rs +++ b/moli-layout/src/lib.rs @@ -12,6 +12,7 @@ pub(crate) const LAYOUT_SUBPIXELS_PER_CSS_PIXEL: f32 = 64.0; mod builder; mod capture; mod containment; +mod environment; mod error; mod form; mod gradient; @@ -44,6 +45,7 @@ pub use capture::{ FULL_DOCUMENT_CAPTURE_CSS_DIMENSION_LIMIT, PaintCaptureRegion, PaintCaptureRequest, PaintCaptureSurface, }; +pub use environment::LayoutEnvironment; pub use error::LayoutError; pub use layout_tree::{ FrozenCoordinateSpace, FrozenEmbeddedFrame, FrozenLayoutBox, FrozenLayoutTree, diff --git a/moli-layout/src/pass.rs b/moli-layout/src/pass.rs index f6fe5716a2..fbf2d088fe 100644 --- a/moli-layout/src/pass.rs +++ b/moli-layout/src/pass.rs @@ -81,6 +81,7 @@ where pub struct LayoutPassRequest { pub viewport: LayoutViewport, pub reason: LayoutFlushReason, + environment: crate::LayoutEnvironment, paint_capture: Option, } @@ -89,6 +90,9 @@ impl LayoutPassRequest { Self { viewport, reason, + environment: crate::LayoutEnvironment { + scrollbars_hidden: false, + }, paint_capture: None, } } @@ -104,11 +108,15 @@ impl LayoutPassRequest { reason: LayoutFlushReason, paint_capture: PaintCaptureRequest, ) -> Self { - Self { - viewport, - reason, - paint_capture: Some(paint_capture), - } + let mut request = Self::new(viewport, reason); + request.paint_capture = Some(paint_capture); + request + } + + /// Sets document-wide runtime policy without changing authored CSS styles. + pub const fn with_environment(mut self, environment: crate::LayoutEnvironment) -> Self { + self.environment = environment; + self } /// Whether this demand also needs immutable software-paint input. @@ -207,6 +215,7 @@ where let started = Instant::now(); let phase_started = Instant::now(); let mut world = build_layout_world(source, styles)?; + world.environment = request.environment; let box_tree_elapsed = phase_started.elapsed(); debug_assert!( world @@ -330,7 +339,10 @@ where { let root_id = world.root; let defining_body = world.viewport_scroll_policy.defining_body(); - world.viewport_scroll_policy.prepare_scrollbar_layout(); + let scrollbars_hidden = world.environment.scrollbars_hidden; + world + .viewport_scroll_policy + .prepare_scrollbar_layout(scrollbars_hidden); for (index, layout_box) in world.boxes.iter_mut().enumerate() { let id = crate::LayoutBoxId::from_index(index); if id == root_id { @@ -338,7 +350,9 @@ where } else if defining_body == Some(id) { layout_box.style.prepare_viewport_defining_body_layout(); } else { - layout_box.style.prepare_scrollbar_layout(false); + layout_box + .style + .prepare_scrollbar_layout(false, scrollbars_hidden); } } @@ -358,6 +372,12 @@ where } else { metrics.followup_passes_elapsed += elapsed; } + // Hidden controls cannot introduce an automatic gutter. Final output + // projection still computes scroll extents, so this demand needs no + // scrollbar-feedback overflow projection or corrective numeric pass. + if scrollbars_hidden { + break; + } let phase_started = Instant::now(); touched.append(&mut pending_feedback_seeds); let candidates = if let Some(projection) = overflow_projection.as_mut() { diff --git a/moli-layout/src/projection.rs b/moli-layout/src/projection.rs index bd6275c6e6..b2022c72e8 100644 --- a/moli-layout/src/projection.rs +++ b/moli-layout/src/projection.rs @@ -291,6 +291,9 @@ where } fn scrollbar_control_thickness(&self, id: LayoutBoxId) -> f32 { + if self.world.environment.scrollbars_hidden { + return 0.0; + } if id == self.world.root { self.world .viewport_scroll_policy @@ -354,6 +357,9 @@ where } fn has_scrollbar(&self, id: LayoutBoxId, axis: LayoutScrollbarAxis, overflowing: bool) -> bool { + if self.world.environment.scrollbars_hidden { + return false; + } if id == self.world.root { self.world .viewport_scroll_policy diff --git a/moli-layout/src/style.rs b/moli-layout/src/style.rs index beb962601e..22e58a4d67 100644 --- a/moli-layout/src/style.rs +++ b/moli-layout/src/style.rs @@ -507,7 +507,6 @@ pub struct ResolvedLayoutStyle { overflow_x: LayoutOverflowMode, overflow_y: LayoutOverflowMode, scrollbar_width: LayoutScrollbarWidth, - scrollbars_hidden: bool, scrollbar_gutter: LayoutScrollbarGutter, scrollbar_colors: Option, revealed_scrollbar_x: bool, @@ -889,7 +888,6 @@ impl ResolvedLayoutStyle { overflow_x, overflow_y, scrollbar_width, - scrollbars_hidden: false, scrollbar_gutter, scrollbar_colors, revealed_scrollbar_x: false, @@ -967,7 +965,6 @@ impl ResolvedLayoutStyle { overflow_x, overflow_y, scrollbar_width: LayoutScrollbarWidth::Auto, - scrollbars_hidden: false, scrollbar_gutter: LayoutScrollbarGutter::Auto, scrollbar_colors: None, revealed_scrollbar_x: false, @@ -1045,11 +1042,6 @@ impl ResolvedLayoutStyle { self.scrollbar_colors = colors; } - /// Hides scrollbar UI and automatic gutters, preserving explicit stable gutters. - pub fn suppress_scrollbars(&mut self) { - self.scrollbars_hidden = true; - } - /// Overrides the alignment keyword for a synthetic inline style. pub fn with_inline_alignment(mut self, alignment: LayoutInlineAlignment) -> Self { self.vertical_align.kind = alignment; @@ -1734,10 +1726,6 @@ impl ResolvedLayoutStyle { self.scrollbar_width } - pub(crate) const fn viewport_scrollbars_hidden(&self) -> bool { - self.scrollbars_hidden - } - pub(crate) const fn viewport_scrollbar_gutter(&self) -> LayoutScrollbarGutter { self.scrollbar_gutter } @@ -1765,11 +1753,11 @@ impl ResolvedLayoutStyle { self.taffy.overflow.y = taffy::Overflow::Visible; } - pub(crate) fn prepare_scrollbar_layout(&mut self, is_root: bool) { + pub(crate) fn prepare_scrollbar_layout(&mut self, is_root: bool, scrollbars_hidden: bool) { self.revealed_scrollbar_x = - !self.scrollbars_hidden && self.overflow_x == LayoutOverflowMode::Scroll; + !scrollbars_hidden && self.overflow_x == LayoutOverflowMode::Scroll; self.revealed_scrollbar_y = - !self.scrollbars_hidden && self.overflow_y == LayoutOverflowMode::Scroll; + !scrollbars_hidden && self.overflow_y == LayoutOverflowMode::Scroll; // Moli resolves CSS scrollbar policy to physical edge insets through // `LayoutPartialTree::get_scrollbar_insets`. Keep Taffy's legacy // scalar disabled so no algorithm reserves the same gutter twice. @@ -1820,10 +1808,7 @@ impl ResolvedLayoutStyle { is_root: bool, overflowing: bool, ) -> bool { - if !overflowing - || self.scrollbars_hidden - || self.scrollbar_width == LayoutScrollbarWidth::None - { + if !overflowing || self.scrollbar_width == LayoutScrollbarWidth::None { return false; } let mode = self.overflow_mode(axis); @@ -1859,7 +1844,7 @@ impl ResolvedLayoutStyle { is_root: bool, overflowing: bool, ) -> bool { - if self.scrollbars_hidden || self.scrollbar_width == LayoutScrollbarWidth::None { + if self.scrollbar_width == LayoutScrollbarWidth::None { return false; } match self.overflow_mode(axis) { @@ -1884,11 +1869,7 @@ impl ResolvedLayoutStyle { } pub(crate) fn scrollbar_control_thickness(&self) -> f32 { - if self.scrollbars_hidden { - 0.0 - } else { - self.scrollbar_width.thickness() - } + self.scrollbar_width.thickness() } pub(crate) fn scrollbar_gutter_thickness(&self, axis: LayoutScrollbarAxis) -> f32 { @@ -2006,7 +1987,6 @@ impl ResolvedLayoutStyle { overflow_x: LayoutOverflowMode::Visible, overflow_y: LayoutOverflowMode::Visible, scrollbar_width: parent.scrollbar_width, - scrollbars_hidden: parent.scrollbars_hidden, scrollbar_gutter: LayoutScrollbarGutter::Auto, scrollbar_colors: parent.scrollbar_colors, revealed_scrollbar_x: false, @@ -2075,7 +2055,6 @@ impl ResolvedLayoutStyle { overflow_x: LayoutOverflowMode::Visible, overflow_y: LayoutOverflowMode::Visible, scrollbar_width: parent.scrollbar_width, - scrollbars_hidden: parent.scrollbars_hidden, scrollbar_gutter: LayoutScrollbarGutter::Auto, scrollbar_colors: parent.scrollbar_colors, revealed_scrollbar_x: false, diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index 67896fb031..2b86514028 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -320,7 +320,6 @@ pub(crate) struct ViewportScrollPolicy { scrollbar_style: ViewportScrollbarStyle, embedder_allows_user_scroll: bool, embedder_allows_scrollbar_controls: bool, - scrollbars_hidden: bool, revealed_scrollbar_x: bool, revealed_scrollbar_y: bool, } @@ -371,17 +370,11 @@ impl ViewportScrollPolicy { }, embedder_allows_user_scroll: !user_scrolling_disabled, embedder_allows_scrollbar_controls: !user_scrolling_disabled, - scrollbars_hidden: false, revealed_scrollbar_x: false, revealed_scrollbar_y: false, } } - pub(crate) const fn with_scrollbars_hidden(mut self, hidden: bool) -> Self { - self.scrollbars_hidden = hidden; - self - } - pub(crate) const fn defining_body(self) -> Option { match self.defining_box { ViewportDefiningBox::Root => None, @@ -389,8 +382,8 @@ impl ViewportScrollPolicy { } } - pub(crate) fn prepare_scrollbar_layout(&mut self) { - let allowed = self.allows_scrollbar_controls(); + pub(crate) fn prepare_scrollbar_layout(&mut self, scrollbars_hidden: bool) { + let allowed = !scrollbars_hidden && self.allows_scrollbar_controls(); self.revealed_scrollbar_x = allowed && self.effective_overflow[0] == LayoutOverflowMode::Scroll; self.revealed_scrollbar_y = @@ -468,8 +461,7 @@ impl ViewportScrollPolicy { } const fn allows_scrollbar_controls(self) -> bool { - !self.scrollbars_hidden - && self.embedder_allows_scrollbar_controls + self.embedder_allows_scrollbar_controls && !matches!(self.scrollbar_style.width, LayoutScrollbarWidth::None) } @@ -662,6 +654,7 @@ where /// Subtree and synthetic sources still use the same internal root slot. pub(crate) root_is_document_element: bool, pub(crate) quirks_mode: style::context::QuirksMode, + pub(crate) environment: crate::LayoutEnvironment, pub(crate) viewport_scroll_policy: ViewportScrollPolicy, pub(crate) viewport_layout: ViewportLayoutState, pub(crate) css_image_references: Vec>, @@ -686,6 +679,7 @@ where root: LayoutBoxId::from_index(0), root_is_document_element, quirks_mode: style::context::QuirksMode::NoQuirks, + environment: crate::LayoutEnvironment::default(), viewport_scroll_policy: ViewportScrollPolicy::default(), viewport_layout: ViewportLayoutState::default(), css_image_references: Vec::new(), diff --git a/moli-layout/tests/phase5_output_contract.rs b/moli-layout/tests/phase5_output_contract.rs index 413ade4c7c..fc1e7ae1f1 100644 --- a/moli-layout/tests/phase5_output_contract.rs +++ b/moli-layout/tests/phase5_output_contract.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use moli_layout::{ DocumentLayoutServices, LayoutControlSurfaceHit, LayoutDisplay, LayoutElementCategory, - LayoutElementSemantics, LayoutError, LayoutFlushReason, LayoutFragmentKind, LayoutNamespace, - LayoutPaintedSurfaceHit, LayoutPassRequest, LayoutPassResult, LayoutPoint, LayoutPosition, - LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect, LayoutScrollbarColors, - LayoutScrollbarGutter, LayoutScrollbarPart, LayoutScrollbarWidth, LayoutSource, - LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport, PaintBrush, - PaintCaptureRequest, PaintColor, PaintFragment, PaintShape, ResolvedLayoutElementStyles, - ResolvedLayoutStyle, build_layout_pass, + LayoutElementSemantics, LayoutEnvironment, LayoutError, LayoutFlushReason, LayoutFragmentKind, + LayoutNamespace, LayoutPaintedSurfaceHit, LayoutPassRequest, LayoutPassResult, LayoutPoint, + LayoutPosition, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect, + LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarPart, LayoutScrollbarWidth, + LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport, + PaintBrush, PaintCaptureRequest, PaintColor, PaintFragment, PaintShape, + ResolvedLayoutElementStyles, ResolvedLayoutStyle, build_layout_pass, }; use style::Atom; use taffy::{ @@ -803,6 +803,83 @@ fn auto_scrollbar_feedback_reveals_the_perpendicular_axis() { assert!(extent.vertical_scrollbar.is_some()); } +#[test] +fn hidden_scrollbar_environment_skips_feedback_and_preserves_scroll_ranges() { + let mut source = Source(vec![ + Node::element("root", vec![1]), + Node::element("scroller", vec![2]), + Node::element("content", Vec::new()), + ]); + source.0[1].scroll = LayoutPoint::new(0.0, 60.0); + let mut styles = Styles::default(); + styles + .0 + .insert(0, fixed_size(LayoutDisplay::Block, 320.0, 240.0)); + styles.0.insert( + 1, + resolved( + LayoutDisplay::Block, + Style { + size: Size { + width: length(200.0), + height: length(100.0), + }, + overflow: Point { + x: Overflow::Scroll, + y: Overflow::Scroll, + }, + ..Style::default() + }, + ), + ); + styles + .0 + .insert(2, fixed_size(LayoutDisplay::Block, 200.0, 200.0)); + + // The same authored styles need three iterations with classic controls: + // vertical overflow reveals a gutter, which then causes horizontal overflow. + let classic = build(&source, &mut styles); + assert_eq!(classic.metrics.numeric_layout_pass_count, 3); + assert_eq!( + classic.element_metrics_for_source(1).unwrap().client_size, + moli_layout::LayoutSize::new(185.0, 85.0) + ); + + let hidden = build_with_request( + &source, + &mut styles, + LayoutPassRequest::with_paint(LayoutViewport::new(320, 240, 1.0), LayoutFlushReason::Test) + .with_environment(LayoutEnvironment { + scrollbars_hidden: true, + }), + ); + assert_eq!(hidden.metrics.numeric_layout_pass_count, 1); + assert_eq!(hidden.metrics.numeric_feedback_invalidated_node_count, 0); + assert_eq!( + hidden + .metrics + .numeric_feedback_overflow_recomputed_node_count, + 0 + ); + let metrics = hidden.element_metrics_for_source(1).unwrap(); + assert_eq!( + metrics.client_size, + moli_layout::LayoutSize::new(200.0, 100.0) + ); + assert_eq!( + metrics.scroll_size, + moli_layout::LayoutSize::new(200.0, 200.0) + ); + let box_id = hidden.source_output(1).unwrap().principal_box.unwrap(); + let extent = hidden.scroll_extent(box_id).unwrap(); + assert_eq!(extent.applied_offset, LayoutPoint::new(0.0, 60.0)); + assert_eq!(extent.maximum_offset, LayoutPoint::new(0.0, 100.0)); + assert!(extent.allows_user_scroll_y); + assert!(extent.horizontal_scrollbar.is_none()); + assert!(extent.vertical_scrollbar.is_none()); + assert!(extent.scrollbar_corner.is_none()); +} + fn fixed_inline_font() -> (ResolvedLayoutStyle, DocumentLayoutServices) { use style::values::computed::font::{ FamilyName, FontFamily, FontFamilyList, FontFamilyNameSyntax, SingleFontFamily, @@ -1198,12 +1275,21 @@ fn hidden_scrollbar_policy_preserves_explicit_stable_gutters_without_controls() }, ); scroller.set_scrollbar_style(width, gutter, None); - scroller.suppress_scrollbars(); styles.0.insert(1, scroller); styles .0 .insert(2, fixed_size(LayoutDisplay::Block, 400.0, 300.0)); - let output = build(&source, &mut styles); + let output = build_with_request( + &source, + &mut styles, + LayoutPassRequest::with_paint( + LayoutViewport::new(320, 240, 1.0), + LayoutFlushReason::Test, + ) + .with_environment(LayoutEnvironment { + scrollbars_hidden: true, + }), + ); let metrics = output.element_metrics_for_source(1).unwrap(); assert_eq!( metrics.client_size, diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs index 858ef75727..68f6b925a9 100644 --- a/moli-renderer-v8/src/layout_renderer/mod.rs +++ b/moli-renderer-v8/src/layout_renderer/mod.rs @@ -49,6 +49,9 @@ fn build_native_layout_pass_recursive( .dom_host() .owner_document_handle(root) .unwrap_or_else(|| runtime.document_handle()); + let request = request.with_environment(moli_layout::LayoutEnvironment { + scrollbars_hidden: runtime.scrollbars_hidden(), + }); document_stack.push(document); let source = source_view::NativeLayoutSourceView::with_paint_resources( runtime, diff --git a/moli-renderer-v8/src/layout_renderer/style_resolver.rs b/moli-renderer-v8/src/layout_renderer/style_resolver.rs index bb7a4a07fb..4c14be7dcd 100644 --- a/moli-renderer-v8/src/layout_renderer/style_resolver.rs +++ b/moli-renderer-v8/src/layout_renderer/style_resolver.rs @@ -14,7 +14,6 @@ pub(super) struct NativeLayoutStyleResolver<'a> { runtime: &'a JsContextHost, reads: StyleObservation<'a>, scripting_enabled: bool, - scrollbars_hidden: bool, profile: Option, } @@ -47,7 +46,6 @@ impl<'a> NativeLayoutStyleResolver<'a> { runtime, reads, scripting_enabled: runtime.document_scripting_enabled(document), - scrollbars_hidden: runtime.scrollbars_hidden(), profile: moli_trace::cpu_profile_enabled() .then(NativeLayoutStyleResolverProfile::default), } @@ -140,14 +138,8 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { ); } let phase_started = self.profile.as_ref().map(|_| Instant::now()); - let mut before = before.map(ResolvedLayoutStyle::from_stylo); - let mut after = after.map(ResolvedLayoutStyle::from_stylo); - if self.scrollbars_hidden { - resolved.suppress_scrollbars(); - for pseudo in before.iter_mut().chain(after.iter_mut()) { - pseudo.suppress_scrollbars(); - } - } + let before = before.map(ResolvedLayoutStyle::from_stylo); + let after = after.map(ResolvedLayoutStyle::from_stylo); if let Some(profile) = self.profile.as_mut() { profile.eager_pseudo_count = profile .eager_pseudo_count @@ -208,11 +200,7 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { } let phase_started = self.profile.as_ref().map(|_| Instant::now()); let resolved = computed.map(|computed| { - let mut resolved = ResolvedLayoutStyle::from_stylo(computed); - if self.scrollbars_hidden { - resolved.suppress_scrollbars(); - } - ResolvedLayoutPseudoStyle::new(resolved) + ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed)) }); if let Some(profile) = self.profile.as_mut() { profile.marker_projection_ns = profile.marker_projection_ns.saturating_add( @@ -258,9 +246,6 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { let phase_started = self.profile.as_ref().map(|_| Instant::now()); let mut resolved = ResolvedLayoutStyle::from_stylo(computed); resolved.force_layout_display(display); - if self.scrollbars_hidden { - resolved.suppress_scrollbars(); - } if let Some(profile) = self.profile.as_mut() { profile.anonymous_projection_ns = profile.anonymous_projection_ns.saturating_add( phase_started