blob: a1c80ff4ecdc464658e381fc90ef0a1bb05e9cdc [file] [log] [blame] [edit]
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use app_units::Au;
use malloc_size_of_derive::MallocSizeOf;
use style::Zero;
use style::color::AbsoluteColor;
use style::computed_values::direction::T as Direction;
use style::computed_values::isolation::T as ComputedIsolation;
use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode;
use style::computed_values::position::T as ComputedPosition;
use style::computed_values::transform_style::T as ComputedTransformStyle;
use style::computed_values::unicode_bidi::T as UnicodeBidi;
use style::logical_geometry::{Direction as AxisDirection, PhysicalSide, WritingMode};
use style::properties::ComputedValues;
use style::properties::longhands::backface_visibility::computed_value::T as BackfaceVisiblity;
use style::properties::longhands::box_sizing::computed_value::T as BoxSizing;
use style::properties::longhands::column_span::computed_value::T as ColumnSpan;
use style::properties::style_structs::Border;
use style::servo::selector_parser::PseudoElement;
use style::values::CSSFloat;
use style::values::computed::basic_shape::ClipPath;
use style::values::computed::image::Image as ComputedImageLayer;
use style::values::computed::{AlignItems, BorderStyle, Color, Inset, LengthPercentage, Margin};
use style::values::generics::box_::Perspective;
use style::values::generics::position::{GenericAspectRatio, PreferredRatio};
use style::values::generics::transform::{GenericRotate, GenericScale, GenericTranslate};
use style::values::specified::align::AlignFlags;
use style::values::specified::{Overflow, WillChangeBits, box_ as stylo};
use unicode_bidi::Level;
use webrender_api as wr;
use webrender_api::units::LayoutTransform;
use crate::dom_traversal::{Contents, NonReplacedContents};
use crate::fragment_tree::FragmentFlags;
use crate::geom::{
AuOrAuto, LengthPercentageOrAuto, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalSides,
PhysicalSize,
};
use crate::sizing::{Size, Sizes};
use crate::table::TableLayoutStyle;
use crate::{ContainingBlock, IndefiniteContainingBlock};
#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) enum Display {
None,
Contents,
GeneratingBox(DisplayGeneratingBox),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DisplayGeneratingBox {
OutsideInside {
outside: DisplayOutside,
inside: DisplayInside,
},
/// <https://drafts.csswg.org/css-display-3/#layout-specific-display>
LayoutInternal(DisplayLayoutInternal),
}
#[derive(Clone, Copy, Debug)]
pub struct AxesOverflow {
pub x: Overflow,
pub y: Overflow,
}
impl Default for AxesOverflow {
fn default() -> Self {
Self {
x: Overflow::Visible,
y: Overflow::Visible,
}
}
}
impl From<&ComputedValues> for AxesOverflow {
fn from(style: &ComputedValues) -> Self {
Self {
x: style.clone_overflow_x(),
y: style.clone_overflow_y(),
}
}
}
impl DisplayGeneratingBox {
pub(crate) fn display_inside(&self) -> DisplayInside {
match *self {
DisplayGeneratingBox::OutsideInside { inside, .. } => inside,
DisplayGeneratingBox::LayoutInternal(layout_internal) => {
layout_internal.display_inside()
},
}
}
pub(crate) fn used_value_for_contents(&self, contents: &Contents) -> Self {
// From <https://www.w3.org/TR/css-display-3/#layout-specific-display>:
// > When the display property of a replaced element computes to one of
// > the layout-internal values, it is handled as having a used value of
// > inline.
if matches!(self, Self::LayoutInternal(_)) && contents.is_replaced() {
Self::OutsideInside {
outside: DisplayOutside::Inline,
inside: DisplayInside::Flow {
is_list_item: false,
},
}
} else if matches!(
contents,
Contents::NonReplaced(NonReplacedContents::OfTextControl)
) {
// If it's an input or textarea, make sure the display-inside is flow-root.
// <https://html.spec.whatwg.org/multipage/#form-controls>
if let DisplayGeneratingBox::OutsideInside { outside, .. } = self {
DisplayGeneratingBox::OutsideInside {
outside: *outside,
inside: DisplayInside::FlowRoot {
is_list_item: false,
},
}
} else {
*self
}
} else {
*self
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DisplayOutside {
Block,
Inline,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DisplayInside {
// “list-items are limited to the Flow Layout display types”
// <https://drafts.csswg.org/css-display/#list-items>
Flow { is_list_item: bool },
FlowRoot { is_list_item: bool },
Flex,
Grid,
Table,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(clippy::enum_variant_names)]
/// <https://drafts.csswg.org/css-display-3/#layout-specific-display>
pub(crate) enum DisplayLayoutInternal {
TableCaption,
TableCell,
TableColumn,
TableColumnGroup,
TableFooterGroup,
TableHeaderGroup,
TableRow,
TableRowGroup,
}
impl DisplayLayoutInternal {
/// <https://drafts.csswg.org/css-display-3/#layout-specific-displa>
pub(crate) fn display_inside(&self) -> DisplayInside {
// When we add ruby, the display_inside of ruby must be Flow.
// TODO: this should be unreachable for everything but
// table cell and caption, once we have box tree fixups.
DisplayInside::FlowRoot {
is_list_item: false,
}
}
}
/// Percentages resolved but not `auto` margins
#[derive(Clone, Debug)]
pub(crate) struct PaddingBorderMargin {
pub padding: LogicalSides<Au>,
pub border: LogicalSides<Au>,
pub margin: LogicalSides<AuOrAuto>,
/// Pre-computed sums in each axis
pub padding_border_sums: LogicalVec2<Au>,
}
impl PaddingBorderMargin {
pub(crate) fn zero() -> Self {
Self {
padding: LogicalSides::zero(),
border: LogicalSides::zero(),
margin: LogicalSides::zero(),
padding_border_sums: LogicalVec2::zero(),
}
}
pub(crate) fn sums_auto_is_zero(
&self,
ignore_block_margins: LogicalSides1D<bool>,
) -> LogicalVec2<Au> {
let margin = self.margin.auto_is(Au::zero);
let mut sums = self.padding_border_sums;
sums.inline += margin.inline_sum();
if !ignore_block_margins.start {
sums.block += margin.block_start;
}
if !ignore_block_margins.end {
sums.block += margin.block_end;
}
sums
}
}
/// Resolved `aspect-ratio` property with respect to a specific element. Depends
/// on that element's `box-sizing` (and padding and border, if that `box-sizing`
/// is `border-box`).
#[derive(Clone, Copy, Debug)]
pub(crate) struct AspectRatio {
/// If the element that this aspect ratio belongs to uses box-sizing:
/// border-box, and the aspect-ratio property does not contain "auto", then
/// the aspect ratio is in respect to the border box. This will then contain
/// the summed sizes of the padding and border. Otherwise, it's 0.
box_sizing_adjustment: LogicalVec2<Au>,
/// The ratio itself (inline over block).
i_over_b: CSSFloat,
}
impl AspectRatio {
/// Given one side length, compute the other one.
pub(crate) fn compute_dependent_size(
&self,
ratio_dependent_axis: AxisDirection,
ratio_determining_size: Au,
) -> Au {
match ratio_dependent_axis {
// Calculate the inline size from the block size
AxisDirection::Inline => {
(ratio_determining_size + self.box_sizing_adjustment.block).scale_by(self.i_over_b) -
self.box_sizing_adjustment.inline
},
// Calculate the block size from the inline size
AxisDirection::Block => {
(ratio_determining_size + self.box_sizing_adjustment.inline)
.scale_by(1.0 / self.i_over_b) -
self.box_sizing_adjustment.block
},
}
}
pub(crate) fn from_logical_content_ratio(i_over_b: CSSFloat) -> Self {
Self {
box_sizing_adjustment: LogicalVec2::zero(),
i_over_b,
}
}
}
#[derive(Clone)]
pub(crate) struct ContentBoxSizesAndPBM {
pub content_box_sizes: LogicalVec2<Sizes>,
pub pbm: PaddingBorderMargin,
pub depends_on_block_constraints: bool,
pub preferred_size_computes_to_auto: LogicalVec2<bool>,
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub(crate) struct BorderStyleColor {
pub style: BorderStyle,
pub color: AbsoluteColor,
}
impl BorderStyleColor {
pub(crate) fn new(style: BorderStyle, color: AbsoluteColor) -> Self {
Self { style, color }
}
pub(crate) fn from_border(
border: &Border,
current_color: &AbsoluteColor,
) -> PhysicalSides<Self> {
let resolve = |color: &Color| color.resolve_to_absolute(current_color);
PhysicalSides::<Self>::new(
Self::new(border.border_top_style, resolve(&border.border_top_color)),
Self::new(
border.border_right_style,
resolve(&border.border_right_color),
),
Self::new(
border.border_bottom_style,
resolve(&border.border_bottom_color),
),
Self::new(border.border_left_style, resolve(&border.border_left_color)),
)
}
pub(crate) fn hidden() -> Self {
Self::new(BorderStyle::Hidden, AbsoluteColor::TRANSPARENT_BLACK)
}
}
impl Default for BorderStyleColor {
fn default() -> Self {
Self::new(BorderStyle::None, AbsoluteColor::TRANSPARENT_BLACK)
}
}
/// <https://drafts.csswg.org/cssom-view/#overflow-directions>
/// > A scrolling box of a viewport or element has two overflow directions,
/// > which are the block-end and inline-end directions for that viewport or element.
pub(crate) struct OverflowDirection {
/// Whether block-end or inline-end direction is [PhysicalSide::Right].
pub rightward: bool,
/// Whether block-end or inline-end direction is [PhysicalSide::Bottom].
pub downward: bool,
}
pub(crate) trait ComputedValuesExt {
fn physical_box_offsets(&self) -> PhysicalSides<LengthPercentageOrAuto<'_>>;
fn box_offsets(&self, writing_mode: WritingMode) -> LogicalSides<LengthPercentageOrAuto<'_>>;
fn box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>>;
fn min_box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>>;
fn max_box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>>;
fn content_box_size_for_box_size(
&self,
box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>>;
fn content_min_box_size_for_min_size(
&self,
box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>>;
fn content_max_box_size_for_max_size(
&self,
box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>>;
fn border_style_color(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<BorderStyleColor>;
fn physical_margin(&self) -> PhysicalSides<LengthPercentageOrAuto<'_>>;
fn margin(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<LengthPercentageOrAuto<'_>>;
fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool;
fn has_transform_or_perspective_style(&self) -> bool;
fn has_effective_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool;
fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool;
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32;
fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow;
fn establishes_block_formatting_context(&self, fragment_flags: FragmentFlags) -> bool;
fn establishes_stacking_context(&self, fragment_flags: FragmentFlags) -> bool;
fn establishes_scroll_container(&self, fragment_flags: FragmentFlags) -> bool;
fn establishes_containing_block_for_absolute_descendants(
&self,
fragment_flags: FragmentFlags,
) -> bool;
fn establishes_containing_block_for_all_descendants(
&self,
fragment_flags: FragmentFlags,
) -> bool;
fn preferred_aspect_ratio(
&self,
natural_aspect_ratio: Option<CSSFloat>,
padding_border_sums: &LogicalVec2<Au>,
) -> Option<AspectRatio>;
fn background_is_transparent(&self) -> bool;
fn get_webrender_primitive_flags(&self) -> wr::PrimitiveFlags;
fn bidi_control_chars(&self) -> (&'static str, &'static str);
fn resolve_align_self(
&self,
resolved_auto_value: AlignItems,
resolved_normal_value: AlignItems,
) -> AlignItems;
fn depends_on_block_constraints_due_to_relative_positioning(
&self,
writing_mode: WritingMode,
) -> bool;
fn is_inline_box(&self, fragment_flags: FragmentFlags) -> bool;
fn overflow_direction(&self) -> OverflowDirection;
fn to_bidi_level(&self) -> Level;
}
impl ComputedValuesExt for ComputedValues {
fn physical_box_offsets(&self) -> PhysicalSides<LengthPercentageOrAuto<'_>> {
fn convert(inset: &Inset) -> LengthPercentageOrAuto<'_> {
match inset {
Inset::LengthPercentage(v) => LengthPercentageOrAuto::LengthPercentage(v),
Inset::Auto => LengthPercentageOrAuto::Auto,
Inset::AnchorFunction(_) => unreachable!("anchor() should be disabled"),
Inset::AnchorSizeFunction(_) => unreachable!("anchor-size() should be disabled"),
Inset::AnchorContainingCalcFunction(_) => {
unreachable!("anchor() and anchor-size() should be disabled")
},
}
}
let position = self.get_position();
PhysicalSides::new(
convert(&position.top),
convert(&position.right),
convert(&position.bottom),
convert(&position.left),
)
}
fn box_offsets(&self, writing_mode: WritingMode) -> LogicalSides<LengthPercentageOrAuto<'_>> {
LogicalSides::from_physical(&self.physical_box_offsets(), writing_mode)
}
fn box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>> {
let position = self.get_position();
LogicalVec2::from_physical_size(
&PhysicalSize::new(
position.clone_width().into(),
position.clone_height().into(),
),
containing_block_writing_mode,
)
}
fn min_box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>> {
let position = self.get_position();
LogicalVec2::from_physical_size(
&PhysicalSize::new(
position.clone_min_width().into(),
position.clone_min_height().into(),
),
containing_block_writing_mode,
)
}
fn max_box_size(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalVec2<Size<LengthPercentage>> {
let position = self.get_position();
LogicalVec2::from_physical_size(
&PhysicalSize::new(
position.clone_max_width().into(),
position.clone_max_height().into(),
),
containing_block_writing_mode,
)
}
fn content_box_size_for_box_size(
&self,
box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>> {
match self.get_position().box_sizing {
BoxSizing::ContentBox => box_size,
// These may be negative, but will later be clamped by `min-width`/`min-height`
// which is clamped to zero.
BoxSizing::BorderBox => box_size.map_inline_and_block_sizes(
|value| value - pbm.padding_border_sums.inline,
|value| value - pbm.padding_border_sums.block,
),
}
}
fn content_min_box_size_for_min_size(
&self,
min_box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>> {
match self.get_position().box_sizing {
BoxSizing::ContentBox => min_box_size,
// Clamp to zero to make sure the used size components are non-negative
BoxSizing::BorderBox => min_box_size.map_inline_and_block_sizes(
|value| Au::zero().max(value - pbm.padding_border_sums.inline),
|value| Au::zero().max(value - pbm.padding_border_sums.block),
),
}
}
fn content_max_box_size_for_max_size(
&self,
max_box_size: LogicalVec2<Size<Au>>,
pbm: &PaddingBorderMargin,
) -> LogicalVec2<Size<Au>> {
match self.get_position().box_sizing {
BoxSizing::ContentBox => max_box_size,
// This may be negative, but will later be clamped by `min-width`
// which itself is clamped to zero.
BoxSizing::BorderBox => max_box_size.map_inline_and_block_sizes(
|value| value - pbm.padding_border_sums.inline,
|value| value - pbm.padding_border_sums.block,
),
}
}
fn border_style_color(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<BorderStyleColor> {
let current_color = self.get_inherited_text().clone_color();
LogicalSides::from_physical(
&BorderStyleColor::from_border(self.get_border(), &current_color),
containing_block_writing_mode,
)
}
fn physical_margin(&self) -> PhysicalSides<LengthPercentageOrAuto<'_>> {
fn convert(inset: &Margin) -> LengthPercentageOrAuto<'_> {
match inset {
Margin::LengthPercentage(v) => LengthPercentageOrAuto::LengthPercentage(v),
Margin::Auto => LengthPercentageOrAuto::Auto,
Margin::AnchorSizeFunction(_) | Margin::AnchorContainingCalcFunction(_) => {
unreachable!("anchor-size() should be disabled")
},
}
}
let margin = self.get_margin();
PhysicalSides::new(
convert(&margin.margin_top),
convert(&margin.margin_right),
convert(&margin.margin_bottom),
convert(&margin.margin_left),
)
}
fn margin(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<LengthPercentageOrAuto<'_>> {
LogicalSides::from_physical(&self.physical_margin(), containing_block_writing_mode)
}
fn is_inline_box(&self, fragment_flags: FragmentFlags) -> bool {
self.get_box().display.is_inline_flow() &&
!fragment_flags
.intersects(FragmentFlags::IS_REPLACED | FragmentFlags::IS_TEXT_CONTROL)
}
/// Returns true if this is a transformable element.
fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool {
// "A transformable element is an element in one of these categories:
// * all elements whose layout is governed by the CSS box model except for
// non-replaced inline boxes, table-column boxes, and table-column-group
// boxes,
// * all SVG paint server elements, the clipPath element and SVG renderable
// elements with the exception of any descendant element of text content
// elements."
// <https://drafts.csswg.org/css-transforms/#transformable-element>
// TODO: check for all cases listed in the above spec.
!self.is_inline_box(fragment_flags)
}
/// Returns true if this style has a transform or perspective property set.
fn has_transform_or_perspective_style(&self) -> bool {
!self.get_box().transform.0.is_empty() ||
self.get_box().scale != GenericScale::None ||
self.get_box().rotate != GenericRotate::None ||
self.get_box().translate != GenericTranslate::None ||
self.get_box().perspective != Perspective::None
}
/// Returns true if this style has a transform or perspective property set, and
/// it applies to this element.
#[inline]
fn has_effective_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool {
self.is_transformable(fragment_flags) && self.has_transform_or_perspective_style()
}
/// Whether the `z-index` property applies to this fragment.
fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool {
// As per CSS 2 § 9.9.1, `z-index` applies to positioned elements.
// <http://www.w3.org/TR/CSS2/visuren.html#z-index>
if self.get_box().position != ComputedPosition::Static {
return true;
}
// More modern specs also apply it to flex and grid items.
// - From <https://www.w3.org/TR/css-flexbox-1/#painting>:
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
// > document order is used in place of raw document order, and z-index values other than auto
// > create a stacking context even if position is static (behaving exactly as if position
// > were relative).
// - From <https://drafts.csswg.org/css-flexbox/#painting>:
// > The painting order of grid items is exactly the same as inline blocks [CSS2], except that
// > order-modified document order is used in place of raw document order, and z-index values
// > other than auto create a stacking context even if position is static (behaving exactly
// > as if position were relative).
fragment_flags.contains(FragmentFlags::IS_FLEX_OR_GRID_ITEM)
}
/// Get the effective z-index of this fragment. Z-indices only apply to positioned elements
/// per CSS 2 9.9.1 (<http://www.w3.org/TR/CSS2/visuren.html#z-index>), so this value may differ
/// from the value specified in the style.
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32 {
if self.z_index_applies(fragment_flags) {
self.get_position().z_index.integer_or(0)
} else {
0
}
}
/// Get the effective overflow of this box. The property only applies to block containers,
/// flex containers, and grid containers. And some box types only accept a few values.
/// <https://www.w3.org/TR/css-overflow-3/#overflow-control>
fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow {
// https://www.w3.org/TR/css-overflow-3/#overflow-propagation
// The element from which the value is propagated must then have a used overflow value of visible.
if fragment_flags.contains(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT) {
return AxesOverflow::default();
}
let mut overflow = AxesOverflow::from(self);
// From <https://www.w3.org/TR/css-overflow-4/#overflow-control>:
// "On replaced elements, the used values of all computed values other than visible is clip."
if fragment_flags.contains(FragmentFlags::IS_REPLACED) {
if overflow.x != Overflow::Visible {
overflow.x = Overflow::Clip;
}
if overflow.y != Overflow::Visible {
overflow.y = Overflow::Clip;
}
return overflow;
}
let ignores_overflow = match self.get_box().display.inside() {
// <https://drafts.csswg.org/css-overflow-3/#overflow-control>
// `overflow` doesn't apply to inline boxes.
stylo::DisplayInside::Flow => self.is_inline_box(fragment_flags),
// According to <https://drafts.csswg.org/css-tables/#global-style-overrides>,
// - overflow applies to table-wrapper boxes and not to table grid boxes.
// That's what Blink and WebKit do, however Firefox matches a CSSWG resolution that says
// the opposite: <https://lists.w3.org/Archives/Public/www-style/2012Aug/0298.html>
// Due to the way that we implement table-wrapper boxes, it's easier to align with Firefox.
// - Tables ignore overflow values different than visible, clip and hidden.
// This affects both axes, to ensure they have the same scrollability.
stylo::DisplayInside::Table => {
!matches!(self.pseudo(), Some(PseudoElement::ServoTableGrid)) ||
matches!(overflow.x, Overflow::Auto | Overflow::Scroll) ||
matches!(overflow.y, Overflow::Auto | Overflow::Scroll)
},
// <https://drafts.csswg.org/css-tables/#global-style-overrides>
// Table-track and table-track-group boxes ignore overflow.
stylo::DisplayInside::TableColumn |
stylo::DisplayInside::TableColumnGroup |
stylo::DisplayInside::TableRow |
stylo::DisplayInside::TableRowGroup |
stylo::DisplayInside::TableHeaderGroup |
stylo::DisplayInside::TableFooterGroup => true,
_ => false,
};
if ignores_overflow {
return AxesOverflow::default();
}
overflow
}
/// Return true if this style is a normal block and establishes
/// a new block formatting context.
///
/// NOTE: This should be kept in sync with the checks in `impl
/// TElement::compute_layout_damage` for `ServoLayoutElement` in
/// `components/script/layout_dom/element.rs`.
fn establishes_block_formatting_context(&self, fragment_flags: FragmentFlags) -> bool {
if self.establishes_scroll_container(fragment_flags) {
return true;
}
if self.get_column().is_multicol() {
return true;
}
if self.get_column().column_span == ColumnSpan::All {
return true;
}
// Per <https://drafts.csswg.org/css-align/#distribution-block>:
// Block containers with an `align-content` value that is not `normal` should
// form an independent block formatting context. This should really only happen
// for block containers, but we do not support subgrid containers yet which is the
// only other case.
if self.get_position().align_content.0.primary() != AlignFlags::NORMAL {
return true;
}
// TODO: We need to handle CSS Contain here.
false
}
/// Whether or not the `overflow` value of this style establishes a scroll container.
fn establishes_scroll_container(&self, fragment_flags: FragmentFlags) -> bool {
// Checking one axis suffices, because the computed value ensures that
// either both axes are scrollable, or none is scrollable.
self.effective_overflow(fragment_flags).x.is_scrollable()
}
/// Returns true if this fragment establishes a new stacking context and false otherwise.
fn establishes_stacking_context(&self, fragment_flags: FragmentFlags) -> bool {
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
// > If any non-initial value of a property would create a stacking context on the element,
// > specifying that property in will-change must create a stacking context on the element.
let will_change_bits = self.clone_will_change().bits;
if will_change_bits
.intersects(WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::OPACITY)
{
return true;
}
// From <https://www.w3.org/TR/CSS2/visuren.html#z-index>, values different than `auto`
// make the box establish a stacking context.
if self.z_index_applies(fragment_flags) &&
(!self.get_position().z_index.is_auto() ||
will_change_bits.intersects(WillChangeBits::Z_INDEX))
{
return true;
}
// Fixed position and sticky position always create stacking contexts.
// Note `will-change: position` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if matches!(
self.get_box().position,
ComputedPosition::Fixed | ComputedPosition::Sticky
) {
return true;
}
// From <https://www.w3.org/TR/css-transforms-1/#transform-rendering>
// > For elements whose layout is governed by the CSS box model, any value other than
// > `none` for the `transform` property results in the creation of a stacking context.
//
// From <https://www.w3.org/TR/css-transforms-2/#individual-transforms>
// > all other values […] create a stacking context and containing block for all
// > descendants, per usual for transforms.
//
// From <https://www.w3.org/TR/css-transforms-2/#perspective-property>
// > any value other than none establishes a stacking context.
//
// From <https://www.w3.org/TR/css-transforms-2/#transform-style-property>
// > A computed value of `preserve-3d` for `transform-style` on a transformable element
// > establishes both a stacking context and a containing block for all descendants.
if self.is_transformable(fragment_flags) &&
(self.has_transform_or_perspective_style() ||
self.get_box().transform_style == ComputedTransformStyle::Preserve3d ||
will_change_bits
.intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE))
{
return true;
}
// From <https://www.w3.org/TR/css-color-3/#transparency>
// > implementations must create a new stacking context for any element with opacity less than 1.
// Note `will-change: opacity` is handled above by `WillChangeBits::OPACITY`.
let effects = self.get_effects();
if effects.opacity != 1.0 {
return true;
}
// From <https://www.w3.org/TR/filter-effects-1/#FilterProperty>
// > A computed value of other than `none` results in the creation of a stacking context
// Note `will-change: filter` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if !effects.filter.0.is_empty() {
return true;
}
// From <https://www.w3.org/TR/compositing-1/#mix-blend-mode>
// > Applying a blendmode other than `normal` to the element must establish a new stacking context
// Note `will-change: mix-blend-mode` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if effects.mix_blend_mode != ComputedMixBlendMode::Normal {
return true;
}
// From <https://www.w3.org/TR/css-masking-1/#the-clip-path>
// > A computed value of other than `none` results in the creation of a stacking context.
// Note `will-change: clip-path` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if self.get_svg().clip_path != ClipPath::None {
return true;
}
// From <https://www.w3.org/TR/compositing-1/#isolation>
// > For CSS, setting `isolation` to `isolate` will turn the element into a stacking context.
// Note `will-change: isolation` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if self.get_box().isolation == ComputedIsolation::Isolate {
return true;
}
// From https://www.w3.org/TR/CSS22/visuren.html#z-index:
// > The root element forms the root stacking context.
if fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT) {
return true;
}
// TODO: We need to handle CSS Contain here.
false
}
/// Returns true if this style establishes a containing block for absolute
/// descendants (`position: absolute`). If this style happens to establish a
/// containing block for “all descendants” (ie including `position: fixed`
/// descendants) this method will return true, but a true return value does
/// not imply that the style establishes a containing block for all descendants.
/// Use `establishes_containing_block_for_all_descendants()` instead.
fn establishes_containing_block_for_absolute_descendants(
&self,
fragment_flags: FragmentFlags,
) -> bool {
if self.establishes_containing_block_for_all_descendants(fragment_flags) {
return true;
}
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
// > If any non-initial value of a property would cause the element to
// > generate a containing block for absolutely positioned elements, specifying that property in
// > will-change must cause the element to generate a containing block for absolutely positioned elements.
if self
.clone_will_change()
.bits
.intersects(WillChangeBits::POSITION)
{
return true;
}
self.clone_position() != ComputedPosition::Static
}
/// Returns true if this style establishes a containing block for
/// all descendants, including fixed descendants (`position: fixed`).
/// Note that this also implies that it establishes a containing block
/// for absolute descendants (`position: absolute`).
fn establishes_containing_block_for_all_descendants(
&self,
fragment_flags: FragmentFlags,
) -> bool {
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
// > If any non-initial value of a property would cause the element to generate a
// > containing block for fixed positioned elements, specifying that property in will-change
// > must cause the element to generate a containing block for fixed positioned elements.
let will_change_bits = self.clone_will_change().bits;
// From <https://drafts.csswg.org/css-transforms-1/#transform-rendering>:
// > any value other than `none` for the `transform` property also causes the element
// > to establish a containing block for all descendants.
//
// From <https://www.w3.org/TR/css-transforms-2/#individual-transforms>
// > all other values […] create a stacking context and containing block for all
// > descendants, per usual for transforms.
//
// From <https://drafts.csswg.org/css-transforms-2/#perspective-property>:
// > The use of this property with any value other than `none` […] establishes a
// > containing block for all descendants, just like the `transform` property does.
//
// From <https://drafts.csswg.org/css-transforms-2/#transform-style-property>:
// > A computed value of `preserve-3d` for `transform-style` on a transformable element
// > establishes both a stacking context and a containing block for all descendants.
if self.is_transformable(fragment_flags) &&
(self.has_transform_or_perspective_style() ||
self.get_box().transform_style == ComputedTransformStyle::Preserve3d ||
will_change_bits
.intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE))
{
return true;
}
// From <https://www.w3.org/TR/filter-effects-1/#propdef-filter>:
// > A value other than none for the filter property results in the creation of a containing
// > block for absolute and fixed positioned descendants unless the element it applies to is
// > a document root element in the current browsing context.
if !fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT) &&
(!self.get_effects().filter.0.is_empty() ||
will_change_bits.intersects(WillChangeBits::FIXPOS_CB_NON_SVG))
{
return true;
}
// TODO: We need to handle CSS Contain here.
false
}
/// Resolve the preferred aspect ratio according to the given natural aspect
/// ratio and the `aspect-ratio` property.
/// See <https://drafts.csswg.org/css-sizing-4/#aspect-ratio>.
fn preferred_aspect_ratio(
&self,
natural_aspect_ratio: Option<CSSFloat>,
padding_border_sums: &LogicalVec2<Au>,
) -> Option<AspectRatio> {
let GenericAspectRatio {
auto,
ratio: mut preferred_ratio,
} = self.clone_aspect_ratio();
// For all cases where a ratio is specified:
// "If the <ratio> is degenerate, the property instead behaves as auto."
if matches!(preferred_ratio, PreferredRatio::Ratio(ratio) if ratio.is_degenerate()) {
preferred_ratio = PreferredRatio::None;
}
let to_logical_ratio = |physical_ratio| {
if self.writing_mode.is_horizontal() {
physical_ratio
} else {
1.0 / physical_ratio
}
};
match (auto, preferred_ratio) {
// The value `auto`. Either the ratio was not specified, or was
// degenerate and set to PreferredRatio::None above.
//
// "Replaced elements with a natural aspect ratio use that aspect
// ratio; otherwise the box has no preferred aspect ratio. Size
// calculations involving the aspect ratio work with the content box
// dimensions always."
(_, PreferredRatio::None) => natural_aspect_ratio
.map(to_logical_ratio)
.map(AspectRatio::from_logical_content_ratio),
// "If both auto and a <ratio> are specified together, the preferred
// aspect ratio is the specified ratio of width / height unless it
// is a replaced element with a natural aspect ratio, in which case
// that aspect ratio is used instead. In all cases, size
// calculations involving the aspect ratio work with the content box
// dimensions always."
(true, PreferredRatio::Ratio(preferred_ratio)) => Some({
let physical_ratio = natural_aspect_ratio
.unwrap_or_else(|| (preferred_ratio.0).0 / (preferred_ratio.1).0);
AspectRatio::from_logical_content_ratio(to_logical_ratio(physical_ratio))
}),
// "The box’s preferred aspect ratio is the specified ratio of width
// / height. Size calculations involving the aspect ratio work with
// the dimensions of the box specified by box-sizing."
(false, PreferredRatio::Ratio(preferred_ratio)) => {
// If the `box-sizing` is `border-box`, use the padding and
// border when calculating the aspect ratio.
let box_sizing_adjustment = match self.clone_box_sizing() {
BoxSizing::ContentBox => LogicalVec2::zero(),
BoxSizing::BorderBox => *padding_border_sums,
};
Some(AspectRatio {
i_over_b: to_logical_ratio((preferred_ratio.0).0 / (preferred_ratio.1).0),
box_sizing_adjustment,
})
},
}
}
/// Whether or not this style specifies a non-transparent background.
fn background_is_transparent(&self) -> bool {
let background = self.get_background();
let color = self.resolve_color(&background.background_color);
color.alpha == 0.0 &&
background
.background_image
.0
.iter()
.all(|layer| matches!(layer, ComputedImageLayer::None))
}
/// Generate appropriate WebRender `PrimitiveFlags` that should be used
/// for display items generated by the `Fragment` which owns this style.
fn get_webrender_primitive_flags(&self) -> wr::PrimitiveFlags {
match self.get_box().backface_visibility {
BackfaceVisiblity::Visible => wr::PrimitiveFlags::default(),
BackfaceVisiblity::Hidden => wr::PrimitiveFlags::empty(),
}
}
/// If the 'unicode-bidi' property has a value other than 'normal', return the bidi control codes
/// to inject before and after the text content of the element.
/// See the table in <http://dev.w3.org/csswg/css-writing-modes/#unicode-bidi>.
fn bidi_control_chars(&self) -> (&'static str, &'static str) {
match (
self.get_text().unicode_bidi,
self.get_inherited_box().direction,
) {
(UnicodeBidi::Normal, _) => ("", ""),
(UnicodeBidi::Embed, Direction::Ltr) => ("\u{202a}", "\u{202c}"),
(UnicodeBidi::Embed, Direction::Rtl) => ("\u{202b}", "\u{202c}"),
(UnicodeBidi::Isolate, Direction::Ltr) => ("\u{2066}", "\u{2069}"),
(UnicodeBidi::Isolate, Direction::Rtl) => ("\u{2067}", "\u{2069}"),
(UnicodeBidi::BidiOverride, Direction::Ltr) => ("\u{202d}", "\u{202c}"),
(UnicodeBidi::BidiOverride, Direction::Rtl) => ("\u{202e}", "\u{202c}"),
(UnicodeBidi::IsolateOverride, Direction::Ltr) => {
("\u{2068}\u{202d}", "\u{202c}\u{2069}")
},
(UnicodeBidi::IsolateOverride, Direction::Rtl) => {
("\u{2068}\u{202e}", "\u{202c}\u{2069}")
},
(UnicodeBidi::Plaintext, _) => ("\u{2068}", "\u{2069}"),
}
}
fn resolve_align_self(
&self,
resolved_auto_value: AlignItems,
resolved_normal_value: AlignItems,
) -> AlignItems {
match self.clone_align_self().0.0 {
AlignFlags::AUTO => resolved_auto_value,
AlignFlags::NORMAL => resolved_normal_value,
value => AlignItems(value),
}
}
fn depends_on_block_constraints_due_to_relative_positioning(
&self,
writing_mode: WritingMode,
) -> bool {
if !matches!(
self.get_box().position,
ComputedPosition::Relative | ComputedPosition::Sticky
) {
return false;
}
let box_offsets = self.box_offsets(writing_mode);
let has_percentage = |offset: LengthPercentageOrAuto<'_>| {
offset
.non_auto()
.is_some_and(LengthPercentage::has_percentage)
};
has_percentage(box_offsets.block_start) || has_percentage(box_offsets.block_end)
}
// <https://drafts.csswg.org/cssom-view/#overflow-directions>
fn overflow_direction(&self) -> OverflowDirection {
let inline_end_direction = self.writing_mode.inline_end_physical_side();
let block_end_direction = self.writing_mode.block_end_physical_side();
let rightward = inline_end_direction == PhysicalSide::Right ||
block_end_direction == PhysicalSide::Right;
let downward = inline_end_direction == PhysicalSide::Bottom ||
block_end_direction == PhysicalSide::Bottom;
// TODO(stevennovaryo): We should consider the flex-container's CSS (e.g. flow-direction: column-reverse).
OverflowDirection {
rightward,
downward,
}
}
/// The default bidirectional embedding level for the writing mode of this style.
///
/// Returns bidi level 0 if the mode is LTR, or 1 otherwise.
fn to_bidi_level(&self) -> Level {
if self.writing_mode.is_bidi_ltr() {
Level::ltr()
} else {
Level::rtl()
}
}
}
pub(crate) enum LayoutStyle<'a> {
Default(&'a ComputedValues),
Table(TableLayoutStyle<'a>),
}
impl LayoutStyle<'_> {
#[inline]
pub(crate) fn style(&self) -> &ComputedValues {
match self {
Self::Default(style) => style,
Self::Table(table) => table.style(),
}
}
#[inline]
pub(crate) fn is_table(&self) -> bool {
matches!(self, Self::Table(_))
}
pub(crate) fn content_box_sizes_and_padding_border_margin(
&self,
containing_block: &IndefiniteContainingBlock,
) -> ContentBoxSizesAndPBM {
// <https://drafts.csswg.org/css-sizing-3/#cyclic-percentage-contribution>
// If max size properties or preferred size properties are set to a value containing
// indefinite percentages, we treat the entire value as the initial value of the property.
// However, for min size properties, as well as for margins and paddings,
// we instead resolve indefinite percentages against zero.
let containing_block_size_or_zero =
containing_block.size.map(|value| value.unwrap_or_default());
let writing_mode = containing_block.writing_mode;
let pbm = self.padding_border_margin_with_writing_mode_and_containing_block_inline_size(
writing_mode,
containing_block_size_or_zero.inline,
);
let style = self.style();
let box_size = style.box_size(writing_mode);
let min_size = style.min_box_size(writing_mode);
let max_size = style.max_box_size(writing_mode);
let preferred_size_computes_to_auto = box_size.map(|size| size.is_initial());
let depends_on_block_constraints = |size: &Size<LengthPercentage>| {
match size {
// fit-content is like clamp(min-content, stretch, max-content), but currently
// min-content and max-content have the same behavior in the block axis,
// so there is no dependency on block constraints.
// TODO: for flex and grid layout, min-content and max-content should be different.
// TODO: We are assuming that Size::Initial doesn't stretch. However, it may actually
// stretch flex and grid items depending on the CSS Align properties, in that case
// the caller needs to take care of it.
Size::Stretch => true,
Size::Numeric(length_percentage) => length_percentage.has_percentage(),
_ => false,
}
};
let depends_on_block_constraints = depends_on_block_constraints(&box_size.block) ||
depends_on_block_constraints(&min_size.block) ||
depends_on_block_constraints(&max_size.block) ||
style.depends_on_block_constraints_due_to_relative_positioning(writing_mode);
let box_size = box_size.map_with(&containing_block.size, |size, basis| {
size.resolve_percentages_for_preferred(*basis)
});
let content_box_size = style.content_box_size_for_box_size(box_size, &pbm);
let min_size = min_size.percentages_relative_to_basis(&containing_block_size_or_zero);
let content_min_box_size = style.content_min_box_size_for_min_size(min_size, &pbm);
let max_size = max_size.map_with(&containing_block.size, |size, basis| {
size.resolve_percentages_for_max(*basis)
});
let content_max_box_size = style.content_max_box_size_for_max_size(max_size, &pbm);
ContentBoxSizesAndPBM {
content_box_sizes: LogicalVec2 {
block: Sizes::new(
content_box_size.block,
content_min_box_size.block,
content_max_box_size.block,
),
inline: Sizes::new(
content_box_size.inline,
content_min_box_size.inline,
content_max_box_size.inline,
),
},
pbm,
depends_on_block_constraints,
preferred_size_computes_to_auto,
}
}
pub(crate) fn padding_border_margin(
&self,
containing_block: &ContainingBlock,
) -> PaddingBorderMargin {
self.padding_border_margin_with_writing_mode_and_containing_block_inline_size(
containing_block.style.writing_mode,
containing_block.size.inline,
)
}
pub(crate) fn padding_border_margin_with_writing_mode_and_containing_block_inline_size(
&self,
writing_mode: WritingMode,
containing_block_inline_size: Au,
) -> PaddingBorderMargin {
let padding = self
.padding(writing_mode)
.percentages_relative_to(containing_block_inline_size);
let style = self.style();
let border = self.border_width(writing_mode);
let margin = style
.margin(writing_mode)
.percentages_relative_to(containing_block_inline_size);
PaddingBorderMargin {
padding_border_sums: LogicalVec2 {
inline: padding.inline_sum() + border.inline_sum(),
block: padding.block_sum() + border.block_sum(),
},
padding,
border,
margin,
}
}
pub(crate) fn padding(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<LengthPercentage> {
if matches!(self, Self::Table(table) if table.collapses_borders()) {
// https://drafts.csswg.org/css-tables/#collapsed-style-overrides
// > The padding of the table-root is ignored (as if it was set to 0px).
return LogicalSides::zero();
}
let padding = self.style().get_padding().clone();
LogicalSides::from_physical(
&PhysicalSides::new(
padding.padding_top.0,
padding.padding_right.0,
padding.padding_bottom.0,
padding.padding_left.0,
),
containing_block_writing_mode,
)
}
pub(crate) fn border_width(
&self,
containing_block_writing_mode: WritingMode,
) -> LogicalSides<Au> {
let border_width = match self {
// For tables in collapsed-borders mode we halve the border widths, because
// > in this model, the width of the table includes half the table border.
// https://www.w3.org/TR/CSS22/tables.html#collapsing-borders
Self::Table(table) if table.collapses_borders() => table
.halved_collapsed_border_widths()
.to_physical(self.style().writing_mode),
_ => {
let border = self.style().get_border();
PhysicalSides::new(
border.border_top_width,
border.border_right_width,
border.border_bottom_width,
border.border_left_width,
)
},
};
LogicalSides::from_physical(&border_width, containing_block_writing_mode)
}
}
impl From<stylo::Display> for Display {
fn from(packed: stylo::Display) -> Self {
let outside = packed.outside();
let inside = packed.inside();
let outside = match outside {
stylo::DisplayOutside::Block => DisplayOutside::Block,
stylo::DisplayOutside::Inline => DisplayOutside::Inline,
stylo::DisplayOutside::TableCaption => {
return Display::GeneratingBox(DisplayGeneratingBox::LayoutInternal(
DisplayLayoutInternal::TableCaption,
));
},
stylo::DisplayOutside::InternalTable => {
let internal = match inside {
stylo::DisplayInside::TableRowGroup => DisplayLayoutInternal::TableRowGroup,
stylo::DisplayInside::TableColumn => DisplayLayoutInternal::TableColumn,
stylo::DisplayInside::TableColumnGroup => {
DisplayLayoutInternal::TableColumnGroup
},
stylo::DisplayInside::TableHeaderGroup => {
DisplayLayoutInternal::TableHeaderGroup
},
stylo::DisplayInside::TableFooterGroup => {
DisplayLayoutInternal::TableFooterGroup
},
stylo::DisplayInside::TableRow => DisplayLayoutInternal::TableRow,
stylo::DisplayInside::TableCell => DisplayLayoutInternal::TableCell,
_ => unreachable!("Non-internal DisplayInside found"),
};
return Display::GeneratingBox(DisplayGeneratingBox::LayoutInternal(internal));
},
// This should not be a value of DisplayInside, but oh well
// special-case display: contents because we still want it to work despite the early return
stylo::DisplayOutside::None if inside == stylo::DisplayInside::Contents => {
return Display::Contents;
},
stylo::DisplayOutside::None => return Display::None,
};
let inside = match packed.inside() {
stylo::DisplayInside::Flow => DisplayInside::Flow {
is_list_item: packed.is_list_item(),
},
stylo::DisplayInside::FlowRoot => DisplayInside::FlowRoot {
is_list_item: packed.is_list_item(),
},
stylo::DisplayInside::Flex => DisplayInside::Flex,
stylo::DisplayInside::Grid => DisplayInside::Grid,
// These should not be values of DisplayInside, but oh well
stylo::DisplayInside::None => return Display::None,
stylo::DisplayInside::Contents => return Display::Contents,
stylo::DisplayInside::Table => DisplayInside::Table,
stylo::DisplayInside::TableRowGroup |
stylo::DisplayInside::TableColumn |
stylo::DisplayInside::TableColumnGroup |
stylo::DisplayInside::TableHeaderGroup |
stylo::DisplayInside::TableFooterGroup |
stylo::DisplayInside::TableRow |
stylo::DisplayInside::TableCell => unreachable!("Internal DisplayInside found"),
};
Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { outside, inside })
}
}
pub(crate) trait Clamp: Sized {
fn clamp_below_max(self, max: Option<Self>) -> Self;
fn clamp_between_extremums(self, min: Self, max: Option<Self>) -> Self;
}
impl Clamp for Au {
fn clamp_below_max(self, max: Option<Self>) -> Self {
match max {
None => self,
Some(max) => self.min(max),
}
}
fn clamp_between_extremums(self, min: Self, max: Option<Self>) -> Self {
self.clamp_below_max(max).max(min)
}
}
pub(crate) trait TransformExt {
fn change_basis(&self, x: f32, y: f32, z: f32) -> Self;
}
impl TransformExt for LayoutTransform {
/// <https://drafts.csswg.org/css-transforms/#transformation-matrix-computation>
fn change_basis(&self, x: f32, y: f32, z: f32) -> Self {
let pre_translation = Self::translation(x, y, z);
let post_translation = Self::translation(-x, -y, -z);
post_translation.then(self).then(&pre_translation)
}
}