Files

2530 lines
95 KiB
Rust

// SPDX-License-Identifier: MIT OR Apache-2.0
//
// The Stylo-to-Taffy projection uses the standalone `stylo_taffy` crate from
// DioxusLabs/blitz commit d788124ab881f9bb537cb452ec1d837604a374a8.
use std::sync::Arc;
use style::{
Atom,
color::ColorSpace,
computed_values::{
content_visibility::T as StyloContentVisibility, isolation::T as StyloIsolation,
mix_blend_mode::T as StyloMixBlendMode, writing_mode::T as StyloWritingMode,
},
properties::ComputedValues,
properties::generated::longhands::position::computed_value::T as StyloPosition,
properties::generated::longhands::scrollbar_width::computed_value::T as StyloScrollbarWidth,
properties::generated::longhands::{
direction::computed_value::T as StyloDirection,
unicode_bidi::computed_value::T as StyloUnicodeBidi,
},
servo_arc::Arc as ServoArc,
values::{
computed::{
AlignmentBaseline, BorderStyle as StyloBorderStyle, Content, ContentItem, Float,
OutlineStyle as StyloOutlineStyle, Overflow, basic_shape::ClipPath as StyloClipPath,
length::CSSPixelLength,
},
generics::{
box_::{
BaselineShift as GenericBaselineShift, BaselineShiftKeyword,
Perspective as GenericPerspective,
},
flex::GenericFlexBasis,
grid::GenericGridTemplateComponent,
image::GenericImage,
length::{GenericMaxSize, GenericSize},
position::PreferredRatio,
transform::{Rotate, Scale, Translate},
ui::GenericScrollbarColor,
},
specified::box_::{DisplayInside, DisplayOutside},
specified::{
TextAlignKeyword, WillChangeBits,
text::{TextTransform, TextTransformCase},
},
},
};
use taffy::{BoxSizing, Display as TaffyDisplay, Position as TaffyPosition, Size, Style};
use crate::{
LayoutPoint, LayoutRect, LayoutScrollbarAxis, LayoutScrollbarColors, LayoutScrollbarGutter,
LayoutScrollbarWidth, LayoutTransform2D, PaintBlendMode, PaintBorderColors, PaintBorderStyle,
PaintBorderStyles, PaintBoxShadow, PaintColor, PaintCornerRadii, PaintCornerRadius,
PaintEdgeSizes, PaintFragment,
};
/// Marker families implemented by the Phase 4 list formatter.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LayoutListMarkerType {
None,
Decimal,
LowerAlpha,
UpperAlpha,
Disc,
Circle,
Square,
DisclosureOpen,
DisclosureClosed,
String(Arc<str>),
Symbols(Vec<Arc<str>>),
Fallback,
}
fn stylo_blend_mode(mode: StyloMixBlendMode) -> PaintBlendMode {
match mode {
StyloMixBlendMode::Normal => PaintBlendMode::Normal,
StyloMixBlendMode::Multiply => PaintBlendMode::Multiply,
StyloMixBlendMode::Screen => PaintBlendMode::Screen,
StyloMixBlendMode::Overlay => PaintBlendMode::Overlay,
StyloMixBlendMode::Darken => PaintBlendMode::Darken,
StyloMixBlendMode::Lighten => PaintBlendMode::Lighten,
StyloMixBlendMode::ColorDodge => PaintBlendMode::ColorDodge,
StyloMixBlendMode::ColorBurn => PaintBlendMode::ColorBurn,
StyloMixBlendMode::HardLight => PaintBlendMode::HardLight,
StyloMixBlendMode::SoftLight => PaintBlendMode::SoftLight,
StyloMixBlendMode::Difference => PaintBlendMode::Difference,
StyloMixBlendMode::Exclusion => PaintBlendMode::Exclusion,
StyloMixBlendMode::Hue => PaintBlendMode::Hue,
StyloMixBlendMode::Saturation => PaintBlendMode::Saturation,
StyloMixBlendMode::Color => PaintBlendMode::Color,
StyloMixBlendMode::Luminosity => PaintBlendMode::Luminosity,
StyloMixBlendMode::PlusLighter => PaintBlendMode::PlusLighter,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LayoutListMarkerPosition {
Inside,
#[default]
Outside,
}
/// CSS whitespace processing mode retained before Parley shaping.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum InlineWhiteSpaceCollapse {
#[default]
Collapse,
Preserve,
PreserveBreaks,
BreakSpaces,
}
/// Case transform applied while producing an IFC's shared logical text.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum InlineTextTransform {
#[default]
None,
Uppercase,
Lowercase,
Capitalize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum InlineDirection {
#[default]
Ltr,
Rtl,
}
/// Physical block-flow direction retained at the Stylo/Taffy boundary.
///
/// Taffy currently has no writing-mode input, so its numeric algorithms run
/// in horizontal-tb coordinates. Keeping the full authored mode here lets the
/// browser adapter physicalize the result without confusing `vertical-lr`
/// and `vertical-rl`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum LayoutWritingMode {
#[default]
HorizontalTb,
VerticalRl,
VerticalLr,
}
impl LayoutWritingMode {
const fn from_stylo(value: StyloWritingMode) -> Self {
match value {
StyloWritingMode::HorizontalTb => Self::HorizontalTb,
StyloWritingMode::VerticalRl => Self::VerticalRl,
StyloWritingMode::VerticalLr => Self::VerticalLr,
}
}
pub(crate) const fn is_horizontal(self) -> bool {
matches!(self, Self::HorizontalTb)
}
/// Physical edge used as block-start by vertical normal flow.
pub(crate) const fn vertical_block_start_is_right(self) -> Option<bool> {
match self {
Self::HorizontalTb => None,
Self::VerticalRl => Some(true),
Self::VerticalLr => Some(false),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum LayoutOverflowMode {
#[default]
Visible,
Clip,
Hidden,
Auto,
Scroll,
}
impl LayoutOverflowMode {
fn from_stylo(value: Overflow) -> Self {
match value {
Overflow::Visible => Self::Visible,
Overflow::Clip => Self::Clip,
Overflow::Hidden => Self::Hidden,
Overflow::Auto => Self::Auto,
Overflow::Scroll => Self::Scroll,
}
}
fn from_taffy(value: taffy::Overflow) -> Self {
match value {
taffy::Overflow::Visible => Self::Visible,
taffy::Overflow::Clip => Self::Clip,
taffy::Overflow::Hidden => Self::Hidden,
// Synthetic tests historically used Taffy's `Scroll` as CSS
// `auto`, because the adapter previously had zero-width bars.
taffy::Overflow::Scroll => Self::Auto,
}
}
pub(crate) const fn creates_scroll_container(self) -> bool {
matches!(self, Self::Hidden | Self::Auto | Self::Scroll)
}
pub(crate) const fn allows_user_scroll(self) -> bool {
matches!(self, Self::Auto | Self::Scroll)
}
/// Overflow values propagated to a viewport use a different used-value
/// mapping from an ordinary element. `visible` becomes the viewport's
/// ordinary automatic scrolling behavior, while `clip` becomes `hidden`
/// so script scrolling remains available without user scrolling or UI.
pub(crate) const fn for_viewport(self) -> Self {
match self {
Self::Visible => Self::Auto,
Self::Clip => Self::Hidden,
Self::Hidden | Self::Auto | Self::Scroll => self,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum InlineUnicodeBidi {
#[default]
Normal,
Embed,
Isolate,
BidiOverride,
IsolateOverride,
Plaintext,
}
/// Alignment of an inline-level box relative to its parent inline box or line.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LayoutInlineAlignment {
#[default]
Baseline,
TextTop,
Middle,
TextBottom,
Top,
Bottom,
}
/// The two independent components of the CSS `vertical-align` shorthand.
/// Positive `baseline_shift` values raise content in CSS coordinates.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct InlineVerticalAlign {
pub(crate) kind: LayoutInlineAlignment,
pub(crate) baseline_shift: f32,
}
/// The two independent components retained from CSS `aspect-ratio`.
///
/// Taffy's public style currently stores only the numeric ratio. Keeping the
/// `auto` component here is essential for replaced elements: `1 / 1` replaces
/// an image's natural ratio, while `auto 1 / 1` uses that natural ratio and
/// only falls back to 1:1 when no natural ratio exists.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) enum PreferredAspectRatio {
#[default]
Auto,
Ratio(f32),
AutoAndRatio(f32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ResolvedAspectRatio {
pub(crate) ratio: Option<f32>,
/// The box whose dimensions the ratio constrains.
pub(crate) box_sizing: BoxSizing,
}
impl PreferredAspectRatio {
fn from_components(auto: bool, ratio: Option<f32>) -> Self {
match (auto, usable_aspect_ratio(ratio)) {
(_, None) => Self::Auto,
(false, Some(ratio)) => Self::Ratio(ratio),
(true, Some(ratio)) => Self::AutoAndRatio(ratio),
}
}
fn from_taffy(ratio: Option<f32>) -> Self {
Self::from_components(false, ratio)
}
fn numeric_ratio(self) -> Option<f32> {
match self {
Self::Auto => None,
Self::Ratio(ratio) | Self::AutoAndRatio(ratio) => Some(ratio),
}
}
fn resolve_for_replaced(
self,
inherent_ratio: Option<f32>,
authored_box_sizing: BoxSizing,
) -> ResolvedAspectRatio {
let inherent_ratio = usable_aspect_ratio(inherent_ratio);
match self {
Self::Ratio(ratio) => ResolvedAspectRatio {
ratio: Some(ratio),
box_sizing: authored_box_sizing,
},
Self::Auto => ResolvedAspectRatio {
ratio: inherent_ratio,
box_sizing: BoxSizing::ContentBox,
},
// Blink's BoxSizingForAspectRatio() uses content-box for the
// combined `auto <ratio>` value, including when its ratio is used
// as the fallback because no natural ratio is available.
Self::AutoAndRatio(fallback) => ResolvedAspectRatio {
ratio: inherent_ratio.or(Some(fallback)),
box_sizing: BoxSizing::ContentBox,
},
}
}
}
/// CSS display classification retained across box construction.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LayoutDisplay {
None,
Contents,
Block,
FlowRoot,
Inline,
InlineBlock,
Flex,
InlineFlex,
Grid,
InlineGrid,
BlockListItem,
InlineListItem,
Table,
InlineTable,
TableCaption,
TableRowGroup,
TableHeaderGroup,
TableFooterGroup,
TableColumnGroup,
TableColumn,
TableRow,
TableCell,
}
impl LayoutDisplay {
pub const fn debug_name(self) -> &'static str {
match self {
Self::None => "none",
Self::Contents => "contents",
Self::Block => "block",
Self::FlowRoot => "flow-root",
Self::Inline => "inline",
Self::InlineBlock => "inline-block",
Self::Flex => "flex",
Self::InlineFlex => "inline-flex",
Self::Grid => "grid",
Self::InlineGrid => "inline-grid",
Self::BlockListItem => "block-list-item",
Self::InlineListItem => "inline-list-item",
Self::Table => "table",
Self::InlineTable => "inline-table",
Self::TableCaption => "table-caption",
Self::TableRowGroup => "table-row-group",
Self::TableHeaderGroup => "table-header-group",
Self::TableFooterGroup => "table-footer-group",
Self::TableColumnGroup => "table-column-group",
Self::TableColumn => "table-column",
Self::TableRow => "table-row",
Self::TableCell => "table-cell",
}
}
pub const fn is_inline_level(self) -> bool {
matches!(
self,
Self::Inline
| Self::InlineBlock
| Self::InlineFlex
| Self::InlineGrid
| Self::InlineListItem
| Self::InlineTable
)
}
pub(crate) const fn is_inline_flow(self) -> bool {
matches!(self, Self::Inline | Self::InlineListItem)
}
pub(crate) const fn is_flex_container(self) -> bool {
matches!(self, Self::Flex | Self::InlineFlex)
}
pub(crate) const fn is_grid_container(self) -> bool {
matches!(self, Self::Grid | Self::InlineGrid)
}
pub(crate) const fn is_list_item(self) -> bool {
matches!(self, Self::BlockListItem | Self::InlineListItem)
}
pub(crate) const fn is_table(self) -> bool {
matches!(
self,
Self::Table
| Self::InlineTable
| Self::TableCaption
| Self::TableRowGroup
| Self::TableHeaderGroup
| Self::TableFooterGroup
| Self::TableColumnGroup
| Self::TableColumn
| Self::TableRow
| Self::TableCell
)
}
}
/// Exact CSS positioning mode retained beside Taffy's reduced two-state model.
///
/// Taffy 0.12 represents both CSS `static` and `relative` as
/// [`taffy::Position::Relative`], and both `absolute` and `fixed` as
/// [`taffy::Position::Absolute`]. Layout must retain the browser-level value so
/// containing-block selection does not accidentally use the direct box parent.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LayoutPosition {
#[default]
Static,
Relative,
Absolute,
Fixed,
Sticky,
}
impl LayoutPosition {
pub const fn debug_name(self) -> &'static str {
match self {
Self::Static => "static",
Self::Relative => "relative",
Self::Absolute => "absolute",
Self::Fixed => "fixed",
Self::Sticky => "sticky",
}
}
pub(crate) const fn is_positioned(self) -> bool {
!matches!(self, Self::Static)
}
pub(crate) const fn is_absolute(self) -> bool {
matches!(self, Self::Absolute)
}
pub(crate) const fn is_fixed(self) -> bool {
matches!(self, Self::Fixed)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum GeneratedContent {
Normal,
None,
Items {
text: Arc<str>,
has_unsupported_items: bool,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ResolvedLayoutTransform {
pub(crate) transform: LayoutTransform2D,
pub(crate) has_unsupported_3d: bool,
pub(crate) establishes_property_space: bool,
}
impl ResolvedLayoutTransform {
pub(crate) const IDENTITY: Self = Self {
transform: LayoutTransform2D::IDENTITY,
has_unsupported_3d: false,
establishes_property_space: false,
};
}
/// Owned style input for one pass-local layout box.
///
/// `computed` deliberately stays alive beside the converted Taffy style. A
/// Taffy calc value can contain a pointer into the Stylo value, so dropping the
/// `ComputedValues` before the world would make otherwise-owned Taffy data
/// invalid.
#[derive(Clone)]
pub struct ResolvedLayoutStyle {
pub(crate) computed: Option<ServoArc<ComputedValues>>,
pub(crate) taffy: Style<Atom>,
preferred_aspect_ratio: PreferredAspectRatio,
display: LayoutDisplay,
background_color: PaintColor,
border_colors: PaintBorderColors,
generated_content: GeneratedContent,
font_size: f32,
line_height: f32,
/// Blink expands `line-height: normal` using every font actually selected
/// during shaping. Explicit line heights keep using the primary strut.
include_used_font_metrics: bool,
text_color: PaintColor,
white_space_collapse: InlineWhiteSpaceCollapse,
text_transform: InlineTextTransform,
text_align: parley::Alignment,
direction: InlineDirection,
writing_mode: LayoutWritingMode,
unicode_bidi: InlineUnicodeBidi,
vertical_align: InlineVerticalAlign,
text_projection_deferred: bool,
overflow_clips: bool,
overflow_x: LayoutOverflowMode,
overflow_y: LayoutOverflowMode,
scrollbar_width: LayoutScrollbarWidth,
scrollbar_gutter: LayoutScrollbarGutter,
scrollbar_colors: Option<LayoutScrollbarColors>,
revealed_scrollbar_x: bool,
revealed_scrollbar_y: bool,
out_of_flow: bool,
position: LayoutPosition,
sticky_inset: taffy::Rect<taffy::LengthPercentageAuto>,
establishes_transform_containing_block: bool,
synthetic_transform: Option<LayoutTransform2D>,
visible: bool,
pointer_events: bool,
order: i32,
intrinsic_sizing_deferred: bool,
grid_template_mode_deferred: bool,
explicit_z_index: Option<i32>,
opacity: f32,
blend_mode: PaintBlendMode,
has_filter_effect: bool,
has_clip_path: bool,
has_mask: bool,
isolation: bool,
layout_containment: bool,
paint_containment: bool,
will_change_containment: bool,
will_change_position: bool,
will_change_stacking_context: bool,
list_marker_type: LayoutListMarkerType,
list_marker_position: LayoutListMarkerPosition,
}
impl std::fmt::Debug for ResolvedLayoutStyle {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ResolvedLayoutStyle")
.field("has_computed_values", &self.computed.is_some())
.field("display", &self.display)
.field("preferred_aspect_ratio", &self.preferred_aspect_ratio)
.field("background_color", &self.background_color)
.field("border_colors", &self.border_colors)
.field("generated_content", &self.generated_content)
.field("font_size", &self.font_size)
.field("line_height", &self.line_height)
.field("text_color", &self.text_color)
.field("white_space_collapse", &self.white_space_collapse)
.field("text_transform", &self.text_transform)
.field("text_align", &self.text_align)
.field("direction", &self.direction)
.field("writing_mode", &self.writing_mode)
.field("unicode_bidi", &self.unicode_bidi)
.field("vertical_align", &self.vertical_align)
.field("text_projection_deferred", &self.text_projection_deferred)
.field("overflow_clips", &self.overflow_clips)
.field("out_of_flow", &self.out_of_flow)
.field("position", &self.position)
.field(
"establishes_transform_containing_block",
&self.establishes_transform_containing_block,
)
.field(
"has_synthetic_transform",
&self.synthetic_transform.is_some(),
)
.field("visible", &self.visible)
.field("pointer_events", &self.pointer_events)
.field("order", &self.order)
.field("intrinsic_sizing_deferred", &self.intrinsic_sizing_deferred)
.field(
"grid_template_mode_deferred",
&self.grid_template_mode_deferred,
)
.field("explicit_z_index", &self.explicit_z_index)
.field("opacity", &self.opacity)
.field("blend_mode", &self.blend_mode)
.field("has_filter_effect", &self.has_filter_effect)
.field("has_clip_path", &self.has_clip_path)
.field("has_mask", &self.has_mask)
.field("isolation", &self.isolation)
.field("layout_containment", &self.layout_containment)
.field("paint_containment", &self.paint_containment)
.field("will_change_containment", &self.will_change_containment)
.field("will_change_position", &self.will_change_position)
.field(
"will_change_stacking_context",
&self.will_change_stacking_context,
)
.field("list_marker_type", &self.list_marker_type)
.field("list_marker_position", &self.list_marker_position)
.finish_non_exhaustive()
}
}
impl ResolvedLayoutStyle {
/// Converts one retained Stylo style while preserving its allocation for
/// the full lifetime of the pass-local Taffy projection.
pub fn from_stylo(computed: ServoArc<ComputedValues>) -> Self {
let display = classify_display(&computed);
let background_color = stylo_background_color(&computed);
let border_colors = stylo_border_colors(&computed);
let generated_content = stylo_generated_content(&computed);
let (font_size, line_height) = stylo_font_metrics(&computed);
let include_used_font_metrics = matches!(
computed.clone_line_height(),
style::values::computed::font::LineHeight::Normal
);
let text_color = stylo_text_color(&computed);
let white_space_collapse = match computed.clone_white_space_collapse() {
style::computed_values::white_space_collapse::T::Collapse => {
InlineWhiteSpaceCollapse::Collapse
}
style::computed_values::white_space_collapse::T::Preserve => {
InlineWhiteSpaceCollapse::Preserve
}
style::computed_values::white_space_collapse::T::PreserveBreaks => {
InlineWhiteSpaceCollapse::PreserveBreaks
}
style::computed_values::white_space_collapse::T::BreakSpaces => {
InlineWhiteSpaceCollapse::BreakSpaces
}
};
let text_transform_value = computed.clone_text_transform();
let text_transform = match text_transform_value.case() {
TextTransformCase::None => InlineTextTransform::None,
TextTransformCase::Uppercase => InlineTextTransform::Uppercase,
TextTransformCase::Lowercase => InlineTextTransform::Lowercase,
TextTransformCase::Capitalize => InlineTextTransform::Capitalize,
};
let text_align = match computed.clone_text_align() {
TextAlignKeyword::Start => parley::Alignment::Start,
TextAlignKeyword::End => parley::Alignment::End,
TextAlignKeyword::Left | TextAlignKeyword::MozLeft => parley::Alignment::Left,
TextAlignKeyword::Right | TextAlignKeyword::MozRight => parley::Alignment::Right,
TextAlignKeyword::Center | TextAlignKeyword::MozCenter => parley::Alignment::Center,
TextAlignKeyword::Justify => parley::Alignment::Justify,
};
let direction = match computed.clone_direction() {
StyloDirection::Ltr => InlineDirection::Ltr,
StyloDirection::Rtl => InlineDirection::Rtl,
};
let writing_mode = LayoutWritingMode::from_stylo(computed.clone_writing_mode());
let unicode_bidi = match computed.clone_unicode_bidi() {
StyloUnicodeBidi::Normal => InlineUnicodeBidi::Normal,
StyloUnicodeBidi::Embed => InlineUnicodeBidi::Embed,
StyloUnicodeBidi::Isolate => InlineUnicodeBidi::Isolate,
StyloUnicodeBidi::BidiOverride => InlineUnicodeBidi::BidiOverride,
StyloUnicodeBidi::IsolateOverride => InlineUnicodeBidi::IsolateOverride,
StyloUnicodeBidi::Plaintext => InlineUnicodeBidi::Plaintext,
};
let (vertical_align, vertical_align_deferred) =
stylo_vertical_align(&computed, font_size, line_height);
let text_projection_deferred = text_transform_value.intersects(TextTransform::FULL_WIDTH)
|| text_transform_value.intersects(TextTransform::FULL_SIZE_KANA)
|| vertical_align_deferred;
let overflow_clips = stylo_overflow_clips(&computed);
let overflow_x = LayoutOverflowMode::from_stylo(computed.clone_overflow_x());
let overflow_y = LayoutOverflowMode::from_stylo(computed.clone_overflow_y());
let scrollbar_width = match computed.clone_scrollbar_width() {
StyloScrollbarWidth::Auto => LayoutScrollbarWidth::Auto,
StyloScrollbarWidth::Thin => LayoutScrollbarWidth::Thin,
StyloScrollbarWidth::None => LayoutScrollbarWidth::None,
};
let computed_gutter = computed.clone_scrollbar_gutter();
let scrollbar_gutter =
if computed_gutter.contains(style::values::computed::ScrollbarGutter::BOTH_EDGES) {
LayoutScrollbarGutter::StableBothEdges
} else if computed_gutter.contains(style::values::computed::ScrollbarGutter::STABLE) {
LayoutScrollbarGutter::Stable
} else {
LayoutScrollbarGutter::Auto
};
let scrollbar_colors = match computed.clone_scrollbar_color() {
GenericScrollbarColor::Auto => None,
GenericScrollbarColor::Colors { thumb, track } => {
let current = computed.clone_color();
Some(LayoutScrollbarColors::new(
absolute_paint_color(thumb.resolve_to_absolute(&current)),
absolute_paint_color(track.resolve_to_absolute(&current)),
))
}
};
let stylo_position = computed.clone_position();
let position = match stylo_position {
StyloPosition::Static => LayoutPosition::Static,
StyloPosition::Relative => LayoutPosition::Relative,
StyloPosition::Absolute => LayoutPosition::Absolute,
StyloPosition::Fixed => LayoutPosition::Fixed,
StyloPosition::Sticky => LayoutPosition::Sticky,
};
let position_style = computed.get_position();
let intrinsic_sizing_deferred = [&position_style.height, &position_style.min_height]
.into_iter()
.any(|size| !matches!(size, GenericSize::Auto | GenericSize::LengthPercentage(_)))
|| [&position_style.max_height].into_iter().any(|size| {
!matches!(
size,
GenericMaxSize::None | GenericMaxSize::LengthPercentage(_)
)
})
|| [&position_style.width, &position_style.min_width]
.into_iter()
.any(|size| {
!matches!(
size,
GenericSize::Auto
| GenericSize::LengthPercentage(_)
| GenericSize::MinContent
| GenericSize::MaxContent
| GenericSize::FitContent
| GenericSize::Stretch
| GenericSize::WebkitFillAvailable
)
})
|| !matches!(
&position_style.max_width,
GenericMaxSize::None
| GenericMaxSize::LengthPercentage(_)
| GenericMaxSize::MinContent
| GenericMaxSize::MaxContent
| GenericMaxSize::FitContent
| GenericMaxSize::Stretch
| GenericMaxSize::WebkitFillAvailable
);
let grid_template_mode_deferred = [
&position_style.grid_template_rows,
&position_style.grid_template_columns,
]
.into_iter()
.any(|template| {
matches!(
template,
GenericGridTemplateComponent::Subgrid(_) | GenericGridTemplateComponent::Masonry
)
});
let out_of_flow = !matches!(
stylo_position,
StyloPosition::Static | StyloPosition::Relative | StyloPosition::Sticky
) || computed.clone_float() != Float::None;
let z_index = computed.clone_z_index();
let explicit_z_index = (!z_index.is_auto()).then(|| z_index.integer_or(0));
let opacity = computed.clone_opacity().clamp(0.0, 1.0);
let blend_mode = stylo_blend_mode(computed.clone_mix_blend_mode());
let effects = computed.get_effects();
let has_filter_effect =
!effects.filter.0.is_empty() || !effects.backdrop_filter.0.is_empty();
let has_clip_path = !matches!(computed.clone_clip_path(), StyloClipPath::None);
let has_mask = computed
.get_svg()
.mask_image
.0
.iter()
.any(|image| !matches!(image, GenericImage::None));
let isolation = computed.clone_isolation() == StyloIsolation::Isolate;
let contain = computed.clone_contain();
let mut layout_containment = contain.contains(style::values::computed::Contain::LAYOUT);
let mut paint_containment = contain.contains(style::values::computed::Contain::PAINT);
if computed.clone_content_visibility() != StyloContentVisibility::Visible {
// Blink folds `content-visibility:auto/hidden` into effective
// layout and paint containment before asking the LayoutObject
// whether containment applies to its principal box. Standalone
// Stylo only performs that adjustment for its Gecko embedding, so
// Moli projects the same effective bits at this browser seam.
layout_containment = true;
paint_containment = true;
}
let will_change = computed.clone_will_change().bits;
let will_change_containment = will_change.contains(WillChangeBits::CONTAIN);
let will_change_position = will_change.contains(WillChangeBits::POSITION);
let will_change_stacking_context = will_change.intersects(
WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL
| WillChangeBits::TRANSFORM
| WillChangeBits::CONTAIN
| WillChangeBits::OPACITY
| WillChangeBits::PERSPECTIVE
| WillChangeBits::Z_INDEX
| WillChangeBits::POSITION
| WillChangeBits::VIEW_TRANSITION_NAME,
);
let order = computed.clone_order();
let list_marker_type = stylo_list_marker_type(&computed);
let list_marker_position = match computed.clone_list_style_position() {
style::computed_values::list_style_position::T::Inside => {
LayoutListMarkerPosition::Inside
}
style::computed_values::list_style_position::T::Outside => {
LayoutListMarkerPosition::Outside
}
};
let specified_aspect_ratio = match position_style.aspect_ratio.ratio {
PreferredRatio::None => None,
PreferredRatio::Ratio(ratio) => Some(ratio.0.0 / ratio.1.0),
};
let preferred_aspect_ratio = PreferredAspectRatio::from_components(
position_style.aspect_ratio.auto,
specified_aspect_ratio,
);
let mut taffy = stylo_taffy::to_taffy_style(&computed);
taffy.size = Size {
width: taffy_size_dimension(&position_style.width, taffy.size.width),
height: taffy_size_dimension(&position_style.height, taffy.size.height),
};
taffy.min_size = Size {
width: taffy_size_dimension(&position_style.min_width, taffy.min_size.width),
height: taffy_size_dimension(&position_style.min_height, taffy.min_size.height),
};
taffy.max_size = Size {
width: taffy_max_size_dimension(&position_style.max_width, taffy.max_size.width),
height: taffy_max_size_dimension(&position_style.max_height, taffy.max_size.height),
};
// Taffy's generic leaf algorithm transfers aspect ratios before a
// replaced-element measure callback runs. CSS Sizing 4 defines zero,
// infinite and NaN ratios as degenerate, so normalize them at the
// Stylo/Taffy seam instead of relying only on replaced measurement.
taffy.aspect_ratio = preferred_aspect_ratio.numeric_ratio();
if matches!(position_style.flex_basis, GenericFlexBasis::Content) {
// Blitz's fixed stylo_taffy revision predates Taffy's typed
// `content` flex-basis. Preserve the Stylo distinction here so
// Taffy ignores the preferred main size and follows the content
// flex-base-size algorithm instead of treating it as `auto`.
taffy.flex_basis = taffy::Dimension::content();
}
taffy.item_is_table = matches!(display, LayoutDisplay::Table | LayoutDisplay::InlineTable);
let sticky_inset = taffy.inset;
let establishes_transform_containing_block =
computed.get_box().has_transform_or_perspective()
|| will_change.intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE);
if matches!(display, LayoutDisplay::Block | LayoutDisplay::BlockListItem)
&& (layout_containment || paint_containment)
{
// Layout and paint containment establish an independent formatting
// context. Taffy's FlowRoot keeps the computed CSS display intact
// in `display` while selecting the non-collapsing block algorithm.
taffy.display = TaffyDisplay::FlowRoot;
}
let visible = computed.clone_visibility() == style::computed_values::visibility::T::Visible;
let pointer_events =
computed.clone_pointer_events() != style::computed_values::pointer_events::T::None;
if matches!(position, LayoutPosition::Static | LayoutPosition::Sticky) {
// `stylo_taffy` must map both CSS static and relative to Taffy's
// single in-flow `Relative` variant. Taffy would therefore apply
// author insets to a static box unless the browser-level adapter
// clears them here.
taffy.inset = taffy::Rect {
left: taffy::LengthPercentageAuto::auto(),
right: taffy::LengthPercentageAuto::auto(),
top: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
};
}
Self {
computed: Some(computed),
taffy,
preferred_aspect_ratio,
display,
background_color,
border_colors,
generated_content,
font_size,
line_height,
include_used_font_metrics,
text_color,
white_space_collapse,
text_transform,
text_align,
direction,
writing_mode,
unicode_bidi,
vertical_align,
text_projection_deferred,
overflow_clips,
overflow_x,
overflow_y,
scrollbar_width,
scrollbar_gutter,
scrollbar_colors,
revealed_scrollbar_x: false,
revealed_scrollbar_y: false,
out_of_flow,
position,
sticky_inset,
establishes_transform_containing_block,
synthetic_transform: None,
visible,
pointer_events,
order,
intrinsic_sizing_deferred,
grid_template_mode_deferred,
explicit_z_index,
opacity,
blend_mode,
has_filter_effect,
has_clip_path,
has_mask,
isolation,
layout_containment,
paint_containment,
will_change_containment,
will_change_position,
will_change_stacking_context,
list_marker_type,
list_marker_position,
}
}
/// Creates a deterministic style for DOM-free construction tests.
pub fn synthetic(
display: LayoutDisplay,
mut taffy: Style<Atom>,
background_color: PaintColor,
) -> Self {
taffy.aspect_ratio = usable_aspect_ratio(taffy.aspect_ratio);
let preferred_aspect_ratio = PreferredAspectRatio::from_taffy(taffy.aspect_ratio);
taffy.display = taffy_display(display);
taffy.item_is_table = matches!(display, LayoutDisplay::Table | LayoutDisplay::InlineTable);
let overflow_clips = taffy.overflow.x != taffy::Overflow::Visible
|| taffy.overflow.y != taffy::Overflow::Visible;
let overflow_x = LayoutOverflowMode::from_taffy(taffy.overflow.x);
let overflow_y = LayoutOverflowMode::from_taffy(taffy.overflow.y);
let out_of_flow = taffy.position == TaffyPosition::Absolute;
let position = if out_of_flow {
LayoutPosition::Absolute
} else {
LayoutPosition::Static
};
let sticky_inset = taffy.inset;
Self {
computed: None,
taffy,
preferred_aspect_ratio,
display,
background_color,
border_colors: PaintBorderColors::all(PaintColor::BLACK),
generated_content: GeneratedContent::None,
font_size: 16.0,
line_height: 19.2,
include_used_font_metrics: false,
text_color: PaintColor::BLACK,
white_space_collapse: InlineWhiteSpaceCollapse::Collapse,
text_transform: InlineTextTransform::None,
text_align: parley::Alignment::Start,
direction: InlineDirection::Ltr,
writing_mode: LayoutWritingMode::HorizontalTb,
unicode_bidi: InlineUnicodeBidi::Normal,
vertical_align: InlineVerticalAlign::default(),
text_projection_deferred: false,
overflow_clips,
overflow_x,
overflow_y,
scrollbar_width: LayoutScrollbarWidth::Auto,
scrollbar_gutter: LayoutScrollbarGutter::Auto,
scrollbar_colors: None,
revealed_scrollbar_x: false,
revealed_scrollbar_y: false,
out_of_flow,
position,
sticky_inset,
establishes_transform_containing_block: false,
synthetic_transform: None,
visible: true,
pointer_events: true,
order: 0,
intrinsic_sizing_deferred: false,
grid_template_mode_deferred: false,
explicit_z_index: None,
opacity: 1.0,
blend_mode: PaintBlendMode::Normal,
has_filter_effect: false,
has_clip_path: false,
has_mask: false,
isolation: false,
layout_containment: false,
paint_containment: false,
will_change_containment: false,
will_change_position: false,
will_change_stacking_context: false,
list_marker_type: LayoutListMarkerType::Disc,
list_marker_position: LayoutListMarkerPosition::Outside,
}
}
/// Adds generated string content to a synthetic pseudo style.
pub fn with_generated_text(mut self, text: impl Into<Arc<str>>) -> Self {
self.generated_content = GeneratedContent::Items {
text: text.into(),
has_unsupported_items: false,
};
self
}
/// Models the initial `content: normal` value in construction tests.
pub fn with_normal_generated_content(mut self) -> Self {
self.generated_content = GeneratedContent::Normal;
self
}
/// Models a legal generated-content item that Phase 1 cannot materialize yet.
pub fn with_unsupported_generated_content(mut self) -> Self {
self.generated_content = GeneratedContent::Items {
text: Arc::from(""),
has_unsupported_items: true,
};
self
}
/// Overrides deterministic text metrics used before Parley lands in P3.
pub fn with_text_metrics(mut self, font_size: f32, line_height: f32) -> Self {
self.font_size = font_size;
self.line_height = line_height;
self.include_used_font_metrics = false;
self
}
/// Supplies the CSS scrollbar properties not yet exposed through Stylo's
/// typed standalone embedding API.
pub fn set_scrollbar_style(
&mut self,
width: LayoutScrollbarWidth,
gutter: LayoutScrollbarGutter,
colors: Option<LayoutScrollbarColors>,
) {
self.scrollbar_width = width;
self.scrollbar_gutter = gutter;
self.scrollbar_colors = colors;
}
/// Overrides the alignment keyword for a synthetic inline style.
pub fn with_inline_alignment(mut self, alignment: LayoutInlineAlignment) -> Self {
self.vertical_align.kind = alignment;
self
}
/// Overrides the CSS-pixel baseline shift for a synthetic inline style.
/// Positive values raise the inline box.
pub fn with_inline_baseline_shift(mut self, shift: f32) -> Self {
self.vertical_align.baseline_shift = shift;
self
}
/// Marks a synthetic box as removed from normal flow.
pub fn with_out_of_flow(mut self) -> Self {
self.out_of_flow = true;
self.position = LayoutPosition::Absolute;
self.taffy.position = TaffyPosition::Absolute;
self
}
/// Sets float/clear for deterministic BFC and IFC tests.
pub fn with_float(mut self, float: taffy::Float, clear: taffy::Clear) -> Self {
self.taffy.float = float;
self.taffy.clear = clear;
self.out_of_flow = float != taffy::Float::None;
self
}
/// Sets an exact CSS positioning mode for deterministic layout tests.
pub fn with_position(mut self, position: LayoutPosition) -> Self {
self.out_of_flow = matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed);
self.position = position;
self.sticky_inset = self.taffy.inset;
self.taffy.position = if self.out_of_flow {
TaffyPosition::Absolute
} else {
TaffyPosition::Relative
};
if matches!(position, LayoutPosition::Static | LayoutPosition::Sticky) {
self.taffy.inset = taffy::Rect {
left: taffy::LengthPercentageAuto::auto(),
right: taffy::LengthPercentageAuto::auto(),
top: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
};
}
self
}
/// Sets the CSS order used for flex/grid layout and paint ordering.
pub fn with_order(mut self, order: i32) -> Self {
self.order = order;
self
}
/// Overrides marker type/position for DOM-free list geometry tests.
pub fn with_list_marker(
mut self,
marker_type: LayoutListMarkerType,
position: LayoutListMarkerPosition,
) -> Self {
self.list_marker_type = marker_type;
self.list_marker_position = position;
self
}
/// Marks a synthetic box as an absolute/fixed containing block created by
/// transform/perspective without applying transform paint.
pub fn with_transform_containing_block(mut self) -> Self {
self.establishes_transform_containing_block = true;
self
}
/// Applies an exact pass-local 2D transform in DOM-free geometry tests.
pub fn with_2d_transform(mut self, transform: LayoutTransform2D) -> Self {
self.establishes_transform_containing_block = true;
self.synthetic_transform = Some(transform);
self
}
/// Applies an exact group opacity in DOM-free paint-order tests.
pub fn with_opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity.clamp(0.0, 1.0);
self
}
/// Applies an exact blend mode in DOM-free paint-order tests.
pub fn with_blend_mode(mut self, blend_mode: PaintBlendMode) -> Self {
self.blend_mode = blend_mode;
self
}
/// Applies a non-auto z-index in DOM-free stacking tests.
pub fn with_z_index(mut self, z_index: i32) -> Self {
self.explicit_z_index = Some(z_index);
self
}
pub fn display(&self) -> LayoutDisplay {
self.display
}
/// Whether the box would have been inline-level before absolute/fixed
/// positioning blockified its computed `display` value.
///
/// CSS static-position rules use this hypothetical display. Stylo retains
/// it as `original_display`, exactly as Blitz and Chromium retain the
/// pre-blockification value for out-of-flow layout.
pub(crate) fn hypothetical_display_is_inline_level(&self) -> bool {
self.computed.as_ref().map_or_else(
|| self.display.is_inline_level(),
|computed| computed.get_box().original_display.outside() == DisplayOutside::Inline,
)
}
/// Returns the retained Stylo allocation backing this pass-local style.
///
/// Renderer-owned anonymous box construction uses the parent computed
/// values as the inheritance input to `Stylist::style_for_anonymous`.
/// Synthetic construction tests deliberately return `None` here.
pub fn stylo_computed_values(&self) -> Option<&ServoArc<ComputedValues>> {
self.computed.as_ref()
}
pub fn background_color(&self) -> PaintColor {
self.background_color
}
pub(crate) fn border_colors(&self) -> PaintBorderColors {
self.border_colors
}
pub(crate) fn border_styles(&self) -> PaintBorderStyles {
self.computed.as_ref().map_or_else(
|| PaintBorderStyles::all(PaintBorderStyle::Solid),
|computed| {
let border = computed.get_border();
PaintBorderStyles {
top: paint_border_style(border.border_top_style),
right: paint_border_style(border.border_right_style),
bottom: paint_border_style(border.border_bottom_style),
left: paint_border_style(border.border_left_style),
}
},
)
}
pub(crate) fn border_radii(&self, width: f32, height: f32) -> PaintCornerRadii {
let Some(computed) = self.computed.as_ref() else {
return PaintCornerRadii::ZERO;
};
let width = CSSPixelLength::new(width.max(0.0));
let height = CSSPixelLength::new(height.max(0.0));
let resolve = |radius: &style::values::computed::BorderCornerRadius| {
PaintCornerRadius::new(
radius.0.width.0.resolve(width).px().max(0.0),
radius.0.height.0.resolve(height).px().max(0.0),
)
};
let border = computed.get_border();
PaintCornerRadii {
top_left: resolve(&border.border_top_left_radius),
top_right: resolve(&border.border_top_right_radius),
bottom_right: resolve(&border.border_bottom_right_radius),
bottom_left: resolve(&border.border_bottom_left_radius),
}
}
pub(crate) fn box_shadows(
&self,
rect: LayoutRect,
radii: PaintCornerRadii,
transform: LayoutTransform2D,
) -> Vec<PaintBoxShadow> {
let Some(computed) = self.computed.as_ref() else {
return Vec::new();
};
let current_color = computed.clone_color();
computed
.get_effects()
.box_shadow
.0
.iter()
.map(|shadow| PaintBoxShadow {
rect,
radii,
color: absolute_paint_color(shadow.base.color.resolve_to_absolute(&current_color)),
offset: LayoutPoint::new(shadow.base.horizontal.px(), shadow.base.vertical.px()),
blur_radius: shadow.base.blur.px().max(0.0),
spread_radius: shadow.spread.px(),
inset: shadow.inset,
transform,
})
.collect()
}
pub(crate) fn outline_fragment(
&self,
rect: LayoutRect,
radii: PaintCornerRadii,
transform: LayoutTransform2D,
) -> Option<PaintFragment> {
let computed = self.computed.as_ref()?;
let outline = computed.get_outline();
let style = match outline.outline_style {
StyloOutlineStyle::Auto => PaintBorderStyle::Solid,
StyloOutlineStyle::BorderStyle(style) => paint_border_style(style),
};
if matches!(style, PaintBorderStyle::None | PaintBorderStyle::Hidden) {
return None;
}
let width = outline.outline_width.0.to_f32_px().max(0.0);
if width <= 0.0 {
return None;
}
let offset = outline.outline_offset.to_f32_px();
let outset = offset + width;
let outline_rect = LayoutRect::new(
rect.x - outset,
rect.y - outset,
(rect.width + outset * 2.0).max(0.0),
(rect.height + outset * 2.0).max(0.0),
);
let outset_radius = |radius: PaintCornerRadius| {
PaintCornerRadius::new((radius.x + outset).max(0.0), (radius.y + outset).max(0.0))
};
let radii = PaintCornerRadii {
top_left: outset_radius(radii.top_left),
top_right: outset_radius(radii.top_right),
bottom_right: outset_radius(radii.bottom_right),
bottom_left: outset_radius(radii.bottom_left),
};
let color = absolute_paint_color(
outline
.outline_color
.resolve_to_absolute(&computed.clone_color()),
);
Some(PaintFragment::Border {
rect: outline_rect,
widths: PaintEdgeSizes::new(width, width, width, width),
colors: PaintBorderColors::all(color),
styles: PaintBorderStyles::all(style),
radii,
transform,
})
}
pub fn generated_text(&self) -> Option<&str> {
match &self.generated_content {
GeneratedContent::Items { text, .. } => Some(text),
GeneratedContent::Normal | GeneratedContent::None => None,
}
}
pub(crate) fn generates_pseudo_box(&self, marker: bool) -> bool {
match self.generated_content {
GeneratedContent::Normal => marker,
GeneratedContent::None => false,
GeneratedContent::Items { .. } => true,
}
}
pub(crate) fn has_unsupported_generated_content(&self) -> bool {
matches!(
self.generated_content,
GeneratedContent::Items {
has_unsupported_items: true,
..
}
)
}
pub fn is_out_of_flow(&self) -> bool {
self.out_of_flow
}
pub fn position(&self) -> LayoutPosition {
self.position
}
pub(crate) fn establishes_positioned_containing_block(
&self,
is_css_box: bool,
containment_eligible: bool,
) -> bool {
self.position.is_positioned()
|| self.will_change_position
|| self.establishes_fixed_containing_block(is_css_box, containment_eligible)
}
pub(crate) fn establishes_fixed_containing_block(
&self,
is_css_box: bool,
containment_eligible: bool,
) -> bool {
(is_css_box && self.establishes_transform_containing_block)
|| (containment_eligible
&& (self.layout_containment
|| self.paint_containment
|| self.will_change_containment))
}
pub(crate) const fn applies_paint_containment(&self) -> bool {
self.paint_containment
}
pub(crate) const fn is_visible(&self) -> bool {
self.visible
}
/// Returns the computed CSS `color` sampled for this pass.
///
/// Besides text paint this is the inherited `currentColor` input for
/// atomic resources such as an inline SVG replaced element.
pub const fn current_color(&self) -> PaintColor {
self.text_color
}
pub(crate) const fn text_color(&self) -> PaintColor {
self.current_color()
}
pub(crate) const fn accepts_pointer_events(&self) -> bool {
self.pointer_events
}
pub(crate) fn resolved_2d_transform(&self, width: f32, height: f32) -> ResolvedLayoutTransform {
if let Some(transform) = self.synthetic_transform {
return ResolvedLayoutTransform {
transform,
has_unsupported_3d: false,
establishes_property_space: true,
};
}
let mut resolved = self
.computed
.as_ref()
.map_or(ResolvedLayoutTransform::IDENTITY, |computed| {
resolve_stylo_2d_transform(computed.get_box(), width, height)
});
resolved.establishes_property_space = self.establishes_transform_containing_block;
resolved
}
pub(crate) fn is_absolute_positioned(&self) -> bool {
self.position.is_absolute()
}
pub(crate) fn is_fixed_positioned(&self) -> bool {
self.position.is_fixed()
}
pub(crate) const fn order(&self) -> i32 {
self.order
}
pub(crate) const fn explicit_z_index(&self) -> Option<i32> {
self.explicit_z_index
}
pub(crate) const fn opacity(&self) -> f32 {
self.opacity
}
pub(crate) const fn blend_mode(&self) -> PaintBlendMode {
self.blend_mode
}
pub(crate) fn creates_stacking_context(
&self,
is_root: bool,
is_flex_or_grid_item: bool,
containment_eligible: bool,
) -> bool {
if is_root
|| self.opacity < 1.0
|| self.blend_mode != PaintBlendMode::Normal
|| self.establishes_transform_containing_block
|| self.has_filter_effect
|| self.has_clip_path
|| self.has_mask
|| self.isolation
|| self.will_change_stacking_context
|| (containment_eligible && (self.layout_containment || self.paint_containment))
{
return true;
}
match self.position {
LayoutPosition::Fixed | LayoutPosition::Sticky => true,
LayoutPosition::Relative | LayoutPosition::Absolute => self.explicit_z_index.is_some(),
LayoutPosition::Static => is_flex_or_grid_item && self.explicit_z_index.is_some(),
}
}
pub(crate) fn list_marker_type(&self) -> &LayoutListMarkerType {
&self.list_marker_type
}
pub(crate) const fn list_marker_position(&self) -> LayoutListMarkerPosition {
self.list_marker_position
}
pub(crate) fn table_layout_is_fixed(&self) -> bool {
self.computed.as_ref().is_some_and(|computed| {
computed.clone_table_layout() == style::computed_values::table_layout::T::Fixed
})
}
pub(crate) fn table_border_is_collapsed(&self) -> bool {
self.computed.as_ref().is_some_and(|computed| {
computed.clone_border_collapse() == style::computed_values::border_collapse::T::Collapse
})
}
pub(crate) fn table_border_spacing(&self) -> Size<f32> {
self.computed.as_ref().map_or(Size::ZERO, |computed| {
let spacing = computed.clone_border_spacing().0;
Size {
width: spacing.width.px(),
height: spacing.height.px(),
}
})
}
pub(crate) fn caption_is_bottom(&self) -> bool {
self.computed.as_ref().is_some_and(|computed| {
computed.clone_caption_side() == style::values::computed::table::CaptionSide::Bottom
})
}
pub(crate) fn is_floated(&self) -> bool {
self.taffy.float != taffy::Float::None
}
pub(crate) fn has_deferred_intrinsic_sizing(&self) -> bool {
self.intrinsic_sizing_deferred
}
pub(crate) fn has_deferred_grid_template_mode(&self) -> bool {
self.grid_template_mode_deferred
}
pub(crate) fn has_auto_inset_axis(&self) -> bool {
(self.taffy.inset.left.is_auto() && self.taffy.inset.right.is_auto())
|| (self.taffy.inset.top.is_auto() && self.taffy.inset.bottom.is_auto())
}
/// Creates a zero-sized, non-painted absolute placeholder that asks the
/// original block formatting context to compute a positioned descendant's
/// hypothetical static position. The real box remains a child of its CSS
/// containing block in the numeric tree.
pub(crate) fn positioned_static_placeholder(&self) -> Self {
let mut placeholder = self.clone();
let zero_dimension = taffy::Dimension::length(0.0);
let zero_length = taffy::LengthPercentage::length(0.0);
placeholder.display = LayoutDisplay::Block;
placeholder.taffy.display = TaffyDisplay::Block;
placeholder.taffy.size = Size {
width: zero_dimension,
height: zero_dimension,
};
placeholder.taffy.min_size = Size {
width: zero_dimension,
height: zero_dimension,
};
placeholder.taffy.max_size = Size {
width: zero_dimension,
height: zero_dimension,
};
placeholder.taffy.padding = taffy::Rect {
left: zero_length,
right: zero_length,
top: zero_length,
bottom: zero_length,
};
placeholder.taffy.border = taffy::Rect {
left: zero_length,
right: zero_length,
top: zero_length,
bottom: zero_length,
};
placeholder.taffy.aspect_ratio = None;
placeholder.preferred_aspect_ratio = PreferredAspectRatio::Auto;
placeholder.background_color = PaintColor::TRANSPARENT;
placeholder.border_colors = PaintBorderColors::all(PaintColor::TRANSPARENT);
placeholder.generated_content = GeneratedContent::None;
placeholder.establishes_transform_containing_block = false;
placeholder.synthetic_transform = None;
// This is an internal static-position probe, not the authored CSS
// principal box. Do not let copied containment or `will-change`
// metadata turn it into a containing block, stacking context, or
// descendant clip in the later projection passes.
placeholder.layout_containment = false;
placeholder.paint_containment = false;
placeholder.will_change_containment = false;
placeholder.will_change_position = false;
placeholder.will_change_stacking_context = false;
placeholder.visible = false;
placeholder.pointer_events = false;
placeholder.explicit_z_index = None;
placeholder
}
pub(crate) const fn sticky_inset(&self) -> taffy::Rect<taffy::LengthPercentageAuto> {
self.sticky_inset
}
/// Applies an HTML-level `display: none` override such as the `hidden`
/// attribute after Stylo has produced the underlying computed values.
pub fn force_display_none(&mut self) {
self.display = LayoutDisplay::None;
self.taffy.display = TaffyDisplay::None;
}
/// Assigns the structural display selected by box construction.
///
/// Stylo's anonymous-box pseudos provide inherited and UA values, while
/// the builder owns the exact anonymous role (for example row-group,
/// flex text item, or grid text item). Keep those two responsibilities
/// separate instead of manufacturing a retained computed style.
pub fn force_layout_display(&mut self, display: LayoutDisplay) {
self.display = display;
self.taffy.display = taffy_display(display);
}
/// Returns the computed CSS font size sampled for this pass.
///
/// Atomic document resources such as inline SVG use this as the inherited
/// context for relative lengths without retaining the Stylo allocation.
pub const fn font_size(&self) -> f32 {
self.font_size
}
pub(crate) fn line_height(&self) -> f32 {
self.line_height
}
pub(crate) fn includes_used_font_metrics(&self) -> bool {
self.include_used_font_metrics
}
pub(crate) fn white_space_collapse(&self) -> InlineWhiteSpaceCollapse {
self.white_space_collapse
}
pub(crate) fn text_transform(&self) -> InlineTextTransform {
self.text_transform
}
pub(crate) fn text_align(&self) -> parley::Alignment {
self.text_align
}
pub(crate) fn direction(&self) -> InlineDirection {
self.direction
}
pub(crate) fn unicode_bidi(&self) -> InlineUnicodeBidi {
self.unicode_bidi
}
pub(crate) fn vertical_align(&self) -> InlineVerticalAlign {
self.vertical_align
}
pub(crate) fn parley_text_style(
&self,
) -> parley::TextStyle<'static, 'static, crate::stylo_to_parley::TextBrush> {
if let Some(computed) = self.computed.as_ref() {
return crate::stylo_to_parley::text_style(computed);
}
parley::TextStyle {
font_size: self.font_size,
line_height: parley::LineHeight::Absolute(self.line_height),
brush: crate::stylo_to_parley::TextBrush {
color: self.text_color,
paint: true,
synthetic_bold: false,
decoration: crate::stylo_to_parley::TextDecorationBrush::default(),
shadows: std::sync::Arc::from([]),
},
..parley::TextStyle::default()
}
}
pub(crate) fn text_indent(&self, containing_width: f32) -> (f32, parley::IndentOptions) {
let Some(computed) = self.computed.as_ref() else {
return (0.0, parley::IndentOptions::default());
};
let indent = computed.clone_text_indent();
(
indent
.length
.resolve(CSSPixelLength::new(containing_width.max(0.0)))
.px(),
parley::IndentOptions {
each_line: indent.each_line,
hanging: indent.hanging,
},
)
}
pub(crate) fn has_deferred_text_projection(&self) -> bool {
self.text_projection_deferred
}
pub(crate) fn clips_overflow(&self) -> bool {
self.overflow_clips
}
pub(crate) fn establishes_scroll_container(&self) -> bool {
// `overflow: clip` clips paint but deliberately does not create the
// scrolling mechanism that selects a sticky scrollport.
[self.overflow_x, self.overflow_y]
.into_iter()
.any(LayoutOverflowMode::creates_scroll_container)
}
pub(crate) fn allows_user_scroll_x(&self) -> bool {
self.overflow_x.allows_user_scroll()
}
pub(crate) fn allows_user_scroll_y(&self) -> bool {
self.overflow_y.allows_user_scroll()
}
pub(crate) const fn overflow_modes(&self) -> [LayoutOverflowMode; 2] {
[self.overflow_x, self.overflow_y]
}
pub(crate) fn overflow_is_visible_along_both_axes(&self) -> bool {
self.overflow_modes()
.into_iter()
.all(|mode| mode == LayoutOverflowMode::Visible)
}
pub(crate) fn applies_any_viewport_containment(&self) -> bool {
let Some(computed) = self.computed.as_ref() else {
return false;
};
!computed.clone_contain().is_empty()
|| computed.clone_container_type().is_size_container_type()
|| computed.clone_content_visibility() != StyloContentVisibility::Visible
}
pub(crate) const fn viewport_scrollbar_width(&self) -> LayoutScrollbarWidth {
self.scrollbar_width
}
pub(crate) const fn viewport_scrollbar_gutter(&self) -> LayoutScrollbarGutter {
self.scrollbar_gutter
}
/// Keeps the document element's numeric root formatting context while its
/// scrollbar policy is represented by the synthetic viewport. Using
/// `Visible` here would let the body's block margins collapse through the
/// root even though the root box establishes an independent formatting
/// context.
pub(crate) fn prepare_viewport_root_layout(&mut self) {
self.revealed_scrollbar_x = false;
self.revealed_scrollbar_y = false;
self.taffy.scrollbar_width = 0.0;
self.taffy.overflow.x = taffy::Overflow::Hidden;
self.taffy.overflow.y = taffy::Overflow::Hidden;
}
/// Removes the numeric effect of overflow transferred from the defining
/// body to the viewport without changing that body's authored style.
pub(crate) fn prepare_viewport_defining_body_layout(&mut self) {
self.revealed_scrollbar_x = false;
self.revealed_scrollbar_y = false;
self.taffy.scrollbar_width = 0.0;
self.taffy.overflow.x = taffy::Overflow::Visible;
self.taffy.overflow.y = taffy::Overflow::Visible;
}
pub(crate) fn prepare_scrollbar_layout(&mut self, is_root: bool) {
self.revealed_scrollbar_x = self.overflow_x == LayoutOverflowMode::Scroll;
self.revealed_scrollbar_y = self.overflow_y == LayoutOverflowMode::Scroll;
// Moli resolves CSS scrollbar policy to physical edge insets through
// `LayoutPartialTree::get_scrollbar_insets`. Keep Taffy's legacy
// scalar disabled so no algorithm reserves the same gutter twice.
self.taffy.scrollbar_width = 0.0;
self.taffy.overflow.x =
self.initial_taffy_overflow(LayoutScrollbarAxis::Horizontal, is_root);
self.taffy.overflow.y = self.initial_taffy_overflow(LayoutScrollbarAxis::Vertical, is_root);
if !is_root
&& self.scrollbar_gutter != LayoutScrollbarGutter::Auto
&& self.scrollbar_width != LayoutScrollbarWidth::None
{
if self.uses_horizontal_writing_mode() && self.overflow_y.creates_scroll_container() {
self.taffy.overflow.y = taffy::Overflow::Scroll;
} else if !self.uses_horizontal_writing_mode()
&& self.overflow_x.creates_scroll_container()
{
self.taffy.overflow.x = taffy::Overflow::Scroll;
}
}
}
fn initial_taffy_overflow(&self, axis: LayoutScrollbarAxis, is_root: bool) -> taffy::Overflow {
let mode = self.overflow_mode(axis);
// The root scrollbar belongs to the initial containing block. Its
// gutter is reserved on Moli's synthetic viewport node so percentage
// sizing and fixed positioning see the reduced layout viewport without
// making the root element reserve the same gutter a second time.
if is_root {
return match mode {
LayoutOverflowMode::Clip => taffy::Overflow::Clip,
LayoutOverflowMode::Visible
| LayoutOverflowMode::Hidden
| LayoutOverflowMode::Auto
| LayoutOverflowMode::Scroll => taffy::Overflow::Hidden,
};
}
match mode {
LayoutOverflowMode::Visible => taffy::Overflow::Visible,
LayoutOverflowMode::Clip => taffy::Overflow::Clip,
LayoutOverflowMode::Hidden | LayoutOverflowMode::Auto => taffy::Overflow::Hidden,
LayoutOverflowMode::Scroll => taffy::Overflow::Scroll,
}
}
pub(crate) fn reveal_auto_scrollbar(
&mut self,
axis: LayoutScrollbarAxis,
is_root: bool,
overflowing: bool,
) -> bool {
if !overflowing || self.scrollbar_width == LayoutScrollbarWidth::None {
return false;
}
let mode = self.overflow_mode(axis);
if !(matches!(mode, LayoutOverflowMode::Auto)
|| is_root && matches!(mode, LayoutOverflowMode::Visible))
{
return false;
}
let revealed = match axis {
LayoutScrollbarAxis::Horizontal => &mut self.revealed_scrollbar_x,
LayoutScrollbarAxis::Vertical => &mut self.revealed_scrollbar_y,
};
if *revealed {
return false;
}
*revealed = true;
if !is_root {
match axis {
LayoutScrollbarAxis::Horizontal => {
self.taffy.overflow.x = taffy::Overflow::Scroll;
}
LayoutScrollbarAxis::Vertical => {
self.taffy.overflow.y = taffy::Overflow::Scroll;
}
}
}
true
}
pub(crate) fn has_scrollbar(
&self,
axis: LayoutScrollbarAxis,
is_root: bool,
overflowing: bool,
) -> bool {
if self.scrollbar_width == LayoutScrollbarWidth::None {
return false;
}
match self.overflow_mode(axis) {
LayoutOverflowMode::Scroll => true,
LayoutOverflowMode::Auto => overflowing,
LayoutOverflowMode::Visible if is_root => overflowing,
LayoutOverflowMode::Visible | LayoutOverflowMode::Clip | LayoutOverflowMode::Hidden => {
false
}
}
}
pub(crate) fn overflow_mode(&self, axis: LayoutScrollbarAxis) -> LayoutOverflowMode {
match axis {
LayoutScrollbarAxis::Horizontal => self.overflow_x,
LayoutScrollbarAxis::Vertical => self.overflow_y,
}
}
pub(crate) const fn scrollbar_colors(&self) -> Option<LayoutScrollbarColors> {
self.scrollbar_colors
}
pub(crate) fn scrollbar_control_thickness(&self) -> f32 {
self.scrollbar_width.thickness()
}
pub(crate) fn scrollbar_gutter_thickness(&self, axis: LayoutScrollbarAxis) -> f32 {
let insets = self.scrollbar_layout_insets(false);
match axis {
LayoutScrollbarAxis::Horizontal => insets.top + insets.bottom,
LayoutScrollbarAxis::Vertical => insets.left + insets.right,
}
}
/// Physical gutter before the scrollport. Classic vertical scrollbars are
/// on the left in RTL, while `both-edges` adds the matching left gutter in
/// LTR as well.
pub(crate) fn scrollbar_leading_gutter_thickness(
&self,
axis: LayoutScrollbarAxis,
is_root: bool,
) -> f32 {
let insets = self.scrollbar_layout_insets(is_root);
match axis {
LayoutScrollbarAxis::Horizontal => insets.top,
LayoutScrollbarAxis::Vertical => insets.left,
}
}
/// Physical gutter contract consumed by Taffy's numeric algorithms.
///
/// CSS relation, writing mode, direction and dynamic scrollbar feedback
/// are resolved here once; Taffy deliberately sees only four edge sizes.
pub(crate) fn scrollbar_layout_insets(&self, is_root: bool) -> taffy::Rect<f32> {
if self.scrollbar_width == LayoutScrollbarWidth::None {
return taffy::Rect::ZERO;
}
let thickness = self.scrollbar_width.thickness();
let mut insets = taffy::Rect::ZERO;
if self.revealed_scrollbar_x {
insets.bottom = thickness;
}
if self.revealed_scrollbar_y {
if self.places_vertical_scrollbar_on_left(is_root) {
insets.left = thickness;
} else {
insets.right = thickness;
}
}
if self.scrollbar_gutter != LayoutScrollbarGutter::Auto {
if self.uses_horizontal_writing_mode() && self.overflow_y.creates_scroll_container() {
if self.places_vertical_scrollbar_on_left(is_root) {
insets.left = thickness;
if self.scrollbar_gutter == LayoutScrollbarGutter::StableBothEdges {
insets.right = thickness;
}
} else {
insets.right = thickness;
if self.scrollbar_gutter == LayoutScrollbarGutter::StableBothEdges {
insets.left = thickness;
}
}
} else if !self.uses_horizontal_writing_mode()
&& self.overflow_x.creates_scroll_container()
{
insets.bottom = thickness;
if self.scrollbar_gutter == LayoutScrollbarGutter::StableBothEdges {
insets.top = thickness;
}
}
}
insets
}
pub(crate) fn places_vertical_scrollbar_on_left(&self, is_root: bool) -> bool {
!is_root && self.uses_horizontal_writing_mode() && self.direction == InlineDirection::Rtl
}
pub(crate) const fn uses_horizontal_writing_mode(&self) -> bool {
self.writing_mode.is_horizontal()
}
pub(crate) const fn vertical_block_start_is_right(&self) -> Option<bool> {
self.writing_mode.vertical_block_start_is_right()
}
pub(crate) fn text_leaf_from(parent: &Self) -> Self {
Self {
computed: parent.computed.clone(),
taffy: Style {
display: TaffyDisplay::Block,
..Style::default()
},
preferred_aspect_ratio: PreferredAspectRatio::Auto,
display: LayoutDisplay::Inline,
background_color: PaintColor::TRANSPARENT,
border_colors: PaintBorderColors::default(),
generated_content: GeneratedContent::None,
font_size: parent.font_size,
line_height: parent.line_height,
include_used_font_metrics: parent.include_used_font_metrics,
text_color: parent.text_color,
white_space_collapse: parent.white_space_collapse,
text_transform: parent.text_transform,
text_align: parent.text_align,
direction: parent.direction,
writing_mode: parent.writing_mode,
unicode_bidi: InlineUnicodeBidi::Normal,
vertical_align: parent.vertical_align,
text_projection_deferred: parent.text_projection_deferred,
overflow_clips: false,
overflow_x: LayoutOverflowMode::Visible,
overflow_y: LayoutOverflowMode::Visible,
scrollbar_width: parent.scrollbar_width,
scrollbar_gutter: LayoutScrollbarGutter::Auto,
scrollbar_colors: parent.scrollbar_colors,
revealed_scrollbar_x: false,
revealed_scrollbar_y: false,
out_of_flow: false,
position: LayoutPosition::Static,
sticky_inset: taffy::Rect {
left: taffy::LengthPercentageAuto::auto(),
right: taffy::LengthPercentageAuto::auto(),
top: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
},
establishes_transform_containing_block: false,
synthetic_transform: None,
visible: parent.visible,
pointer_events: parent.pointer_events,
order: 0,
intrinsic_sizing_deferred: false,
grid_template_mode_deferred: false,
explicit_z_index: None,
opacity: 1.0,
blend_mode: PaintBlendMode::Normal,
has_filter_effect: false,
has_clip_path: false,
has_mask: false,
isolation: false,
layout_containment: false,
paint_containment: false,
will_change_containment: false,
will_change_position: false,
will_change_stacking_context: false,
list_marker_type: parent.list_marker_type.clone(),
list_marker_position: parent.list_marker_position,
}
}
/// Derives a deterministic anonymous style when a resolver has no retained
/// Stylo allocation (primarily DOM-free tests and conservative fallback).
pub fn anonymous_from(parent: &Self, display: LayoutDisplay) -> Self {
Self {
computed: parent.computed.clone(),
taffy: Style {
display: taffy_display(display),
..Style::default()
},
preferred_aspect_ratio: PreferredAspectRatio::Auto,
display,
background_color: PaintColor::TRANSPARENT,
border_colors: PaintBorderColors::default(),
generated_content: GeneratedContent::None,
font_size: parent.font_size,
line_height: parent.line_height,
include_used_font_metrics: parent.include_used_font_metrics,
text_color: parent.text_color,
white_space_collapse: parent.white_space_collapse,
text_transform: parent.text_transform,
text_align: parent.text_align,
direction: parent.direction,
writing_mode: parent.writing_mode,
unicode_bidi: InlineUnicodeBidi::Normal,
vertical_align: parent.vertical_align,
text_projection_deferred: parent.text_projection_deferred,
overflow_clips: false,
overflow_x: LayoutOverflowMode::Visible,
overflow_y: LayoutOverflowMode::Visible,
scrollbar_width: parent.scrollbar_width,
scrollbar_gutter: LayoutScrollbarGutter::Auto,
scrollbar_colors: parent.scrollbar_colors,
revealed_scrollbar_x: false,
revealed_scrollbar_y: false,
out_of_flow: false,
position: LayoutPosition::Static,
sticky_inset: taffy::Rect {
left: taffy::LengthPercentageAuto::auto(),
right: taffy::LengthPercentageAuto::auto(),
top: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
},
establishes_transform_containing_block: false,
synthetic_transform: None,
visible: parent.visible,
pointer_events: parent.pointer_events,
order: 0,
intrinsic_sizing_deferred: false,
grid_template_mode_deferred: false,
explicit_z_index: None,
opacity: 1.0,
blend_mode: PaintBlendMode::Normal,
has_filter_effect: false,
has_clip_path: false,
has_mask: false,
isolation: false,
layout_containment: false,
paint_containment: false,
will_change_containment: false,
will_change_position: false,
will_change_stacking_context: false,
list_marker_type: parent.list_marker_type.clone(),
list_marker_position: parent.list_marker_position,
}
}
pub(crate) fn blockify_for_item(&mut self) {
if self.display.is_inline_level() {
self.taffy.display = match self.display {
LayoutDisplay::InlineBlock => TaffyDisplay::FlowRoot,
LayoutDisplay::InlineFlex => TaffyDisplay::Flex,
LayoutDisplay::InlineGrid => TaffyDisplay::Grid,
_ => TaffyDisplay::Block,
};
}
}
pub(crate) fn mark_replaced(&mut self, inherent_ratio: Option<f32>) {
self.taffy.item_is_replaced = true;
self.taffy.aspect_ratio = self
.preferred_aspect_ratio
.resolve_for_replaced(inherent_ratio, self.taffy.box_sizing)
.ratio;
}
pub(crate) fn resolved_replaced_aspect_ratio(
&self,
inherent_ratio: Option<f32>,
) -> ResolvedAspectRatio {
self.preferred_aspect_ratio
.resolve_for_replaced(inherent_ratio, self.taffy.box_sizing)
}
pub(crate) fn mark_intrinsic_form_control_container(&mut self) {
// A button retains and lays out its real DOM children, but its
// block-level outer display still uses the same non-stretch intrinsic
// width exception as other native controls.
self.taffy.item_is_table = true;
// Blink's UA sheet gives <button> a private safe block-content center
// alignment. Taffy's block algorithm already implements the standard
// equivalent. Only flow buttons use that algorithm; author flex/grid
// containers retain their own align-content behavior.
if matches!(
self.display,
LayoutDisplay::Block
| LayoutDisplay::FlowRoot
| LayoutDisplay::Inline
| LayoutDisplay::InlineBlock
) {
self.taffy.align_content = Some(taffy::AlignContent::SAFE_CENTER);
}
}
}
fn usable_aspect_ratio(ratio: Option<f32>) -> Option<f32> {
ratio.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
}
fn taffy_size_dimension(
size: &GenericSize<style::values::computed::NonNegativeLengthPercentage>,
fallback: taffy::Dimension,
) -> taffy::Dimension {
match size {
GenericSize::MinContent => taffy::Dimension::min_content(),
GenericSize::MaxContent => taffy::Dimension::max_content(),
GenericSize::FitContent => taffy::Dimension::fit_content(),
GenericSize::Stretch | GenericSize::WebkitFillAvailable => taffy::Dimension::stretch(),
_ => fallback,
}
}
fn taffy_max_size_dimension(
size: &GenericMaxSize<style::values::computed::NonNegativeLengthPercentage>,
fallback: taffy::Dimension,
) -> taffy::Dimension {
match size {
GenericMaxSize::MinContent => taffy::Dimension::min_content(),
GenericMaxSize::MaxContent => taffy::Dimension::max_content(),
GenericMaxSize::FitContent => taffy::Dimension::fit_content(),
GenericMaxSize::Stretch | GenericMaxSize::WebkitFillAvailable => {
taffy::Dimension::stretch()
}
_ => fallback,
}
}
fn taffy_display(display: LayoutDisplay) -> TaffyDisplay {
match display {
LayoutDisplay::None => TaffyDisplay::None,
LayoutDisplay::FlowRoot | LayoutDisplay::InlineBlock => TaffyDisplay::FlowRoot,
LayoutDisplay::Flex | LayoutDisplay::InlineFlex => TaffyDisplay::Flex,
LayoutDisplay::Grid | LayoutDisplay::InlineGrid => TaffyDisplay::Grid,
LayoutDisplay::Contents
| LayoutDisplay::Block
| LayoutDisplay::Inline
| LayoutDisplay::BlockListItem
| LayoutDisplay::InlineListItem
| LayoutDisplay::Table
| LayoutDisplay::InlineTable
| LayoutDisplay::TableCaption
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
| LayoutDisplay::TableColumnGroup
| LayoutDisplay::TableColumn
| LayoutDisplay::TableRow
| LayoutDisplay::TableCell => TaffyDisplay::Block,
}
}
fn classify_display(computed: &ComputedValues) -> LayoutDisplay {
let display = computed.clone_display();
if display.is_none() {
LayoutDisplay::None
} else if display.is_contents() {
LayoutDisplay::Contents
} else {
let outside = display.outside();
let inside = display.inside();
if display.is_list_item() {
return if outside == DisplayOutside::Inline {
LayoutDisplay::InlineListItem
} else {
LayoutDisplay::BlockListItem
};
}
match inside {
DisplayInside::None | DisplayInside::Contents => LayoutDisplay::Block,
DisplayInside::Flow => match outside {
DisplayOutside::Inline => LayoutDisplay::Inline,
DisplayOutside::TableCaption => LayoutDisplay::TableCaption,
DisplayOutside::None | DisplayOutside::Block | DisplayOutside::InternalTable => {
LayoutDisplay::Block
}
},
DisplayInside::FlowRoot => {
if outside == DisplayOutside::Inline {
LayoutDisplay::InlineBlock
} else {
LayoutDisplay::FlowRoot
}
}
DisplayInside::Flex => {
if outside == DisplayOutside::Inline {
LayoutDisplay::InlineFlex
} else {
LayoutDisplay::Flex
}
}
DisplayInside::Grid => {
if outside == DisplayOutside::Inline {
LayoutDisplay::InlineGrid
} else {
LayoutDisplay::Grid
}
}
DisplayInside::Table => {
if outside == DisplayOutside::Inline {
LayoutDisplay::InlineTable
} else {
LayoutDisplay::Table
}
}
DisplayInside::TableRowGroup => LayoutDisplay::TableRowGroup,
DisplayInside::TableHeaderGroup => LayoutDisplay::TableHeaderGroup,
DisplayInside::TableFooterGroup => LayoutDisplay::TableFooterGroup,
DisplayInside::TableColumnGroup => LayoutDisplay::TableColumnGroup,
DisplayInside::TableColumn => LayoutDisplay::TableColumn,
DisplayInside::TableRow => LayoutDisplay::TableRow,
DisplayInside::TableCell => LayoutDisplay::TableCell,
}
}
}
fn stylo_background_color(computed: &ComputedValues) -> PaintColor {
let current_color = computed.clone_color();
let absolute = computed
.clone_background_color()
.resolve_to_absolute(&current_color)
.to_color_space(ColorSpace::Srgb);
let [red, green, blue, alpha] = *absolute.raw_components();
PaintColor::new(red, green, blue, alpha)
}
pub(crate) fn absolute_paint_color(color: style::color::AbsoluteColor) -> PaintColor {
let color = color.to_color_space(ColorSpace::Srgb);
let [red, green, blue, alpha] = *color.raw_components();
PaintColor::new(red, green, blue, alpha)
}
fn paint_border_style(style: StyloBorderStyle) -> PaintBorderStyle {
match style {
StyloBorderStyle::None => PaintBorderStyle::None,
StyloBorderStyle::Hidden => PaintBorderStyle::Hidden,
StyloBorderStyle::Solid => PaintBorderStyle::Solid,
StyloBorderStyle::Dotted => PaintBorderStyle::Dotted,
StyloBorderStyle::Dashed => PaintBorderStyle::Dashed,
StyloBorderStyle::Double => PaintBorderStyle::Double,
StyloBorderStyle::Groove => PaintBorderStyle::Groove,
StyloBorderStyle::Ridge => PaintBorderStyle::Ridge,
StyloBorderStyle::Inset => PaintBorderStyle::Inset,
StyloBorderStyle::Outset => PaintBorderStyle::Outset,
}
}
fn stylo_text_color(computed: &ComputedValues) -> PaintColor {
let absolute = computed.clone_color().to_color_space(ColorSpace::Srgb);
let [red, green, blue, alpha] = *absolute.raw_components();
PaintColor::new(red, green, blue, alpha)
}
fn stylo_border_colors(computed: &ComputedValues) -> PaintBorderColors {
let current_color = computed.clone_color();
let border = computed.get_border();
let [top, right, bottom, left] = [
(&border.border_top_color, border.border_top_style),
(&border.border_right_color, border.border_right_style),
(&border.border_bottom_color, border.border_bottom_style),
(&border.border_left_color, border.border_left_style),
]
.map(|(color, border_style)| {
if border_style.none_or_hidden() {
return PaintColor::TRANSPARENT;
}
let absolute = color
.resolve_to_absolute(&current_color)
.to_color_space(ColorSpace::Srgb);
let [red, green, blue, alpha] = *absolute.raw_components();
PaintColor::new(red, green, blue, alpha)
});
PaintBorderColors {
top,
right,
bottom,
left,
}
}
fn stylo_overflow_clips(computed: &ComputedValues) -> bool {
!matches!(computed.clone_overflow_x(), Overflow::Visible)
|| !matches!(computed.clone_overflow_y(), Overflow::Visible)
}
// Ported from Blitz `stylo_to_kurbo.rs` at d788124a. The output is kept in a
// layout-owned affine type so geometry/query consumers do not depend on a
// paint library. Three-dimensional operations are diagnosed and conservatively
// omitted until the 3D transform/paint phase rather than flattened silently.
fn resolve_stylo_2d_transform(
box_styles: &style::properties::generated::style_structs::Box,
width: f32,
height: f32,
) -> ResolvedLayoutTransform {
let reference_box = euclid::default::Rect::new(
euclid::default::Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
euclid::default::Size2D::new(
CSSPixelLength::new(width.max(0.0)),
CSSPixelLength::new(height.max(0.0)),
),
);
let mut has_unsupported_3d = matches!(box_styles.perspective, GenericPerspective::Length(_));
let translate = match &box_styles.translate {
Translate::None => None,
Translate::Translate(x, y, z) => {
has_unsupported_3d |= z.px() != 0.0;
Some(LayoutTransform2D::translation(
x.resolve(reference_box.width()).px(),
y.resolve(reference_box.height()).px(),
))
}
};
let rotate = match &box_styles.rotate {
Rotate::None => None,
Rotate::Rotate(angle) => Some(LayoutTransform2D::rotation(angle.radians64())),
Rotate::Rotate3D(x, y, z, angle) if *x == 0.0 && *y == 0.0 && *z != 0.0 => {
let radians = if *z < 0.0 {
-angle.radians64()
} else {
angle.radians64()
};
Some(LayoutTransform2D::rotation(radians))
}
Rotate::Rotate3D(..) => {
has_unsupported_3d = true;
None
}
};
let scale = match &box_styles.scale {
Scale::None => None,
Scale::Scale(x, y, z) => {
has_unsupported_3d |= *z != 1.0;
Some(LayoutTransform2D::scale(f64::from(*x), f64::from(*y)))
}
};
let transform = if box_styles.transform.0.is_empty() {
None
} else {
match box_styles
.transform
.to_transform_3d_matrix(Some(&reference_box))
{
Ok((_matrix, true)) => {
has_unsupported_3d = true;
None
}
Ok((matrix, false)) => Some(LayoutTransform2D::new([
f64::from(matrix.m11),
f64::from(matrix.m12),
f64::from(matrix.m21),
f64::from(matrix.m22),
f64::from(matrix.m41),
f64::from(matrix.m42),
])),
Err(_) => {
has_unsupported_3d = true;
None
}
}
};
let mut resolved = LayoutTransform2D::IDENTITY;
for transform in [translate, rotate, scale, transform].into_iter().flatten() {
resolved = resolved.concatenate(transform);
}
if resolved != LayoutTransform2D::IDENTITY {
let origin = &box_styles.transform_origin;
let origin_x = origin.horizontal.resolve(reference_box.width()).px();
let origin_y = origin.vertical.resolve(reference_box.height()).px();
resolved = LayoutTransform2D::translation(origin_x, origin_y)
.concatenate(resolved)
.concatenate(LayoutTransform2D::translation(-origin_x, -origin_y));
}
if !resolved.is_finite() {
resolved = LayoutTransform2D::IDENTITY;
has_unsupported_3d = true;
}
ResolvedLayoutTransform {
transform: resolved,
has_unsupported_3d,
establishes_property_space: false,
}
}
fn stylo_generated_content(computed: &ComputedValues) -> GeneratedContent {
match &computed.get_counters().content {
Content::Items(item_data) => {
let items = &item_data.items[..item_data.alt_start];
let mut output = String::new();
let mut has_unsupported_items = false;
for item in items {
match item {
ContentItem::String(text) => output.push_str(text),
_ => has_unsupported_items = true,
}
}
GeneratedContent::Items {
text: Arc::from(output),
has_unsupported_items,
}
}
Content::Normal => GeneratedContent::Normal,
Content::None => GeneratedContent::None,
}
}
fn stylo_list_marker_type(computed: &ComputedValues) -> LayoutListMarkerType {
use style::counter_style::{CounterStyle, Symbol};
match computed.clone_list_style_type().0 {
CounterStyle::None => LayoutListMarkerType::None,
CounterStyle::Name(name) => match &*name.0 {
"decimal" => LayoutListMarkerType::Decimal,
"lower-alpha" | "lower-latin" => LayoutListMarkerType::LowerAlpha,
"upper-alpha" | "upper-latin" => LayoutListMarkerType::UpperAlpha,
"disc" => LayoutListMarkerType::Disc,
"circle" => LayoutListMarkerType::Circle,
"square" => LayoutListMarkerType::Square,
"disclosure-open" => LayoutListMarkerType::DisclosureOpen,
"disclosure-closed" => LayoutListMarkerType::DisclosureClosed,
_ => LayoutListMarkerType::Fallback,
},
CounterStyle::String(value) => LayoutListMarkerType::String(Arc::from(value.as_ref())),
CounterStyle::Symbols { symbols, .. } => LayoutListMarkerType::Symbols(
symbols
.0
.iter()
.map(|symbol| match symbol {
Symbol::String(value) => Arc::from(value.as_ref()),
Symbol::Ident(value) => Arc::from(value.0.as_ref()),
})
.collect(),
),
}
}
fn stylo_font_metrics(computed: &ComputedValues) -> (f32, f32) {
use style::values::computed::font::LineHeight;
let font_size = computed.clone_font_size().used_size().px();
let line_height = match computed.clone_line_height() {
LineHeight::Normal => font_size * 1.2,
LineHeight::Number(number) => font_size * number.0,
LineHeight::Length(length) => length.0.px(),
};
(font_size, line_height)
}
fn stylo_vertical_align(
computed: &ComputedValues,
font_size: f32,
line_height: f32,
) -> (InlineVerticalAlign, bool) {
let (baseline_kind, baseline_shift) = match computed.clone_baseline_shift() {
GenericBaselineShift::Keyword(BaselineShiftKeyword::Sub) => {
(LayoutInlineAlignment::Baseline, -font_size * 0.2)
}
GenericBaselineShift::Keyword(BaselineShiftKeyword::Super) => {
(LayoutInlineAlignment::Baseline, font_size / 3.0)
}
GenericBaselineShift::Keyword(BaselineShiftKeyword::Top) => {
(LayoutInlineAlignment::Top, 0.0)
}
GenericBaselineShift::Keyword(BaselineShiftKeyword::Center) => {
(LayoutInlineAlignment::Middle, 0.0)
}
GenericBaselineShift::Keyword(BaselineShiftKeyword::Bottom) => {
(LayoutInlineAlignment::Bottom, 0.0)
}
GenericBaselineShift::Length(value) => (
LayoutInlineAlignment::Baseline,
value.resolve(CSSPixelLength::new(line_height)).px(),
),
};
let (alignment_kind, deferred) = match computed.clone_alignment_baseline() {
AlignmentBaseline::Baseline | AlignmentBaseline::Alphabetic => {
(LayoutInlineAlignment::Baseline, false)
}
AlignmentBaseline::TextTop => (LayoutInlineAlignment::TextTop, false),
AlignmentBaseline::Middle | AlignmentBaseline::Central => {
(LayoutInlineAlignment::Middle, false)
}
AlignmentBaseline::TextBottom => (LayoutInlineAlignment::TextBottom, false),
AlignmentBaseline::Ideographic
| AlignmentBaseline::Mathematical
| AlignmentBaseline::Hanging => (LayoutInlineAlignment::Baseline, true),
};
(
InlineVerticalAlign {
kind: if baseline_kind == LayoutInlineAlignment::Baseline {
alignment_kind
} else {
baseline_kind
},
baseline_shift,
},
deferred,
)
}
pub(crate) fn resolve_stylo_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
use style::values::computed::{CSSPixelLength, length_percentage::CalcLengthPercentage};
// SAFETY: `stylo_taffy` creates calc pointers from a live
// `CalcLengthPercentage`. Every converted style in this crate retains its
// originating `ComputedValues` until the containing `LayoutWorld` drops.
let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
calc.resolve(CSSPixelLength::new(parent_size)).px()
}
#[cfg(test)]
mod aspect_ratio_tests {
use super::*;
#[test]
fn replaced_ratio_resolution_preserves_auto_precedence_and_box_basis() {
let specified =
PreferredAspectRatio::Ratio(1.0).resolve_for_replaced(Some(2.0), BoxSizing::BorderBox);
assert_eq!(specified.ratio, Some(1.0));
assert_eq!(specified.box_sizing, BoxSizing::BorderBox);
let natural = PreferredAspectRatio::AutoAndRatio(1.0)
.resolve_for_replaced(Some(2.0), BoxSizing::BorderBox);
assert_eq!(natural.ratio, Some(2.0));
assert_eq!(natural.box_sizing, BoxSizing::ContentBox);
let fallback = PreferredAspectRatio::AutoAndRatio(1.0)
.resolve_for_replaced(None, BoxSizing::BorderBox);
assert_eq!(fallback.ratio, Some(1.0));
assert_eq!(fallback.box_sizing, BoxSizing::ContentBox);
}
}