//! CSS 2.1 ยง11.1.1's `overflow` property, reduced to the one question the three-engine //! oracle can answer without platform-dependent pixels: does a box get a scrollbar on an //! axis. The dump's per-node probe (`docs/dump-format.md` -> "Scrollbar probe") reports //! each element's client and scroll dimensions, which differ exactly when the element //! scrolls that axis, and `harness::compare::agrees_on_scrollbars` scores this seam //! against the browsers' unanimous verdict. One plain-data function a generated candidate //! implements: //! //! - [`resolve`]-shaped ([`OverflowResolver`]): given a box's computed `overflow`, its own //! client (padding-box) size and the extent of its content, returns whether a scrollbar //! appears on each axis. //! //! Scope. In: `visible`, `hidden`, `scroll` and `auto` on block-level boxes, and whether a //! scrollbar appears in the boolean sense. Out: the pixel width a scrollbar steals from //! its descendants (platform- and theme-dependent, and deliberately not in the dump //! contract), `overflow-x`/`overflow-y` as independent values, scroll position, //! interactivity, nested scroll containers, and painting a scrollbar in `paint.rs`. //! //! Two things a candidate has to get right that the property's name does not suggest, //! both of them measured off Chromium, Firefox and WebKit rather than read off the //! property's definition (all three agree on every case in this seam's corpus): //! //! - A box reports overflow when its content spills past its padding box whatever its own //! `overflow` value -- `visible` included. What `overflow` changes is the other //! direction: a box that clips keeps its own spill out of its ancestors' extents, which //! is why [`OverflowInputs::content_width`] already has a clipping descendant's spill //! removed before a candidate sees it. //! - The root element's client box is the viewport's, not its own, so the root reports //! overflow whenever the page is larger than the viewport. [`inputs_from_layout`] //! supplies that client box; [`OverflowInputs::is_root`] says which box it was. //! //! [`resolve_baseline`] is the seam's deliberately wrong starting candidate: no box ever //! gets a scrollbar, which is what `crates/layout` did before this seam existed -- nothing //! in it resolved `overflow` at all. /// The computed `overflow` value, as CSS 2.1 11.1.1 defines it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Overflow { Visible, Hidden, Scroll, Auto, } impl Overflow { /// The computed keyword as `getComputedStyle` spells it. Anything unrecognized is /// `visible`, the initial value. pub fn parse(keyword: &str) -> Overflow { match keyword.trim() { "hidden" => Overflow::Hidden, "scroll" => Overflow::Scroll, "auto" => Overflow::Auto, _ => Overflow::Visible, } } /// Whether this value clips the box's content, keeping the box's own spill out of its /// ancestors' extents. Every value but `visible` does. pub fn scrolls(self) -> bool { !matches!(self, Overflow::Visible) } } /// Plain-data inputs to one box's scrollbar decision. `content_width`/`content_height` are /// the extent of everything the box lays out inside it, measured from its own content-box /// origin; `client_width`/`client_height` are its padding box, the same pair CSSOM-View /// reports. `fragment.rs` measures both from the already-laid-out fragment tree, so a /// candidate here is never asked to redo layout. #[derive(Debug, Clone, Copy, PartialEq)] pub struct OverflowInputs { pub overflow: Overflow, pub client_width: f64, pub client_height: f64, pub content_width: f64, pub content_height: f64, /// Whether `box-sizing` is `border-box` rather than `content-box` /// (`dom::computed::is_border_box`). It changes what the box's own `width` meant, and /// so whether content that fits the border box still overflows the padding box. pub border_box_sizing: bool, /// Whether this is the root element, whose `client_width`/`client_height` above are /// the viewport's rather than its own box's (this module's own doc comment). pub is_root: bool, } /// Whether a scrollbar appears on each axis, in the boolean sense the dump's probe /// reports and nothing more. #[derive(Debug, Clone, Copy, PartialEq)] pub struct OverflowOutputs { pub scrollbar_x: bool, pub scrollbar_y: bool, } /// One candidate's implementation of the seam: plain data in, plain data out. Every /// candidate module the generation loop compiles exports a function of this type named /// `resolve`. pub type OverflowResolver = fn(&OverflowInputs) -> OverflowOutputs; /// The seam's deliberately wrong starting candidate (this module's own doc comment): no /// box ever scrolls, whatever its `overflow` value and however far its content spills. pub fn resolve_baseline(_inputs: &OverflowInputs) -> OverflowOutputs { OverflowOutputs { scrollbar_x: false, scrollbar_y: false } } /// Every box's inputs, read off the finished fragment tree rather than computed during /// layout -- the same shape `crate::stacking::StackingInputs::from_layout` takes, and for /// the same reason: this seam's scope stops at whether a scrollbar appears, so nothing it /// decides feeds back into where a box is placed. `viewport` supplies the root element's /// client box, which is the viewport's rather than the root box's own. /// /// A child that clips contributes only its own border box to its parent's content extent; /// a child whose `overflow` is `visible` contributes its whole subtree, since its own /// spilled content keeps spilling into the parent's scrolling area. pub fn inputs_from_layout( document: &dom::Document, root: &crate::fragment::Fragment, viewport: (f64, f64), ) -> Vec<(dom::node::NodeId, OverflowInputs)> { let mut out = Vec::new(); scan(document, root, 0.0, 0.0, true, viewport, &mut out); out } /// Appends `frag`'s inputs and its descendants', and returns the subtree's own extent in /// absolute coordinates as `(right, bottom)`. fn scan( document: &dom::Document, frag: &crate::fragment::Fragment, parent_x: f64, parent_y: f64, is_root: bool, viewport: (f64, f64), out: &mut Vec<(dom::node::NodeId, OverflowInputs)>, ) -> (f64, f64) { let x = parent_x + frag.offset_x; let y = parent_y + frag.offset_y; let border_left = dom::computed::border_left_width(document, frag.node); let border_right = dom::computed::border_right_width(document, frag.node); let border_top = dom::computed::border_top_width(document, frag.node); let border_bottom = dom::computed::border_bottom_width(document, frag.node); let padding_box_left = x + border_left; let padding_box_top = y + border_top; let padding_box_width = (frag.width - border_left - border_right).max(0.0); let padding_box_height = (frag.height - border_top - border_bottom).max(0.0); let mut content_right = padding_box_left; let mut content_bottom = padding_box_top; for child in &frag.children { let (child_right, child_bottom) = scan(document, child, x, y, false, viewport, out); content_right = content_right.max(child_right); content_bottom = content_bottom.max(child_bottom); } // `overflow-x` alone: `overflow-x`/`overflow-y` as independent values are out of this // seam's scope (`specs/overflow-scrollbars.md`), and every corpus it is scored on sets // only the `overflow` shorthand, so the two computed values are always equal there. let overflow = Overflow::parse(&dom::computed::overflow_x(document, frag.node)); let (client_width, client_height) = if is_root { viewport } else { (padding_box_width, padding_box_height) }; out.push(( frag.node, OverflowInputs { overflow, client_width, client_height, content_width: (content_right - padding_box_left).max(0.0), content_height: (content_bottom - padding_box_top).max(0.0), border_box_sizing: dom::computed::is_border_box(document, frag.node), is_root, }, )); if overflow.scrolls() { // A clipping box's own spill stops here: its parent's scrolling area sees the box // itself, not what the box clipped. (x + frag.width, y + frag.height) } else { ((x + frag.width).max(content_right), (y + frag.height).max(content_bottom)) } } #[cfg(test)] mod tests { use super::*; fn inputs() -> OverflowInputs { OverflowInputs { overflow: Overflow::Scroll, client_width: 100.0, client_height: 50.0, content_width: 400.0, content_height: 200.0, border_box_sizing: false, is_root: false, } } #[test] fn baseline_reports_no_scrollbar_on_a_box_that_plainly_scrolls() { let out = resolve_baseline(&inputs()); assert!(!out.scrollbar_x, "not the 400px of content in a 100px box"); assert!(!out.scrollbar_y, "not the 200px of content in a 50px box"); } #[test] fn baseline_reports_no_scrollbar_on_the_root_either() { let mut i = inputs(); i.overflow = Overflow::Visible; i.is_root = true; let out = resolve_baseline(&i); assert!(!out.scrollbar_x); assert!(!out.scrollbar_y); } #[test] fn every_value_but_visible_gives_the_box_a_scrolling_box() { assert!(!Overflow::Visible.scrolls()); assert!(Overflow::Hidden.scrolls()); assert!(Overflow::Scroll.scrolls()); assert!(Overflow::Auto.scrolls()); } #[test] fn parse_falls_back_to_the_initial_value() { assert_eq!(Overflow::parse("hidden"), Overflow::Hidden); assert_eq!(Overflow::parse("scroll"), Overflow::Scroll); assert_eq!(Overflow::parse("auto"), Overflow::Auto); assert_eq!(Overflow::parse("visible"), Overflow::Visible); assert_eq!(Overflow::parse("clip"), Overflow::Visible, "not a CSS 2.1 value"); } }