//! obscura-render: the optional scoped render layer for Obscura. //! //! Obscura's default build has no layout or paint engine, which is the source //! of its speed and low memory. This crate adds a render layer behind a feature //! flag: real CSS box geometry (so getBoundingClientRect, elementFromPoint, and //! IntersectionObserver return true values) and, with the paint feature, //! rasterized PNG screenshots. //! //! Phase 1 (this file): the layout core. A `LayoutNode` tree plus a viewport is //! laid out with taffy and the resulting border-box geometry is returned per //! node. Later phases build the `LayoutNode` tree from the live DOM + computed //! styles, feed geometry back to JS, and add text + paint. use taffy::prelude::*; pub mod css; pub use css::Stylesheet; pub mod style; pub use style::compute_style; pub mod dom; pub use dom::{ layout_dom, layout_dom_with_images, layout_dom_with_resources, DomLayout, StickyLayout, }; /// Whether an image MIME type names a format supported by the renderer build. /// /// Compare the MIME essence case-insensitively and ignore parameters. HTML /// allows a `` value to include parameters, and HTTP MIME types /// are ASCII case-insensitive. Keep this list aligned with the `image` crate /// features in `Cargo.toml` plus the SVG path in `paint.rs`. pub(crate) fn source_type_supported(value: &str) -> bool { let essence = value.split_once(';').map_or(value, |(essence, _)| essence).trim(); [ "image/apng", "image/bmp", "image/gif", "image/jpeg", "image/jpg", "image/png", "image/svg+xml", "image/vnd.microsoft.icon", "image/webp", "image/x-icon", ] .iter() .any(|supported| essence.eq_ignore_ascii_case(supported)) } #[cfg(test)] mod image_capability_tests { use super::source_type_supported; #[test] fn image_mime_filter_uses_case_insensitive_essence_and_parameters() { for supported in [ "image/apng", "image/bmp", "image/gif", "image/jpeg", "image/jpg", "image/png", "image/svg+xml", "image/vnd.microsoft.icon", "image/webp", "image/x-icon", " IMAGE/WEBP ; codecs=lossless ", "Image/Svg+Xml;charset=utf-8", ] { assert!(source_type_supported(supported), "{supported}"); } for unsupported in [ "", "image/avif", "image/jxl", "image/png,image/webp", "text/html", ";image/png", ] { assert!(!source_type_supported(unsupported), "{unsupported}"); } } } #[cfg(feature = "paint")] mod paint; #[cfg(feature = "paint")] pub use paint::{ paint_dom, paint_dom_scrolled, paint_prepared, prepare_dom, prepare_dom_with_dynamic_fonts, screenshot_png, screenshot_png_scrolled, screenshot_prepared, DynamicFontFace, PreparedRender, RenderResourceCache, RenderResourceLoader, SelectedImage, }; // Real inline text layout (cosmic-text) lives behind the paint feature; the // layout-only build keeps the lighter word-split geometry. The stub lets // `dom.rs` name `inline::TextEngine` and call `try_build` unconditionally. #[cfg(feature = "paint")] pub mod inline; #[cfg(not(feature = "paint"))] pub mod inline { use obscura_dom::tree::{DomTree, NodeId}; use std::collections::HashMap; #[derive(Default)] pub struct TextEngine; impl TextEngine { pub fn new() -> Self { TextEngine } /// Layout-only builds have no shaper, so no container is ever treated /// as a cosmic-text inline formatting context: the word-split path /// handles text geometry for `getBoundingClientRect`. pub fn try_build(&mut self, _tree: &DomTree, _id: NodeId, _styles: &HashMap) -> Option { None } /// See `try_build`: inline runs likewise fall back to word-split /// geometry in layout-only builds. pub fn try_build_run( &mut self, _tree: &DomTree, _parent: NodeId, _run: &[NodeId], _styles: &HashMap, ) -> Option { None } /// Layout-only builds do not load page fonts. Preserve the same /// line-vs-fragment contract with deterministic bundled metrics. pub(crate) fn inline_font_box_height(&self, style: &crate::LayoutStyle) -> f32 { style.font_size.unwrap_or(16.0) } pub(crate) fn selected_line_height(&self, style: &crate::LayoutStyle) -> f32 { match style.line_height.unwrap_or(crate::LineHeight::Normal) { crate::LineHeight::Normal => style.font_size.unwrap_or(16.0) * 1.2, crate::LineHeight::Ratio(value) => style.font_size.unwrap_or(16.0) * value, crate::LineHeight::Px(value) => value, crate::LineHeight::Relative(crate::Dimension::Percent(value)) => { style.font_size.unwrap_or(16.0) * value } crate::LineHeight::Relative(crate::Dimension::Px(value)) => value, crate::LineHeight::Relative(_) => style.font_size.unwrap_or(16.0), } } pub(crate) fn push_generated_text( &mut self, _text: &str, _style: &crate::LayoutStyle, ) -> Option { None } pub(crate) fn measure_word(&mut self, _idx: usize) -> (f32, f32) { (0.0, 0.0) } } } /// An axis-aligned rectangle in CSS pixels, relative to the containing block. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct Rect { pub x: f32, pub y: f32, pub width: f32, pub height: f32, } impl Rect { /// The overlap of two rects, or `None` if they do not intersect (or the /// overlap is degenerate). Used to accumulate an ancestor clip chain for /// `overflow: hidden`. pub fn intersect(&self, other: &Rect) -> Option { let x0 = self.x.max(other.x); let y0 = self.y.max(other.y); let x1 = (self.x + self.width).min(other.x + other.width); let y1 = (self.y + self.height).min(other.y + other.height); if x1 > x0 && y1 > y0 { Some(Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 }) } else { None } } /// The smallest rect covering both. Used to derive a table row/section box /// from its cells, since ``/`` are not laid out as taffy boxes. pub fn union(&self, other: &Rect) -> Rect { let x0 = self.x.min(other.x); let y0 = self.y.min(other.y); let x1 = (self.x + self.width).max(other.x + other.width); let y1 = (self.y + self.height).max(other.y + other.height); Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } } } /// Per-edge box values (margin / padding / border) in CSS pixels. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct Edges { pub top: f32, pub right: f32, pub bottom: f32, pub left: f32, } /// The display modes obscura-render cares about for phase 1. Inline text layout /// arrives with the text/paint phase and is folded in then. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum Display { #[default] Block, Flex, Grid, Inline, #[allow(dead_code)] None, } /// Computed `container-type` value. Stage A carries it through the cascade; /// the layout convergence stage will use it for query eligibility. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum ContainerType { #[default] Normal, InlineSize, Size, } #[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum Dimension { #[default] Auto, Px(f32), /// 0.0-1.0 fraction of the containing block. Percent(f32), /// Font-relative and viewport-relative units, kept unresolved at parse /// time (the element font-size and viewport are not known then) and /// resolved to `Px` during `dom::layout_dom`'s top-down pass via /// [`Dimension::resolve`]. Resolving font/viewport units against a /// hardcoded 16px at /// parse time (the old behavior) silently corrupted every relative length. Em(f32), Ex(f32), Rem(f32), Vw(f32), Vh(f32), Vmin(f32), Vmax(f32), } impl Dimension { /// Resolve font/viewport-relative units to `Px`. `em_px` is the element's /// own font-size, `rem_px` the root's, and `vw`/`vh` are one hundredth of /// the viewport width/height. `Px`, `Percent`, and `Auto` pass through /// (`Percent` stays for taffy to resolve against the containing block). pub fn resolve(self, em_px: f32, rem_px: f32, vw: f32, vh: f32) -> Dimension { match self { Dimension::Em(v) => Dimension::Px(v * em_px), // Liberation Sans is the deterministic generic sans face used by // the renderer. This is its x-height as a fraction of the em, // matching Chromium's generic sans face on the capture host. Dimension::Ex(v) => Dimension::Px(v * em_px * 0.528_320_3), Dimension::Rem(v) => Dimension::Px(v * rem_px), Dimension::Vw(v) => Dimension::Px(v * vw), Dimension::Vh(v) => Dimension::Px(v * vh), Dimension::Vmin(v) => Dimension::Px(v * vw.min(vh)), Dimension::Vmax(v) => Dimension::Px(v * vw.max(vh)), other => other, } } } /// Computed CSS `font-optical-sizing`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum FontOpticalSizing { /// Let a variable font's `opsz` axis follow the computed font size. #[default] Auto, /// Leave the font's optical-size axis at its normal/default setting. None, } /// One canonical CSS `font-variation-settings` axis assignment. /// /// The CSS parser guarantees a printable four-byte ASCII tag and a finite /// value. Settings are stored in tag order with duplicate tags collapsed so /// shaping and rasterization can consume one deterministic axis tuple. #[derive(Debug, Clone, Copy, PartialEq)] pub struct FontVariationSetting { pub tag: [u8; 4], pub value: f32, } /// The supported uniform `border-radius` value as a CSS /// ``. Percentages resolve independently against the /// border box's width and height, so `50%` produces an ellipse on a rectangle /// and a circle on a square. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct BorderRadius { length: f32, percentage: f32, } impl BorderRadius { pub const fn pixels(length: f32) -> Self { Self { length, percentage: 0.0 } } pub const fn percentage(percentage: f32) -> Self { Self { length: 0.0, percentage } } pub fn resolve(self, width: f32, height: f32) -> (f32, f32) { ( (self.length + self.percentage * width).max(0.0), (self.length + self.percentage * height).max(0.0), ) } pub fn is_zero(self) -> bool { self.length == 0.0 && self.percentage == 0.0 } } /// One axis of a CSS `background-position`. /// /// CSS positions combine an absolute offset with a percentage of the space /// left after sizing the background image. Keeping both terms is necessary /// for sprite sheets: `-24px` is an offset from the start edge, while /// `right 10px` is equivalent to `100% - 10px`. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct BackgroundPositionAxis { length: f32, percentage: f32, } impl BackgroundPositionAxis { pub const fn pixels(length: f32) -> Self { Self { length, percentage: 0.0, } } pub const fn percentage(percentage: f32) -> Self { Self { length: 0.0, percentage, } } pub const fn length_percentage(length: f32, percentage: f32) -> Self { Self { length, percentage } } pub const fn from_end_offset(offset: Self) -> Self { Self { length: -offset.length, percentage: 1.0 - offset.percentage, } } pub fn resolve(self, leftover_space: f32) -> f32 { self.length + self.percentage * leftover_space } } /// The two axes of CSS `background-position`. /// /// The derived default is the CSS initial value `0% 0%`. A one-value /// longhand such as `background-position: 0` is parsed as `0 center`. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct BackgroundPosition { pub x: BackgroundPositionAxis, pub y: BackgroundPositionAxis, } impl BackgroundPosition { pub const fn new(x: BackgroundPositionAxis, y: BackgroundPositionAxis) -> Self { Self { x, y } } } /// Fill rule for a CSS `clip-path: polygon(...)`. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum ClipPathFillRule { #[default] Nonzero, Evenodd, } /// The supported CSS basic-shape clip. /// /// Coordinates retain their CSS length/percentage unit until paint, where /// percentages resolve independently against the border box width and height. /// This mirrors the reference-box resolution used by browser engines and /// avoids baking responsive polygon geometry into computed style. #[derive(Debug, Clone, PartialEq)] pub struct ClipPathPolygon { pub fill_rule: ClipPathFillRule, pub points: Vec<(Dimension, Dimension)>, } /// One CSS gradient from the ordered `background-image` layer list. /// /// CSS paints the first authored layer closest to the user, so paint walks /// this vector in reverse over the background color. Keeping the authored /// order is essential when a hero combines a linear fade with several /// partially transparent radial highlights. #[derive(Debug, Clone, PartialEq)] pub enum BackgroundGradientLayer { Linear { angle: f32, stops: Vec<([u8; 4], Option)>, /// Authored stop positions, retained until paint so absolute lengths /// can resolve against the final gradient-line length. stop_positions: Vec>, /// `repeating-linear-gradient()` repeats the interval from its first /// resolved stop through its last, independently of background tiling. repeating: bool, }, Radial { center: (f32, f32), stops: Vec<([u8; 4], Option)>, }, Conic { angle: f32, center: (f32, f32), stops: Vec<([u8; 4], Option)>, }, } /// One computed CSS counter operation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CounterDirective { pub name: String, pub value: i32, } /// Counter styles supported in generated text. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum GeneratedCounterStyle { #[default] Decimal, DecimalLeadingZero, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman, } /// One item in a computed `content` value. #[derive(Debug, Clone, PartialEq, Eq)] pub enum GeneratedContentItem { Text(String), Counter { name: String, style: GeneratedCounterStyle, }, Counters { name: String, separator: String, style: GeneratedCounterStyle, }, } /// The subset of CSS that influences box layout. Expanded in later phases. #[derive(Debug, Clone, Default)] pub struct LayoutStyle { pub display: Display, /// The specified `display` value was the CSS-wide `inherit` keyword. /// /// `display` is normally non-inherited, so the cascade cannot resolve this /// until the parent's computed outer/inner display is known. The DOM /// top-down pass copies that provenance and then clears this marker. pub(crate) display_inherit: bool, /// Computed CSS `container-type` (not inherited). pub container_type: ContainerType, /// The specified value was the CSS-wide `inherit` keyword. Resolved /// top-down before layout because `container-type` is otherwise /// non-inherited. pub(crate) container_type_inherit: bool, /// Computed CSS `container-name`; empty represents `none` (not inherited). pub container_names: Vec, /// The specified `container-name` value was CSS-wide `inherit`. pub(crate) container_names_inherit: bool, /// True when `display:flex` is only an internal stand-in for native HTML /// layout such as table cells, rather than the computed CSS display. /// Descendants are not CSS flex items in these containers. pub internal_flex_container: bool, /// The computed inner display is `table`. /// /// Taffy has no table display mode, so authored table boxes are represented /// by block/grid-compatible layout styles. Keep this provenance for CSS /// features such as container-query axis availability that depend on the /// computed display rather than our internal layout approximation. pub(crate) is_table_box: bool, /// The HTML UA sheet's vendor `text-align` behavior for `
`. /// Unlike ordinary `text-align:center`, it also centers fixed-width block /// descendants while leaving auto-width blocks fill-available. pub legacy_center: bool, pub width: Dimension, /// The preferred inline size is the intrinsic `fit-content` keyword. /// /// Taffy's box-size dimension cannot represent intrinsic sizing keywords, /// so `width` remains `Auto` while the DOM layout convergence pass applies /// the CSS shrink-to-fit formula from min/max-content measurements. pub width_fit_content: bool, pub height: Dimension, /// Which box edge `width`/`height` and min/max sizes describe. CSS starts /// at `content-box`; many modern reset sheets opt into `border-box`. pub box_sizing: BoxSizing, /// Whether `width`/`height` was set by an author rule (including an explicit /// `auto`). Presentational `width`/`height` HTML attributes are a lower /// priority than author CSS, so they apply only when these are false; an /// explicit `width:auto` must still suppress a `width="408"` attribute so /// the element keeps its aspect-ratio size instead of the intrinsic one. pub width_set: bool, pub height_set: bool, pub min_width: Dimension, pub min_height: Dimension, pub max_width: Dimension, pub max_height: Dimension, /// Deferred CSS math expressions for width, height, min-width, min-height, /// max-width, and max-height. Functional lengths can depend on the actual /// viewport, containing block, or computed font size and cannot be safely /// collapsed to pixels during stylesheet parsing. pub size_expressions: [Option; 6], /// `aspect-ratio` as width/height, or an image's intrinsic ratio resolved /// at layout. Lets a replaced element (or a padding-box card) derive the /// missing dimension from the given one, so a `width:100%` image gets a /// real height instead of collapsing to zero. pub aspect_ratio: Option, /// Fetched intrinsic CSS-pixel size for a replaced element. Kept alongside /// `aspect_ratio` so its taffy leaf can contribute a real min/max-content /// size when percentage dimensions are resolved through an auto-sized /// wrapper (`img { width:100%; height:auto }`). pub intrinsic_size: Option<(f32, f32)>, /// The current ratio came from HTML width/height presentation hints /// (`` or its selected ``), rather than authored /// CSS. A decoded image's natural ratio replaces this provisional ratio. pub aspect_ratio_is_mapped: bool, /// Whether this element generates a replaced box. /// /// Replaced elements with `display:inline` are atomic and therefore keep /// authored width/height/min/max sizing. Ordinary inline boxes compute /// those properties but ignore them for used layout. This provenance /// cannot be inferred from intrinsic dimensions because an unloaded or /// broken image remains replaced. pub(crate) is_replaced_box: bool, pub margin: Edges, /// Which margin sides are `auto` (top, right, bottom, left). `margin: 0 /// auto` / `margin-inline: auto` centering needs a real Auto margin, which /// the f32 `margin` cannot express; this flag drives it at taffy mapping. pub margin_auto: [bool; 4], /// Percentage margin per side (top, right, bottom, left) as a 0..1 fraction, /// `None` when the side is a fixed length. Like padding, every side resolves /// against the containing block's WIDTH; the f32 `margin` cannot carry a /// percentage, so this is resolved to px during `dom::layout_dom`'s top-down /// pass once the containing-block width is known. pub margin_percent: [Option; 4], /// Font- and viewport-relative margin lengths (top, right, bottom, left). /// These retain their unit until the top-down pass knows the element font /// size, root font size, and viewport dimensions. pub margin_relative: [Option; 4], /// Deferred `calc()`/`min()`/`max()`/`clamp()` margin expressions. pub margin_expressions: [Option; 4], pub padding: Edges, /// Percentage padding per side (top, right, bottom, left) as a 0..1 /// fraction, `None` when the side is a fixed length. All four sides resolve /// against the containing block's WIDTH (per CSS, including top/bottom): this /// is the responsive aspect-ratio-box trick (`padding-top:56.25%` reserves a /// 16:9 area). The percentage remains typed through Taffy layout so flex/grid /// min/max sizing can establish the final containing-block width first. /// `dom::layout_dom` writes Taffy's resolved used pixels back into `padding` /// before paint and geometry consumers inspect the computed layout. pub padding_percent: [Option; 4], /// Font- and viewport-relative padding lengths (top, right, bottom, left), /// resolved alongside `margin_relative` during the top-down pass. pub padding_relative: [Option; 4], /// Deferred `calc()`/`min()`/`max()`/`clamp()` padding expressions. pub padding_expressions: [Option; 4], pub border: Edges, /// `border-radius` (uniform; the first value of the shorthand). Rounds the /// background fill, replaced content, border, and shadow. pub border_radius: BorderRadius, /// `clip-path: polygon(...)`, resolved against the final border box at /// paint time. `None` is the computed `none` value. pub clip_path: Option, /// RGBA for the paint step. Parsed always (cheap), used only with `paint`. pub background_color: Option<[u8; 4]>, /// `linear-gradient(...)` background: (angle in degrees clockwise from 12 /// o'clock per CSS, list of (rgba, optional 0..1 stop position)). Modern /// hero sections use gradients heavily; without this they paint white. pub background_gradient: Option<(f32, Vec<([u8; 4], Option)>)>, /// First `radial-gradient(...)` layer: center in box-relative fractions /// and color stops. It is painted below the first linear layer, matching /// the common `linear-gradient(...), radial-gradient(...)` hero pattern. pub background_radial_gradient: Option<((f32, f32), Vec<([u8; 4], Option)>)>, /// `conic-gradient(...)` background. The angle is the CSS `from` angle, /// the center is a fraction of the border box, and stops are normalized /// during paint. Conic gradients commonly provide the color source for a /// repeated SVG mask in modern hero artwork. pub background_conic_gradient: Option<(f32, (f32, f32), Vec<([u8; 4], Option)>)>, /// Every parsed gradient in authored background-layer order. The legacy /// single-kind fields above remain populated for mask/text fast paths. pub background_gradient_layers: Vec, /// The first `url(...)` reference from `background`/`background-image` /// (gradients and repeat keywords in the same shorthand are ignored: we /// paint the referenced image, not the gradient layer). pub background_image: Option, /// `background-size`, in px, when given as explicit length(s) (a bare /// `10px` applies to both axes, matching how small square icons are /// almost always sized). pub background_size: Option<(f32, f32)>, /// Raw one/two-axis `background-size` expression, retained for paint-time /// resolution against the final owner box (`calc(100% - 2rem) auto`). pub background_size_expression: Option, /// Keyword `background-size` behavior. `None` is CSS `auto`, which uses /// the image's intrinsic dimensions rather than stretching it to the box. pub background_size_fit: Option, /// `background-position`, retained as a length-plus-percentage per axis. /// Percentages apply to the leftover space after resolving explicit, /// intrinsic, cover, or contain size. pub background_position: BackgroundPosition, /// Explicit `(repeat-x, repeat-y)` choice. `None` is the CSS initial /// `repeat` in both axes. pub background_repeat: Option<(bool, bool)>, /// `background-clip: text` / `-webkit-background-clip: text`: the background /// paints only through the element's glyphs, not as a filled box. Combined /// with a transparent text color this is the common gradient-text technique /// (hero headings, buttons like astro.build's "Get Started"); without /// honoring it those labels paint invisible. Consumed in the text paint path /// (`inline::TextEngine` fills glyphs from the background; `paint` suppresses /// the box fill so the gradient does not paint as a rectangle). pub background_clip_text: bool, /// `mask-image`/`-webkit-mask-image: url(...)`: the ubiquitous "colored, /// scalable icon" pattern (an SVG shape used as a stencil, tinted by /// `background-color`/`color` instead of carrying its own colors). Without /// this, every such icon paints as a solid filled square. pub mask_image: Option, /// Explicit `mask-size` / `-webkit-mask-size` in CSS px. pub mask_size: Option<(f32, f32)>, /// Explicit `(repeat-x, repeat-y)` choice. `None` retains the CSS default /// (`repeat` on both axes) when an explicit tile size exists, while /// preserving the legacy fill-box fallback for unsized icon masks. pub mask_repeat: Option<(bool, bool)>, /// Foreground (text) color for the paint step. pub color: Option<[u8; 4]>, /// Computed SVG presentation properties supplied by author CSS. Inline /// SVG is serialized into a standalone document for resvg; without /// carrying these values across that boundary, stylesheet-driven icons /// and text lose their fill/stroke and can become completely invisible. pub svg_fill: Option, pub svg_stroke: Option, pub svg_stroke_width: Option, pub border_color: Option<[u8; 4]>, /// Used color scheme for CSS Color 5 `light-dark()`. The renderer's /// current user preference is light; an inherited `color-scheme: dark` /// subtree switches this to true, while `normal`, `light`, or a list that /// permits light keeps the light scheme. pub color_scheme_dark: bool, pub font_size: Option, /// `font-size` given in a font/viewport-relative unit, resolved to /// `font_size` (px) during the inheritance pass against the parent and /// root font-sizes. `None` when font-size was absolute or unset. pub font_size_raw: Option, /// Deferred functional `font-size` (`clamp()`, `min()`, `max()`, `calc()`). /// These expressions must see the live viewport and parent font size; /// eagerly treating `9vw` as the number 9 made responsive headings pin to /// the minimum arm of their clamp. pub font_size_expression: Option, /// Computed `letter-spacing` in CSS pixels. This inherited property is /// resolved top-down because `em` is relative to the element's own /// computed font size while `rem` and viewport units need live context. pub letter_spacing: Option, /// Non-pixel `letter-spacing` retained until the inheritance pass. pub letter_spacing_raw: Option, /// Deferred functional `letter-spacing` (`calc()`, `min()`, `clamp()`). pub letter_spacing_expression: Option, /// Whether the computed value came from a non-`normal` declaration. /// Keeping this provenance distinguishes an explicit zero from the /// `normal` initial value while resolving the inherited property. pub letter_spacing_non_normal: Option, /// Specified CSS font weight during cascade (`1..1000`, `bolder`, or /// `lighter`), normalized to its numeric computed value by the inheritance /// pass before layout and shaping. pub font_weight: Option, /// The computed `font-family` list, lowercased. Inherited. The text engine /// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way /// Chromium picks a generic family on this host. pub font_family: Option, /// Computed inherited `font-optical-sizing`. `None` during cascade means /// inherit; the top-down pass resolves every element to `Some`. pub font_optical_sizing: Option, /// Computed inherited `font-variation-settings`. `None` during cascade /// means inherit, while `Some(Vec::new())` is the `normal` initial value. pub font_variation_settings: Option>, /// Inherited `text-align`, represented with the matching horizontal /// alignment keywords. Kept separate from flex/grid `align-items`: using /// one field for both made `text-align:left` shrink-wrap flex children. pub text_align: Option, /// Computed inherited `text-indent`. Font/viewport-relative lengths are /// resolved to pixels during the top-down inheritance pass; percentages /// remain typed until the final inline-formatting-context width is known. /// `None` during cascade means inherit, while the root resolves to the /// initial zero length. pub text_indent: Option, pub align_items: Option, pub justify_items: Option, pub align_self: Option, pub justify_self: Option, pub align_content: Option, pub flex_direction: Option, pub flex_wrap: Option, pub justify_content: Option, pub flex_grow: Option, pub flex_shrink: Option, /// Flex/grid item order. The formatting algorithm consumes children in /// order-modified document order, with source order breaking ties. pub order: i32, /// `flex-basis` (longhand, or the length in a `flex:` shorthand). `Auto` /// is the default. Fixed-basis sidebars/columns (`flex: 0 0 260px`) /// collapse to content width without it. pub flex_basis: Dimension, // CSS Grid. Tracks are stored as taffy sizing functions; `grid_areas` is the // parsed `grid-template-areas` matrix (one Vec per row, `.` for a null cell), // resolved to line placements on children in a later pass. pub grid_template_columns: Vec>, pub grid_template_rows: Vec>, /// `grid-template-columns: subgrid`. Taffy has no native subgrid track /// component, so `dom` resolves the safe full-span column subset after /// measuring the non-subgridded ancestor's intrinsic track contributions. pub grid_template_columns_subgrid: bool, pub grid_auto_flow: Option, pub grid_areas: Option>>, pub grid_area_name: Option, pub grid_column: Option>, pub grid_row: Option>, /// `[line-name]` -> 1-based grid line number, parsed from /// `grid-template-columns`/`-rows`. taffy has no native named-line support, /// so children placed by name (`grid-column: content-start / content-end`, /// widely used by the Guardian and other editorial grids) are resolved to /// numeric lines against these maps in `dom::resolve_grid_areas`. pub grid_col_line_names: Option>, pub grid_row_line_names: Option>, /// Raw `grid-column`/`grid-row` value when it references a named line (so it /// cannot be resolved to a `taffy::Line` until the parent's line-name map is /// known). Resolved in the same later pass; numeric/`span` values still fill /// `grid_column`/`grid_row` directly at cascade time. pub grid_column_raw: Option, pub grid_row_raw: Option, pub column_gap: Option, pub row_gap: Option, /// Deferred gap values. Font- and viewport-relative units cannot be /// converted until the element's computed font-size and the live viewport /// are known; eagerly treating `rem` as 16px breaks pages that customize /// the root font-size. pub column_gap_expression: Option, pub row_gap_expression: Option, // CSS Multi-column Layout. A count greater than one creates that many // equal-width fragmentainer columns in `dom::build`; the first layout // pass measures the in-flow child boxes at their real column width and a // bounded balancing pass then distributes them in column-major order. // `None` is the initial `auto` column count. pub column_count: Option, /// `break-inside: avoid` makes this box an atomic balancing unit. The /// current box-level multicol implementation cannot fragment the inside /// of a child yet, but retaining the computed value makes that limitation /// explicit and lets the balancing path distinguish authored break /// avoidance as finer-grained fragmentation is added. pub break_inside_avoid: bool, /// `border-spacing: ?` (or the `cellspacing` /// attribute). Only meaningful on a ``; taffy has no native table /// display mode, so `dom::propagate_border_spacing` distributes this down /// as the table's own row gap and each descendant ``'s column gap. pub border_spacing: Option<(f32, f32)>, /// Computed `border-collapse`. This property is inherited; `None` means /// no value was specified on this node yet and is resolved top-down /// before table construction. The collapsed-border conflict/paint model /// is still approximate, but collapsed tables must at minimum contribute /// no border-spacing to their geometry. pub border_collapse: Option, // Positioning. `position: absolute|fixed` takes the box out of normal flow. pub position: Option, /// Distinguishes `fixed` from `absolute`; both map to taffy's absolute /// layout mode, but fixed boxes use the initial containing block. pub position_fixed: bool, /// Distinguishes `sticky` from ordinary relative positioning. Sticky boxes /// remain in normal flow; their insets constrain a scroll-time translation /// and therefore must not be applied as taffy relative offsets. pub position_sticky: bool, pub inset: [Option; 4], // top, right, bottom, left /// Deferred functional inset expressions in top/right/bottom/left order. pub inset_expressions: [Option; 4], /// `overflow`/-x/-y other than `visible`: clips this element's descendants /// to its border box during paint. This is what makes the ubiquitous /// "visually-hidden but accessible" pattern (a 1x1 absolutely-positioned, /// clipped box used for skip-links and screen-reader-only labels) actually /// invisible instead of painting its text wherever it lands. pub overflow_hidden: bool, /// Independent computed overflow clips. `overflow_hidden` remains the /// aggregate compatibility/BFC flag; paint and automatic minimum sizing /// must consult the relevant axis. pub overflow_clip_x: bool, pub overflow_clip_y: bool, pub(crate) overflow_axes_set: bool, pub(crate) overflow_specified_x: u8, pub(crate) overflow_specified_y: u8, pub(crate) overflow_inherit_x: bool, pub(crate) overflow_inherit_y: bool, pub(crate) overflow_scroll_x: bool, pub(crate) overflow_scroll_y: bool, /// This element's authored overflow is propagated to the viewport. Its /// own box therefore behaves as `overflow: visible` for layout/BFC /// purposes while the capture viewport supplies the paint clip. pub(crate) overflow_propagated_to_viewport: bool, /// Whether computed overflow establishes a scroll container. `clip` /// clips paint but deliberately does not establish one, which matters for /// selecting the scrollport that controls a sticky descendant. pub overflow_scroll_container: bool, /// Number of classic scrollbar gutters reserved by /// `scrollbar-gutter:stable` (one) or `stable both-edges` (two). Root /// gutters reduce the initial containing block even when the scrollbar /// itself is visually hidden in a headless screenshot. pub scrollbar_gutters: u8, /// `float: left|right`. True CSS float needs per-line reflow around the /// float's shape, which taffy's block/flex/grid modes do not do; see /// `dom::group_float_zone` for the bounded approximation this drives. pub float: Option, /// `visibility: hidden|visible`, own value. `None` means "inherit the /// ancestor's computed value" (visibility, unlike most box properties, is /// a real inherited CSS property). Resolved into `effectively_invisible` /// during `dom::layout_dom`'s inheritance pass. pub visibility_hidden: Option, /// `opacity`, own (non-inherited) value in 0.0-1.0. `None` means the /// default of 1.0. pub opacity: Option, /// First CSS animation name and its timing contract. The stylesheet /// sampler contributes animated opacity after normal declarations and /// before author `!important`, matching the animation cascade origin. pub animation_name: Option, pub animation_timing: AnimationTiming, /// `vertical-align` for a table cell's content. Cells effectively default /// to `middle` in browsers (the HTML UA sheet sets it on row groups and /// cells inherit it); obscura applies it as main-axis alignment of the /// cell's flex-column stand-in. `None` on non-cell elements. pub vertical_align: Option, /// `z-index` on a positioned element. `None` is `auto` (tree order). A /// non-zero value lifts the element's whole subtree into a separate paint /// layer: negatives under the normal flow, positives above it, sorted. pub z_index: Option, /// `clear`, when set: this element moves below preceding floats on the /// given side(s), ending their float zone. pub clear: Option, /// Non-inherited CSS counter operations in computed declaration order. /// Reset operations run before increments on the same element. pub counter_reset: Vec, pub counter_increment: Vec, pub counter_set: Vec, /// Resolved during the inheritance pass: true when this element should /// not be painted at all, either from its own or an inherited /// `visibility: hidden`, or because the product of its own and every /// ancestor's `opacity` is zero. Fractional values remain paintable and /// are isolated into composited groups by the paint pass. pub effectively_invisible: bool, /// A CSS image supplied by `content: url(...)` on a replaced element. /// This is distinct from generated pseudo text: on an `` it becomes /// the element's image source and contributes intrinsic dimensions just /// like an HTML `src`. pub content_image: Option, /// Literal text injected by a `::before`/`::after` rule with a plain /// string-literal `content` (see `css::Stylesheet::pseudo_content`). /// Rendered as an extra word-run at the start/end of this element's /// children, same as if it were real text content. pub before_content: Option, pub after_content: Option, /// Typed computed `content` items retained until the document-order /// counter pass can resolve `counter()` and `counters()`. pub generated_content: Option>, /// Computed boxes generated by `::before`/`::after`. Text-only pseudos /// continue through `before_content`/`after_content`; these styles retain /// positioned decorative boxes for layout-independent painting. pub before_pseudo: Option>, pub after_pseudo: Option>, /// True for `inline-block`/`inline-flex`/`inline-grid`: participates in /// the surrounding inline flow from the outside, like plain `inline` /// (both currently collapse to `Display::Inline` — this engine has no /// separate inline-block layout mode), but unlike plain `inline` it must /// stay a single atomic box rather than have its own content merge into /// the parent's line-breaking. `dom::is_flattenable_inline` uses this to /// avoid flattening these away: doing so would lose the element as its /// own box (including any `::before`/`::after` content attached to it). pub is_inline_block: bool, /// `display: flow-root`: generates a normal block box but establishes a /// new block formatting context, containing descendant floats and stopping /// their exclusion bands from propagating into outside siblings. pub flow_root: bool, /// `display: contents`: the element generates no box of its own; its /// children participate in the parent's formatting context directly /// (`dom::build_any` splices them into the parent's child list). Kept as a /// flag beside `display` because the element still carries inherited styles /// for its subtree and `display:none` must still win. pub display_contents: bool, /// `list-style-type` (or the `list-style` shorthand). Inherited, like in /// real CSS; `None` means "not set on this element, inherit". Resolved to /// a concrete value during the inheritance pass. Only `
  • ` elements draw /// a marker from it, but it is carried on every element because it /// inherits (a `list-style: none` on a `
      ` must reach its `
    • ` /// children, which is how nav menus suppress bullets). pub list_style: Option, /// `line-height`. Inherited. `None` means "not set, inherit"; resolved to /// a concrete value in the inheritance pass. Drives the vertical rhythm of /// shaped text (a fixed ratio made real-site prose noticeably tighter than /// Chromium). pub line_height: Option, /// `white-space`. Inherited; `None` means inherit the nearest ancestor's /// value (or the initial `normal`). The inline shaper uses this to retain /// author/source newlines in code blocks and to select wrapping behavior. pub white_space: Option, /// `text-wrap-style`. Inherited; `None` means inherit the nearest /// ancestor's value (or the initial `auto`). `Balance` keeps the natural /// line count but tightens the effective line-breaking width during the /// inline formatter's final shaping pass. pub text_wrap_style: Option, /// Deferred functional line-height (`calc()`, `min()`, `clamp()`) resolved /// after the element font and live viewport are known. pub line_height_expression: Option, /// `text-transform`. Inherited. Applied to span text before shaping. pub text_transform: Option, /// `text-decoration-line: underline` (or the `text-decoration` shorthand). /// Not inherited in CSS, but a decoration visually covers descendant inline /// text, so it is propagated into the shaped spans of the element's subtree /// (this is what underlines links, which are underlined by UA default). pub underline: Option, /// `font-style: italic|oblique`. Inherited. Selects an available oblique /// face when shaping; the bundled Linux `system-ui` face synthesizes its /// slant from DejaVu Sans regular/bold to match Chromium. `None` means /// inherit. pub font_style_italic: Option, /// `object-fit` for a replaced element (``). Controls how the decoded /// image is scaled into the element's box when their aspect ratios differ; /// `Fill` (default) stretches to the box, the rest preserve aspect ratio. /// Only consulted in the image paint path. pub object_fit: ObjectFit, /// `transform: translate(x[, y])` / `translateX` / `translateY`, stored /// unresolved as (dx, dy). Applied at paint time as an offset to the /// element's own box AND its whole descendant subtree; percentages resolve /// against the element's own border-box size then. This is what makes the /// canonical `translate(-50%,-50%)` centering of an absolutely-positioned /// box land in the right place, and moves a `translate(-9999px,0)` /// off-screen skip-link out of view instead of painting it on-screen. Not /// inherited (transform is a non-inherited property). pub transform_translate: Option<(Dimension, Dimension)>, /// Individual CSS `translate` property. This composes independently with /// the legacy `transform` property, so `transform:none` must not clear it. /// Functional values are retained separately until the final border box /// is known because percentages resolve against that box's own axes. pub individual_translate: Option<(Dimension, Dimension)>, pub individual_translate_expressions: [Option; 2], /// Independent CSS-property triggers that establish containing blocks for /// absolute and fixed descendants. Kept as a bitset so `filter:none` /// cannot clear a transform/containment trigger from another property. pub containing_block_triggers: u16, /// `transform: scale(sx[, sy])` / `scaleX` / `scaleY`, or the individual /// `scale` property, captured as (sx, sy). Raster-image paint applies it to /// the element's subtree together with [`LayoutStyle::transform_projection`]. pub transform_scale: Option<(f32, f32)>, /// The affine x/y projection of authored `rotate`, `rotateX`, `rotateY`, /// and `rotateZ` transform functions. CSS transforms without perspective /// remain affine when their input plane has z=0, so this preserves modern /// 3D-tilted image cards without introducing a full scene graph. /// Stored as `[a, b, c, d]` for `x'=a*x+c*y`, `y'=b*x+d*y`. pub transform_projection: Option<[f32; 4]>, /// Authored `transform-origin`, unresolved so percentages use the final /// border-box dimensions. `None` is the CSS initial value, 50% 50%. pub transform_origin: Option<(Dimension, Dimension)>, /// `box-shadow` (first layer only). Painted behind the element's own /// background/border box: cards, buttons, menus, and modals across the /// modern web rely on it for depth, and without it those elements paint /// flat. See [`BoxShadow`] and `paint::paint_box_shadow`. pub box_shadow: Option, } /// Whether this box has an inline outer display and participates in an inline /// formatting context. `display` retains the inner layout mode for /// inline-flex/grid, so this cannot be inferred from `Display::Inline` alone. pub(crate) fn is_inline_level_box(style: &LayoutStyle) -> bool { style.display == Display::Inline || style.is_inline_block } /// Apply CSS Display blockification while preserving the inner display mode. /// Thus inline-flex becomes flex and inline-grid becomes grid, while /// inline-block/plain inline become block. pub(crate) fn blockify_outer_display(style: &mut LayoutStyle) { if !is_inline_level_box(style) { return; } if style.display == Display::Inline { style.display = Display::Block; } style.is_inline_block = false; } pub(crate) const CB_TRIGGER_TRANSFORM: u16 = 1 << 0; pub(crate) const CB_TRIGGER_FILTER: u16 = 1 << 1; pub(crate) const CB_TRIGGER_BACKDROP_FILTER: u16 = 1 << 2; pub(crate) const CB_TRIGGER_PERSPECTIVE: u16 = 1 << 3; pub(crate) const CB_TRIGGER_CONTAIN: u16 = 1 << 4; pub(crate) const CB_TRIGGER_WILL_CHANGE: u16 = 1 << 5; pub(crate) const CB_TRIGGER_CONTENT_VISIBILITY: u16 = 1 << 6; pub(crate) const CB_TRIGGER_TRANSLATE: u16 = 1 << 7; impl LayoutStyle { /// Whether CSS box sizes compute normally but do not apply to this box's /// used geometry. The display value here is post-blockification, so roots, /// flex/grid items, floats, and out-of-flow boxes retain applicable sizes. pub(crate) fn ignores_used_box_sizes(&self) -> bool { self.display == Display::Inline && !self.is_inline_block && !self.is_replaced_box } pub(crate) fn establishes_positioning_containing_block(&self) -> bool { self.containing_block_triggers != 0 } pub(crate) fn clips_overflow_x(&self) -> bool { if self.overflow_axes_set { self.overflow_clip_x } else { self.overflow_hidden } } pub(crate) fn clips_overflow_y(&self) -> bool { if self.overflow_axes_set { self.overflow_clip_y } else { self.overflow_hidden } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Float { Left, Right, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum BoxSizing { #[default] ContentBox, BorderBox, /// A specified CSS-wide `inherit` value. The DOM's top-down computed-style /// pass resolves this to the parent's computed value before layout. Inherit, } /// One `box-shadow` layer. Offsets, blur, and spread are in CSS px; `color` is /// the resolved RGBA (falling back to the element's text color, per CSS /// `currentColor`, when the value omits a color); `inset` distinguishes an /// inner shadow from the default outer (drop) shadow. Only the first layer of a /// comma-separated list is modeled. #[derive(Debug, Clone, Copy, PartialEq)] pub struct BoxShadow { pub offset_x: f32, pub offset_y: f32, pub blur: f32, pub spread: f32, pub color: [u8; 4], pub inset: bool, } /// `object-fit` for replaced elements (``): how the image's intrinsic /// content is scaled into its box when their aspect ratios differ. `Fill` (the /// default) stretches to the whole box; the others preserve the image's aspect /// ratio, either letterboxing inside the box (`Contain`), cropping to cover it /// (`Cover`), or using the intrinsic size (`None`, or `ScaleDown` which is /// `Contain` capped at the intrinsic size so it never upscales). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ObjectFit { #[default] Fill, Contain, Cover, ScaleDown, None, } /// `vertical-align` positions for table-cell content. `baseline` (and the /// text-level values like sub/super, which do not apply to cells) map to /// `Top` as an approximation: real per-row baseline alignment needs shared /// ascent metrics across the row. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VerticalAlign { Top, Middle, Bottom, } /// `clear`: which floated side(s) an element moves below. Ends the float /// zone in `dom::build_children_with_float_zone` (the clearfix idiom). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Clear { Left, Right, Both, } /// `line-height`: `normal` (a font-relative default), a unitless multiple of /// font-size, or an absolute pixel length. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { Normal, /// Unitless number. Unlike length/percentage values, this remains a ratio /// when inherited and therefore scales with each descendant's font size. Ratio(f32), Px(f32), /// A specified length or percentage awaiting computed-value resolution. /// It becomes `Px` on the declaring element before inheritance. Relative(Dimension), } /// The legacy `white-space` shorthand values needed by inline collection and /// line breaking. `BreakSpaces` currently shares pre-wrap's wrapping model; /// its finer trailing-space opportunity rules can be added independently. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum WhiteSpace { #[default] Normal, NoWrap, Pre, PreWrap, PreLine, BreakSpaces, } /// Deterministic CSS animation sample time relative to the animation's /// timeline origin. Screenshot rendering currently uses the initial value /// (0ms); keeping it explicit lets capture plumbing freeze other engines at /// the same instant instead of depending on wall-clock settling. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct AnimationSampleTime { pub milliseconds: f32, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AnimationDirection { #[default] Normal, Reverse, Alternate, AlternateReverse, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AnimationFillMode { #[default] None, Forwards, Backwards, Both, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AnimationPlayState { #[default] Running, Paused, } /// Timing fields for the first CSS animation. Milliseconds are used /// internally so `s`, `ms`, negative delays, and calculated stagger values /// share one unit. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AnimationTiming { pub duration_ms: f32, pub delay_ms: f32, /// A finite non-negative count, or positive infinity for `infinite`. pub iteration_count: f32, pub direction: AnimationDirection, pub fill_mode: AnimationFillMode, pub play_state: AnimationPlayState, } impl Default for AnimationTiming { fn default() -> Self { Self { duration_ms: 0.0, delay_ms: 0.0, iteration_count: 1.0, direction: AnimationDirection::Normal, fill_mode: AnimationFillMode::None, play_state: AnimationPlayState::Running, } } } /// The implemented `text-wrap-style` values. Other line-breaking strategies /// such as `pretty` remain unsupported until their distinct scoring model is /// available; treating them as `auto` would make `@supports` lie. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TextWrapStyle { #[default] Auto, Balance, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextTransform { None, Uppercase, Lowercase, Capitalize, } /// `list-style-type` values we render a marker for. `Decimal` numbers the /// item by its position among sibling list items. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ListStyle { None, Disc, Circle, Square, Decimal, } /// A node in the input layout tree. `text` is carried for the paint phase; it /// does not affect layout in phase 1 (inline/text layout comes later). #[derive(Debug, Clone)] pub struct LayoutNode { pub style: LayoutStyle, #[allow(dead_code)] pub text: Option, pub children: Vec, } impl LayoutNode { pub fn leaf(style: LayoutStyle) -> Self { LayoutNode { style, text: None, children: Vec::new() } } } /// Computed geometry for one node and its subtree. #[derive(Debug, Clone, Default)] pub struct NodeRect { /// Border box, in viewport coordinates. pub border_box: Rect, pub children: Vec, } /// Lay out `root` within a `viewport` (width, height) in CSS pixels and return /// the border-box geometry per node, mirroring the input tree. pub fn layout(root: &LayoutNode, viewport: (f32, f32)) -> NodeRect { let mut tree: TaffyTree = TaffyTree::new(); let root_id = build_node(&mut tree, root); let _ = tree.compute_layout( root_id, taffy::Size { width: taffy::AvailableSpace::Definite(viewport.0), height: taffy::AvailableSpace::Definite(viewport.1), }, ); read_node(&tree, root_id) } fn build_node(tree: &mut TaffyTree, node: &LayoutNode) -> NodeId { let style = to_taffy_style(&node.style); if node.children.is_empty() { tree.new_leaf(style).expect("taffy new_leaf") } else { let child_ids: Vec = node.children.iter().map(|c| build_node(tree, c)).collect(); tree.new_with_children(style, &child_ids).expect("taffy new_with_children") } } fn read_node(tree: &TaffyTree, id: NodeId) -> NodeRect { let layout = tree.layout(id).expect("taffy layout"); NodeRect { border_box: Rect { x: layout.location.x, y: layout.location.y, width: layout.size.width, height: layout.size.height, }, children: tree .children(id) .unwrap_or_default() .iter() .map(|&cid| read_node(tree, cid)) .collect(), } } pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style { let mut s = Style::DEFAULT; s.box_sizing = match style.box_sizing { BoxSizing::ContentBox => taffy::BoxSizing::ContentBox, BoxSizing::BorderBox => taffy::BoxSizing::BorderBox, // Programmatic LayoutStyle users do not have a DOM inheritance pass; // fall back to the property's CSS initial value in that case. BoxSizing::Inherit => taffy::BoxSizing::ContentBox, }; // A block box with centered/right inline content needs a flex-column // stand-in because taffy's native block algorithm has no line alignment. // `text_align` is separate from real flex/grid `align-items`, so a // text-align declaration never changes how flex children are sized. let promote_for_alignment = style.display == Display::Block && matches!(style.text_align, Some(taffy::AlignItems::CENTER) | Some(taffy::AlignItems::FLEX_END)); s.display = match style.display { Display::Block if promote_for_alignment => taffy::style::Display::Flex, Display::Block => taffy::style::Display::Block, Display::Flex => taffy::style::Display::Flex, Display::Grid => taffy::style::Display::Grid, Display::Inline => taffy::style::Display::Flex, Display::None => taffy::style::Display::None, }; if promote_for_alignment { s.flex_direction = taffy::FlexDirection::Column; s.align_items = style.text_align; } if let Some(fd) = style.flex_direction { s.flex_direction = fd; } if let Some(fw) = style.flex_wrap { s.flex_wrap = fw; } else if style.display == Display::Inline { s.flex_direction = taffy::FlexDirection::Row; s.flex_wrap = taffy::FlexWrap::Wrap; } else { s.flex_wrap = taffy::FlexWrap::NoWrap; } s.size = taffy::Size { width: dimension(style.width), height: dimension(style.height), }; // Tell taffy's layout algorithm about computed overflow, not just our // paint-time clips: a flex/grid automatic minimum depends on the relevant // axis. Overflow propagated from html/body belongs to the viewport and // must leave the source element's own layout overflow visible. if style.overflow_hidden && !style.overflow_propagated_to_viewport { s.overflow = taffy::Point { x: if style.overflow_scroll_x { taffy::style::Overflow::Hidden } else if style.clips_overflow_x() { taffy::style::Overflow::Clip } else { taffy::style::Overflow::Visible }, y: if style.overflow_scroll_y { taffy::style::Overflow::Hidden } else if style.clips_overflow_y() { taffy::style::Overflow::Clip } else { taffy::style::Overflow::Visible }, }; } s.min_size = taffy::Size { width: dimension(style.min_width), height: dimension(style.min_height), }; s.max_size = taffy::Size { width: dimension(style.max_width), height: dimension(style.max_height), }; if let Some(ar) = style.aspect_ratio { if ar.is_finite() && ar > 0.0 { s.aspect_ratio = Some(ar); } } if style.ignores_used_box_sizes() { s.size = taffy::Size { width: taffy::Dimension::auto(), height: taffy::Dimension::auto(), }; s.min_size = taffy::Size { width: taffy::Dimension::auto(), height: taffy::Dimension::auto(), }; s.max_size = taffy::Size { width: taffy::Dimension::auto(), height: taffy::Dimension::auto(), }; s.aspect_ratio = None; } if style.display != Display::Block { if let Some(ai) = style.align_items { s.align_items = Some(ai); } } else if !promote_for_alignment { // `align-items` has no effect on a block formatting context. s.align_items = None; } s.justify_items = style.justify_items; s.align_self = style.align_self; s.justify_self = style.justify_self; s.align_content = style.align_content; if let Some(jc) = style.justify_content { s.justify_content = Some(jc); } else if style.is_inline_block { // Taffy's inline-box stand-in is a wrapping flex row. `text-align` // aligns each line of an inline-block's internal formatting context; // map that inherited value onto the row axis without affecting how // the atomic box itself participates in its parent's inline flow. s.justify_content = match style.text_align { Some(taffy::AlignItems::CENTER) => Some(taffy::JustifyContent::CENTER), Some(taffy::AlignItems::FLEX_END) => Some(taffy::JustifyContent::FLEX_END), _ => None, }; } if let Some(fg) = style.flex_grow { s.flex_grow = fg; } if let Some(fs) = style.flex_shrink { s.flex_shrink = fs; } if style.flex_basis != Dimension::Auto { s.flex_basis = dimension(style.flex_basis); } // Grid container tracks and gaps. Numeric repeat() values are expanded // during parsing, while auto-fill/auto-fit remain native taffy repetition // components so their count can use the final container size. The 0.7-era // fr->Auto row workaround (which stopped `minmax(0,1fr)` image rows from // collapsing to a sliver) is gone: taffy 0.12 treats an in-flow child's // vertical available space as indefinite, so fr rows of an auto-height grid // size to their content the way real CSS does. if style.display == Display::Grid { if !style.grid_template_columns.is_empty() { s.grid_template_columns = style.grid_template_columns.clone(); } if !style.grid_template_rows.is_empty() { s.grid_template_rows = style.grid_template_rows.clone(); } if let Some(flow) = style.grid_auto_flow { s.grid_auto_flow = flow; } } let cg = style.column_gap.unwrap_or(0.0); let rg = style.row_gap.unwrap_or(0.0); s.gap = taffy::Size { width: taffy::style::LengthPercentage::length(cg), height: taffy::style::LengthPercentage::length(rg), }; // Grid item placement (resolved from grid-area names or explicit lines). // `GridPlacement` is no longer `Copy` in taffy 0.12 (it can carry a named // line), so clone out of the borrowed style. if let Some(gc) = &style.grid_column { s.grid_column = gc.clone(); } if let Some(gr) = &style.grid_row { s.grid_row = gr.clone(); } // Positioning. Absolute/fixed take the box out of flow. if let Some(pos) = style.position { s.position = pos; if !style.position_sticky { s.inset = taffy::Rect { top: inset_lpa(style.inset[0]), right: inset_lpa(style.inset[1]), bottom: inset_lpa(style.inset[2]), left: inset_lpa(style.inset[3]), }; } } s.margin = rect_auto(style.margin, style.margin_auto); s.padding = rect_lp_percent(style.padding, style.padding_percent); s.border = rect_lp(style.border); if style.ignores_used_box_sizes() { // Block-axis margins on an ordinary non-replaced inline neither move // nor size its fragment. Padding and border do paint around the raw // font box, but they protrude outside the CSS line-height rather than // increasing line advance. Keep them in LayoutStyle for fragment // synthesis/paint and remove them only from this Taffy line surrogate. s.margin.top = taffy::LengthPercentageAuto::length(0.0); s.margin.bottom = taffy::LengthPercentageAuto::length(0.0); s.padding.top = taffy::LengthPercentage::length(0.0); s.padding.bottom = taffy::LengthPercentage::length(0.0); s.border.top = taffy::LengthPercentage::length(0.0); s.border.bottom = taffy::LengthPercentage::length(0.0); } s } fn inset_lpa(v: Option) -> taffy::style::LengthPercentageAuto { match v { Some(Dimension::Px(px)) => taffy::style::LengthPercentageAuto::length(px), Some(Dimension::Percent(p)) => taffy::style::LengthPercentageAuto::percent(p), // Relative units are resolved to Px before layout; unresolved leftovers // and `auto`/absent both map to Auto. _ => taffy::style::LengthPercentageAuto::auto(), } } fn dimension(v: Dimension) -> taffy::style::Dimension { match v { Dimension::Px(px) => taffy::style::Dimension::length(px), Dimension::Percent(p) => taffy::style::Dimension::percent(p), Dimension::Auto => taffy::style::Dimension::auto(), // Relative units are resolved to Px before layout; if one slips // through unresolved, fall back to its raw magnitude (em/rem ~16px) // rather than panicking. Dimension::Em(v) | Dimension::Rem(v) => taffy::style::Dimension::length(v * 16.0), Dimension::Ex(v) => taffy::style::Dimension::length(v * 16.0 * 0.528_320_3), Dimension::Vw(v) | Dimension::Vh(v) | Dimension::Vmin(v) | Dimension::Vmax(v) => taffy::style::Dimension::length(v), } } fn rect_lp(e: Edges) -> taffy::Rect { taffy::Rect { top: taffy::style::LengthPercentage::length(e.top), right: taffy::style::LengthPercentage::length(e.right), bottom: taffy::style::LengthPercentage::length(e.bottom), left: taffy::style::LengthPercentage::length(e.left), } } fn rect_lp_percent( e: Edges, percent: [Option; 4], ) -> taffy::Rect { let side = |value, percent| match percent { Some(percent) => taffy::style::LengthPercentage::percent(percent), None => taffy::style::LengthPercentage::length(value), }; taffy::Rect { top: side(e.top, percent[0]), right: side(e.right, percent[1]), bottom: side(e.bottom, percent[2]), left: side(e.left, percent[3]), } } fn rect_auto(e: Edges, auto: [bool; 4]) -> taffy::Rect { let side = |value, is_auto| { if is_auto { taffy::style::LengthPercentageAuto::auto() } else { taffy::style::LengthPercentageAuto::length(value) } }; taffy::Rect { top: side(e.top, auto[0]), right: side(e.right, auto[1]), bottom: side(e.bottom, auto[2]), left: side(e.left, auto[3]), } } #[cfg(test)] mod tests { use super::*; fn make_box(display: Display, w: f32, h: f32) -> LayoutStyle { LayoutStyle { display, width: Dimension::Px(w), height: Dimension::Px(h), ..Default::default() } } #[test] fn block_children_stack_vertically() { // A 1000px-wide viewport, two fixed-size block children: they should // stack top-to-bottom at the expected y offsets. let root = LayoutNode { style: make_box(Display::Block, 1000.0, 800.0), text: None, children: vec![ LayoutNode::leaf(make_box(Display::Block, 1000.0, 50.0)), LayoutNode::leaf(make_box(Display::Block, 1000.0, 30.0)), ], }; let out = layout(&root, (1000.0, 800.0)); assert_eq!(out.border_box.width, 1000.0); assert_eq!(out.children.len(), 2); assert_eq!(out.children[0].border_box.height, 50.0); assert_eq!(out.children[1].border_box.height, 30.0); // Second block begins where the first ended. assert!( (out.children[1].border_box.y - out.children[0].border_box.y).abs() >= out.children[0].border_box.height - 0.01, "blocks should stack: c0.y={} c1.y={}", out.children[0].border_box.y, out.children[1].border_box.y ); } #[test] fn block_auto_margins_absorb_horizontal_free_space() { let centered = LayoutStyle { display: Display::Block, width: Dimension::Px(300.0), height: Dimension::Px(40.0), margin_auto: [false, true, false, true], ..Default::default() }; let pushed_end = LayoutStyle { display: Display::Block, width: Dimension::Px(200.0), height: Dimension::Px(40.0), margin: Edges { right: 50.0, ..Default::default() }, margin_auto: [false, false, false, true], ..Default::default() }; let root = LayoutNode { style: make_box(Display::Block, 900.0, 200.0), text: None, children: vec![LayoutNode::leaf(centered), LayoutNode::leaf(pushed_end)], }; let out = layout(&root, (900.0, 200.0)); assert!((out.children[0].border_box.x - 300.0).abs() < 0.01); assert!((out.children[1].border_box.x - 650.0).abs() < 0.01); } #[test] fn negative_flex_margin_overlays_without_shifting_items() { let main = make_box(Display::Block, 900.0, 200.0); let sidebar = LayoutStyle { display: Display::Flex, width: Dimension::Px(225.0), height: Dimension::Px(180.0), margin: Edges { left: -900.0, ..Default::default() }, ..Default::default() }; let root = LayoutNode { style: make_box(Display::Flex, 900.0, 220.0), text: None, children: vec![LayoutNode::leaf(main), LayoutNode::leaf(sidebar)], }; let out = layout(&root, (900.0, 220.0)); assert!( out.children[0].border_box.x.abs() < 0.01, "main shifted to {:?}", out.children[0].border_box ); assert!( out.children[1].border_box.x.abs() < 0.01, "overlay shifted to {:?}", out.children[1].border_box ); } #[test] fn flex_row_lays_out_horizontally() { let root = LayoutNode { style: LayoutStyle { display: Display::Flex, width: Dimension::Px(600.0), height: Dimension::Px(100.0), ..Default::default() }, text: None, children: vec![ LayoutNode::leaf(make_box(Display::Block, 200.0, 100.0)), LayoutNode::leaf(make_box(Display::Block, 200.0, 100.0)), ], }; let out = layout(&root, (600.0, 400.0)); assert_eq!(out.border_box.width, 600.0); assert_eq!(out.children.len(), 2); // In a row the second child is to the right of the first. assert!( out.children[1].border_box.x > out.children[0].border_box.x, "flex row should place children horizontally: c0.x={} c1.x={}", out.children[0].border_box.x, out.children[1].border_box.x ); } #[test] fn padding_expands_content_box_but_not_border_box() { let content_box = LayoutNode { style: LayoutStyle { display: Display::Block, width: Dimension::Px(100.0), height: Dimension::Px(100.0), padding: Edges { top: 10.0, right: 10.0, bottom: 10.0, left: 10.0 }, ..Default::default() }, text: None, children: vec![], }; let content_out = layout(&content_box, (1000.0, 800.0)); assert_eq!(content_out.border_box.width, 120.0); let mut border_box = content_box; border_box.style.box_sizing = BoxSizing::BorderBox; let border_out = layout(&border_box, (1000.0, 800.0)); assert_eq!(border_out.border_box.width, 100.0); } }