1715 lines
72 KiB
Rust
1715 lines
72 KiB
Rust
//! 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 `<source type>` 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<NodeId, crate::LayoutStyle>) -> Option<usize> {
|
|
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<NodeId, crate::LayoutStyle>,
|
|
) -> Option<usize> {
|
|
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<usize> {
|
|
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<Rect> {
|
|
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 `<tr>`/`<tbody>` 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
|
|
/// `<length-percentage>`. 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<f32>)>,
|
|
/// Authored stop positions, retained until paint so absolute lengths
|
|
/// can resolve against the final gradient-line length.
|
|
stop_positions: Vec<Option<String>>,
|
|
/// `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<f32>)>,
|
|
},
|
|
Conic {
|
|
angle: f32,
|
|
center: (f32, f32),
|
|
stops: Vec<([u8; 4], Option<f32>)>,
|
|
},
|
|
}
|
|
|
|
/// 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<String>,
|
|
/// 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 `<center>`.
|
|
/// 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<String>; 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<f32>,
|
|
/// 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
|
|
/// (`<img>` or its selected `<picture><source>`), 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<f32>; 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<Dimension>; 4],
|
|
/// Deferred `calc()`/`min()`/`max()`/`clamp()` margin expressions.
|
|
pub margin_expressions: [Option<String>; 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<f32>; 4],
|
|
/// Font- and viewport-relative padding lengths (top, right, bottom, left),
|
|
/// resolved alongside `margin_relative` during the top-down pass.
|
|
pub padding_relative: [Option<Dimension>; 4],
|
|
/// Deferred `calc()`/`min()`/`max()`/`clamp()` padding expressions.
|
|
pub padding_expressions: [Option<String>; 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<ClipPathPolygon>,
|
|
/// 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<f32>)>)>,
|
|
/// 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<f32>)>)>,
|
|
/// `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<f32>)>)>,
|
|
/// 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<BackgroundGradientLayer>,
|
|
/// 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<String>,
|
|
/// `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<String>,
|
|
/// 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<ObjectFit>,
|
|
/// `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<String>,
|
|
/// 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<String>,
|
|
pub svg_stroke: Option<String>,
|
|
pub svg_stroke_width: Option<String>,
|
|
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<f32>,
|
|
/// `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<Dimension>,
|
|
/// 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<String>,
|
|
/// 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<f32>,
|
|
/// Non-pixel `letter-spacing` retained until the inheritance pass.
|
|
pub letter_spacing_raw: Option<Dimension>,
|
|
/// Deferred functional `letter-spacing` (`calc()`, `min()`, `clamp()`).
|
|
pub letter_spacing_expression: Option<String>,
|
|
/// 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<bool>,
|
|
/// 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<String>,
|
|
/// 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<String>,
|
|
/// Computed inherited `font-optical-sizing`. `None` during cascade means
|
|
/// inherit; the top-down pass resolves every element to `Some`.
|
|
pub font_optical_sizing: Option<FontOpticalSizing>,
|
|
/// Computed inherited `font-variation-settings`. `None` during cascade
|
|
/// means inherit, while `Some(Vec::new())` is the `normal` initial value.
|
|
pub font_variation_settings: Option<Vec<FontVariationSetting>>,
|
|
/// 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<taffy::AlignItems>,
|
|
/// 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<Dimension>,
|
|
pub align_items: Option<taffy::AlignItems>,
|
|
pub justify_items: Option<taffy::JustifyItems>,
|
|
pub align_self: Option<taffy::AlignSelf>,
|
|
pub justify_self: Option<taffy::JustifySelf>,
|
|
pub align_content: Option<taffy::AlignContent>,
|
|
pub flex_direction: Option<taffy::FlexDirection>,
|
|
pub flex_wrap: Option<taffy::FlexWrap>,
|
|
pub justify_content: Option<taffy::JustifyContent>,
|
|
pub flex_grow: Option<f32>,
|
|
pub flex_shrink: Option<f32>,
|
|
/// 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<taffy::GridTemplateComponent<String>>,
|
|
pub grid_template_rows: Vec<taffy::GridTemplateComponent<String>>,
|
|
/// `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<taffy::GridAutoFlow>,
|
|
pub grid_areas: Option<Vec<Vec<String>>>,
|
|
pub grid_area_name: Option<String>,
|
|
pub grid_column: Option<taffy::Line<taffy::GridPlacement>>,
|
|
pub grid_row: Option<taffy::Line<taffy::GridPlacement>>,
|
|
/// `[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<std::collections::HashMap<String, i16>>,
|
|
pub grid_row_line_names: Option<std::collections::HashMap<String, i16>>,
|
|
/// 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<String>,
|
|
pub grid_row_raw: Option<String>,
|
|
pub column_gap: Option<f32>,
|
|
pub row_gap: Option<f32>,
|
|
/// 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<String>,
|
|
pub row_gap_expression: Option<String>,
|
|
|
|
// 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<u16>,
|
|
/// `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: <horizontal> <vertical>?` (or the `cellspacing`
|
|
/// attribute). Only meaningful on a `<table>`; 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 `<tr>`'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<bool>,
|
|
|
|
// Positioning. `position: absolute|fixed` takes the box out of normal flow.
|
|
pub position: Option<taffy::Position>,
|
|
/// 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<Dimension>; 4], // top, right, bottom, left
|
|
/// Deferred functional inset expressions in top/right/bottom/left order.
|
|
pub inset_expressions: [Option<String>; 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<Float>,
|
|
|
|
/// `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<bool>,
|
|
/// `opacity`, own (non-inherited) value in 0.0-1.0. `None` means the
|
|
/// default of 1.0.
|
|
pub opacity: Option<f32>,
|
|
/// 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<String>,
|
|
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<VerticalAlign>,
|
|
/// `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<i32>,
|
|
/// `clear`, when set: this element moves below preceding floats on the
|
|
/// given side(s), ending their float zone.
|
|
pub clear: Option<Clear>,
|
|
/// Non-inherited CSS counter operations in computed declaration order.
|
|
/// Reset operations run before increments on the same element.
|
|
pub counter_reset: Vec<CounterDirective>,
|
|
pub counter_increment: Vec<CounterDirective>,
|
|
pub counter_set: Vec<CounterDirective>,
|
|
/// 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 `<img>` it becomes
|
|
/// the element's image source and contributes intrinsic dimensions just
|
|
/// like an HTML `src`.
|
|
pub content_image: Option<String>,
|
|
/// 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<String>,
|
|
pub after_content: Option<String>,
|
|
/// Typed computed `content` items retained until the document-order
|
|
/// counter pass can resolve `counter()` and `counters()`.
|
|
pub generated_content: Option<Vec<GeneratedContentItem>>,
|
|
/// 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<Box<LayoutStyle>>,
|
|
pub after_pseudo: Option<Box<LayoutStyle>>,
|
|
|
|
/// 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 `<li>` elements draw
|
|
/// a marker from it, but it is carried on every element because it
|
|
/// inherits (a `list-style: none` on a `<ul>` must reach its `<li>`
|
|
/// children, which is how nav menus suppress bullets).
|
|
pub list_style: Option<ListStyle>,
|
|
|
|
/// `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<LineHeight>,
|
|
/// `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<WhiteSpace>,
|
|
/// `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<TextWrapStyle>,
|
|
/// Deferred functional line-height (`calc()`, `min()`, `clamp()`) resolved
|
|
/// after the element font and live viewport are known.
|
|
pub line_height_expression: Option<String>,
|
|
|
|
/// `text-transform`. Inherited. Applied to span text before shaping.
|
|
pub text_transform: Option<TextTransform>,
|
|
|
|
/// `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<bool>,
|
|
|
|
/// `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<bool>,
|
|
|
|
/// `object-fit` for a replaced element (`<img>`). 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<String>; 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<BoxShadow>,
|
|
}
|
|
|
|
/// 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 (`<img>`): 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<String>,
|
|
pub children: Vec<LayoutNode>,
|
|
}
|
|
|
|
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<NodeRect>,
|
|
}
|
|
|
|
/// 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<NodeId> =
|
|
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<Dimension>) -> 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::style::LengthPercentage> {
|
|
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<f32>; 4],
|
|
) -> taffy::Rect<taffy::style::LengthPercentage> {
|
|
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<taffy::style::LengthPercentageAuto> {
|
|
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);
|
|
}
|
|
}
|