Expand modern CSS layout and paint parity

This commit is contained in:
SGavrl committed 2026-07-31 06:47:51 +00:00
1 parent 87f6d5a47d
commit af5ae70d31
7 files changed
+4348 -576

No files matched your search

+213 -24
View File
@@ -235,6 +235,65 @@ impl BorderRadius {
}
}
/// 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 {
@@ -255,6 +314,69 @@ pub struct ClipPathPolygon {
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 {
@@ -285,6 +407,12 @@ pub struct LayoutStyle {
/// 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`.
@@ -373,8 +501,10 @@ pub struct LayoutStyle {
/// 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>)>)>,
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).
@@ -389,10 +519,13 @@ pub struct LayoutStyle {
/// 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` as a 0.0-1.0 fraction per axis (0,0 = default
/// top-left; 1,0.5 = "right center"). The fraction applies to the leftover
/// space after resolving explicit, intrinsic, cover, or contain size.
pub background_position: (f32, f32),
/// `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
@@ -571,6 +704,22 @@ pub struct LayoutStyle {
/// 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.
@@ -611,15 +760,16 @@ pub struct LayoutStyle {
/// `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` has collapsed to (near) zero. Real `opacity`
/// composites as translucent groups, which our paint step does not do;
/// collapsing it to a binary paint/don't-paint decision is enough to
/// make the extremely common "opacity:0 + visibility:hidden" collapsed
/// dropdown/panel pattern actually invisible, without needing full alpha
/// compositing.
/// 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.
@@ -633,6 +783,9 @@ pub struct LayoutStyle {
/// 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.
@@ -696,9 +849,10 @@ pub struct LayoutStyle {
/// (this is what underlines links, which are underlined by UA default).
pub underline: Option<bool>,
/// `font-style: italic|oblique`. Inherited. Selects the oblique face when
/// shaping (we embed the DejaVu Sans oblique/bold-oblique faces). `None`
/// means inherit.
/// `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
@@ -715,6 +869,12 @@ pub struct LayoutStyle {
/// 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.
@@ -766,11 +926,28 @@ 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 {
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)]
@@ -1070,15 +1247,27 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
width: dimension(style.width),
height: dimension(style.height),
};
// Tell taffy's own layout algorithm about `overflow: hidden`, not just our
// paint-time clip rects: per spec, a flex/grid item's automatic minimum
// size is content-based only when its overflow is `visible`, and `0`
// otherwise. Without this, an explicit `height: 0; overflow: hidden`
// element (the common collapsed-dropdown-panel pattern: hidden until a
// sibling checkbox is checked) still grows to fit its content, since
// taffy has no way to know it should not.
if style.overflow_hidden {
s.overflow = taffy::Point { x: taffy::style::Overflow::Hidden, y: taffy::style::Overflow::Hidden };
// 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),