From 01905923dbc2165c11a59bd5b097134c31bfbb59 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sat, 6 Jun 2020 00:07:49 +0200 Subject: [PATCH] First pass at implementing the Flex Layout Algorithm https://drafts.csswg.org/css-flexbox/#layout-algorithm --- components/layout_2020/flexbox/geom.rs | 270 +++++ components/layout_2020/flexbox/layout.rs | 1080 ++++++++++++++++- components/layout_2020/flexbox/mod.rs | 8 +- components/layout_2020/formatting_contexts.rs | 13 +- components/layout_2020/geom.rs | 14 + components/style/values/computed/length.rs | 15 + 6 files changed, 1381 insertions(+), 19 deletions(-) create mode 100644 components/layout_2020/flexbox/geom.rs diff --git a/components/layout_2020/flexbox/geom.rs b/components/layout_2020/flexbox/geom.rs new file mode 100644 index 000000000000..2e68508d71bc --- /dev/null +++ b/components/layout_2020/flexbox/geom.rs @@ -0,0 +1,270 @@ +/* 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/. */ + +//! https://drafts.csswg.org/css-flexbox/#box-model + +use crate::geom::flow_relative::{Rect, Sides, Vec2}; +use style::properties::longhands::flex_direction::computed_value::T as FlexDirection; + +#[derive(Clone, Copy)] +pub(super) struct FlexRelativeVec2 { + pub main: T, + pub cross: T, +} + +#[derive(Clone, Copy)] +pub(super) struct FlexRelativeSides { + pub cross_start: T, + pub main_start: T, + pub cross_end: T, + pub main_end: T, +} + +pub(super) struct FlexRelativeRect { + pub start_corner: FlexRelativeVec2, + pub size: FlexRelativeVec2, +} + +impl std::ops::Add for FlexRelativeVec2 +where + T: std::ops::Add, +{ + type Output = FlexRelativeVec2; + fn add(self, rhs: Self) -> Self::Output { + FlexRelativeVec2 { + main: self.main + rhs.main, + cross: self.cross + rhs.cross, + } + } +} + +impl std::ops::Sub for FlexRelativeVec2 +where + T: std::ops::Sub, +{ + type Output = FlexRelativeVec2; + fn sub(self, rhs: Self) -> Self::Output { + FlexRelativeVec2 { + main: self.main - rhs.main, + cross: self.cross - rhs.cross, + } + } +} + +impl FlexRelativeSides { + pub fn sum_by_axis(self) -> FlexRelativeVec2 + where + T: std::ops::Add, + { + FlexRelativeVec2 { + main: self.main_start + self.main_end, + cross: self.cross_start + self.cross_end, + } + } +} + +/// One of the two bits set by the `flex-direction` property +/// (The other is "forward" v.s. reverse.) +#[derive(Clone, Copy, PartialEq)] +pub(super) enum FlexAxis { + /// The main axis is the inline axis of the container (not necessarily of flex items!), + /// cross is block. + Row, + /// The main axis is the block axis, cross is inline. + Column, +} + +/// Which flow-relative sides map to the main-start and cross-start sides, respectively. +/// See https://drafts.csswg.org/css-flexbox/#box-model +#[derive(Clone, Copy)] +pub(super) enum MainStartCrossStart { + InlineStartBlockStart, + InlineStartBlockEnd, + BlockStartInlineStart, + BlockStartInlineEnd, + InlineEndBlockStart, + InlineEndBlockEnd, + BlockEndInlineStart, + BlockEndInlineEnd, +} + +impl FlexAxis { + pub fn from(flex_direction: FlexDirection) -> Self { + match flex_direction { + FlexDirection::Row | FlexDirection::RowReverse => FlexAxis::Row, + FlexDirection::Column | FlexDirection::ColumnReverse => FlexAxis::Column, + } + } + + pub fn vec2_to_flex_relative(self, flow_relative: Vec2) -> FlexRelativeVec2 { + let Vec2 { inline, block } = flow_relative; + match self { + FlexAxis::Row => FlexRelativeVec2 { + main: inline, + cross: block, + }, + FlexAxis::Column => FlexRelativeVec2 { + main: block, + cross: inline, + }, + } + } + + pub fn vec2_to_flow_relative(self, flex_relative: FlexRelativeVec2) -> Vec2 { + let FlexRelativeVec2 { main, cross } = flex_relative; + match self { + FlexAxis::Row => Vec2 { + inline: main, + block: cross, + }, + FlexAxis::Column => Vec2 { + block: main, + inline: cross, + }, + } + } +} + +macro_rules! sides_mapping_methods { + ( + $( + $variant: path => { + $( $flex_relative_side: ident <=> $flow_relative_side: ident, )+ + }, + )+ + ) => { + pub fn sides_to_flex_relative(self, flow_relative: Sides) -> FlexRelativeSides { + match self { + $( + $variant => FlexRelativeSides { + $( $flex_relative_side: flow_relative.$flow_relative_side, )+ + }, + )+ + } + } + + pub fn sides_to_flow_relative(self, flex_relative: FlexRelativeSides) -> Sides { + match self { + $( + $variant => Sides { + $( $flow_relative_side: flex_relative.$flex_relative_side, )+ + }, + )+ + } + } + } +} + +impl MainStartCrossStart { + pub fn from(flex_direction: FlexDirection, flex_wrap_reverse: bool) -> Self { + match (flex_direction, flex_wrap_reverse) { + // See definition of each keyword in + // https://drafts.csswg.org/css-flexbox/#flex-direction-property and + // https://drafts.csswg.org/css-flexbox/#flex-wrap-property, + // or the tables (though they map to physical rather than flow-relative) at + // https://drafts.csswg.org/css-flexbox/#axis-mapping + (FlexDirection::Row, true) => MainStartCrossStart::InlineStartBlockEnd, + (FlexDirection::Row, false) => MainStartCrossStart::InlineStartBlockStart, + (FlexDirection::Column, true) => MainStartCrossStart::BlockStartInlineEnd, + (FlexDirection::Column, false) => MainStartCrossStart::BlockStartInlineStart, + (FlexDirection::RowReverse, true) => MainStartCrossStart::InlineEndBlockEnd, + (FlexDirection::RowReverse, false) => MainStartCrossStart::InlineEndBlockStart, + (FlexDirection::ColumnReverse, true) => MainStartCrossStart::BlockEndInlineEnd, + (FlexDirection::ColumnReverse, false) => MainStartCrossStart::BlockEndInlineStart, + } + } + + sides_mapping_methods! { + MainStartCrossStart::InlineStartBlockStart => { + main_start <=> inline_start, + cross_start <=> block_start, + main_end <=> inline_end, + cross_end <=> block_end, + }, + MainStartCrossStart::InlineStartBlockEnd => { + main_start <=> inline_start, + cross_start <=> block_end, + main_end <=> inline_end, + cross_end <=> block_start, + }, + MainStartCrossStart::BlockStartInlineStart => { + main_start <=> block_start, + cross_start <=> inline_start, + main_end <=> block_end, + cross_end <=> inline_end, + }, + MainStartCrossStart::BlockStartInlineEnd => { + main_start <=> block_start, + cross_start <=> inline_end, + main_end <=> block_end, + cross_end <=> inline_start, + }, + MainStartCrossStart::InlineEndBlockStart => { + main_start <=> inline_end, + cross_start <=> block_start, + main_end <=> inline_start, + cross_end <=> block_end, + }, + MainStartCrossStart::InlineEndBlockEnd => { + main_start <=> inline_end, + cross_start <=> block_end, + main_end <=> inline_start, + cross_end <=> block_start, + }, + MainStartCrossStart::BlockEndInlineStart => { + main_start <=> block_end, + cross_start <=> inline_start, + main_end <=> block_start, + cross_end <=> inline_end, + }, + MainStartCrossStart::BlockEndInlineEnd => { + main_start <=> block_end, + cross_start <=> inline_end, + main_end <=> block_start, + cross_end <=> inline_start, + }, + } +} + +/// The start corner coordinates in both the input rectangle and output rectangle +/// are relative to some “base rectangle” whose size is passed here. +pub(super) fn rect_to_flow_relative( + flex_axis: FlexAxis, + main_start_cross_start_sides_are: MainStartCrossStart, + base_rect_size: FlexRelativeVec2, + rect: FlexRelativeRect, +) -> Rect +where + T: Copy + std::ops::Add + std::ops::Sub, +{ + // First, convert from (start corner, size) to offsets from the edges of the base rectangle + + let end_corner_position = rect.start_corner + rect.size; + let end_corner_offsets = base_rect_size - end_corner_position; + // No-ops, but hopefully clarifies to human readers: + let start_corner_position = rect.start_corner; + let start_corner_offsets = start_corner_position; + + // Then, convert to flow-relative using methods above + let flow_relative_offsets = + main_start_cross_start_sides_are.sides_to_flow_relative(FlexRelativeSides { + main_start: start_corner_offsets.main, + cross_start: start_corner_offsets.cross, + main_end: end_corner_offsets.main, + cross_end: end_corner_offsets.cross, + }); + let flow_relative_base_rect_size = flex_axis.vec2_to_flow_relative(base_rect_size); + + // Finally, convert back to (start corner, size) + let start_corner = Vec2 { + inline: flow_relative_offsets.inline_start, + block: flow_relative_offsets.block_start, + }; + let end_corner_position = Vec2 { + inline: flow_relative_base_rect_size.inline - flow_relative_offsets.inline_end, + block: flow_relative_base_rect_size.block - flow_relative_offsets.block_end, + }; + let size = &end_corner_position - &start_corner; + Rect { start_corner, size } +} diff --git a/components/layout_2020/flexbox/layout.rs b/components/layout_2020/flexbox/layout.rs index 1da062b77647..62804d8152cc 100644 --- a/components/layout_2020/flexbox/layout.rs +++ b/components/layout_2020/flexbox/layout.rs @@ -2,17 +2,129 @@ * 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 super::FlexContainer; +use super::geom::{ + FlexAxis, FlexRelativeRect, FlexRelativeSides, FlexRelativeVec2, MainStartCrossStart, +}; +use super::{FlexContainer, FlexLevelBox}; use crate::context::LayoutContext; -use crate::formatting_contexts::IndependentLayout; -use crate::positioned::PositioningContext; +use crate::formatting_contexts::{IndependentFormattingContext, IndependentLayout}; +use crate::fragments::{ + AbsoluteOrFixedPositionedFragment, BoxFragment, CollapsedBlockMargins, Fragment, +}; +use crate::geom::flow_relative::{Rect, Sides, Vec2}; +use crate::geom::LengthOrAuto; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; +use crate::sizing::ContentSizes; +use crate::style_ext::ComputedValuesExt; use crate::ContainingBlock; +use atomic_refcell::AtomicRefMut; +use std::cell::Cell; +use style::properties::longhands::box_sizing::computed_value::T as BoxSizing; +use style::properties::longhands::flex_direction::computed_value::T as FlexDirection; +use style::properties::longhands::flex_wrap::computed_value::T as FlexWrap; +use style::values::computed::length::Size; use style::values::computed::Length; +use style::values::generics::flex::GenericFlexBasis as FlexBasis; use style::Zero; -// FIXME: `min-width: auto` is not zero: https://drafts.csswg.org/css-flexbox/#min-size-auto +// FIMXE: “Flex items […] `z-index` values other than `auto` create a stacking context +// even if `position` is `static` (behaving exactly as if `position` were `relative`).” +// https://drafts.csswg.org/css-flexbox/#painting +// (likely in `display_list/stacking_context.rs`) + +/// Layout parameters and intermediate results about a flex container, +/// grouped to avoid passing around many parameters +struct FlexContext<'a> { + layout_context: &'a LayoutContext<'a>, + positioning_context: &'a mut PositioningContext, + containing_block: &'a ContainingBlock<'a>, // For items + container_is_single_line: bool, + container_min_cross_size: Length, + container_max_cross_size: Option, + flex_axis: FlexAxis, + main_start_cross_start_sides_are: MainStartCrossStart, + container_definite_inner_size: FlexRelativeVec2>, +} + +/// A flex item with some intermediate results +struct FlexItem<'a> { + box_: &'a mut IndependentFormattingContext, + tree_rank: usize, + content_box_size: FlexRelativeVec2, + content_min_size: FlexRelativeVec2, + content_max_size: FlexRelativeVec2>, + padding: FlexRelativeSides, + border: FlexRelativeSides, + margin: FlexRelativeSides, + + /// Sum of padding, border, and margin (with `auto` assumed to be zero) in each axis. + /// This is the difference between an outer and inner size. + pbm_auto_is_zero: FlexRelativeVec2, + + /// https://drafts.csswg.org/css-flexbox/#algo-main-item + flex_base_size: Length, + + /// https://drafts.csswg.org/css-flexbox/#algo-main-item + hypothetical_main_size: Length, +} + +/// A flex line with some intermediate results +struct FlexLine<'a> { + items: &'a mut [FlexItem<'a>], + outer_hypothetical_main_sizes_sum: Length, +} + +/// Return type of `FlexItem::layout` +struct FlexItemLayoutResult { + hypothetical_cross_size: Length, + fragments: Vec, +} + +/// Return type of `FlexLine::layout` +struct FlexLineLayoutResult { + cross_size: Length, + item_fragments: Vec, // One per flex item, in the given order +} + +impl FlexContext<'_> { + fn vec2_to_flex_relative(&self, x: Vec2) -> FlexRelativeVec2 { + self.flex_axis.vec2_to_flex_relative(x) + } + + fn sides_to_flex_relative(&self, x: Sides) -> FlexRelativeSides { + self.main_start_cross_start_sides_are + .sides_to_flex_relative(x) + } + + fn sides_to_flow_relative(&self, x: FlexRelativeSides) -> Sides { + self.main_start_cross_start_sides_are + .sides_to_flow_relative(x) + } + + fn rect_to_flow_relative( + &self, + base_rect_size: FlexRelativeVec2, + rect: FlexRelativeRect, + ) -> Rect { + super::geom::rect_to_flow_relative( + self.flex_axis, + self.main_start_cross_start_sides_are, + base_rect_size, + rect, + ) + } +} impl FlexContainer { + pub fn inline_content_sizes(&self) -> ContentSizes { + // FIXME: implement this. The spec for it is the same as for "normal" layout: + // https://drafts.csswg.org/css-flexbox/#layout-algorithm + // … except that the parts that say “the flex container is being sized + // under a min or max-content constraint” apply. + ContentSizes::zero() // Return an incorrect result rather than panic + } + + /// https://drafts.csswg.org/css-flexbox/#layout-algorithm pub(crate) fn layout( &self, layout_context: &LayoutContext, @@ -20,14 +132,960 @@ impl FlexContainer { containing_block: &ContainingBlock, tree_rank: usize, ) -> IndependentLayout { - // FIXME - let _ = layout_context; - let _ = positioning_context; - let _ = containing_block; - let _ = tree_rank; + // Actual length may be less, but we guess that usually not by a lot + let mut flex_items = Vec::with_capacity(self.children.len()); + + // Absolutely-positioned children of the flex container may be interleaved + // with flex items. We need to preserve their relative order for correct painting order, + // which is the order of `Fragment`s in this function’s return value. + let original_order_with_absolutely_positioned = self + .children + .iter() + .enumerate() + .map(|(tree_rank, arcrefcell)| { + let borrowed = arcrefcell.borrow_mut(); + match &*borrowed { + FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(absolutely_positioned) => { + Ok(absolutely_positioned.clone()) + }, + FlexLevelBox::FlexItem(_) => { + let item = AtomicRefMut::map(borrowed, |child| match child { + FlexLevelBox::FlexItem(item) => item, + _ => unreachable!(), + }); + flex_items.push((tree_rank, item)); + Err(()) + }, + } + }) + .collect::>(); + + let mut content_block_size_option_dance = None; + let fragments = + positioning_context.adjust_static_positions(tree_rank, |positioning_context| { + let (mut flex_item_fragments, content_block_size) = layout( + layout_context, + positioning_context, + containing_block, + flex_items + .iter_mut() + .map(|(tree_rank, child)| (*tree_rank, &mut **child)), + ); + content_block_size_option_dance = Some(content_block_size); + let fragments = original_order_with_absolutely_positioned + .into_iter() + .enumerate() + .map(|(tree_rank, child_as_abspos)| match child_as_abspos { + Err(()) => { + // The `()` here is a place-holder for a flex item. + // The `flex_item_fragments` iterator yields one fragment + // per flex item, in the original order. + Fragment::Box(flex_item_fragments.next().unwrap()) + }, + Ok(absolutely_positioned) => { + let position = absolutely_positioned + .borrow() + .context + .style() + .clone_position(); + let hoisted_box = AbsolutelyPositionedBox::to_hoisted( + absolutely_positioned, + Vec2::zero(), + tree_rank, + containing_block, + ); + let hoisted_fragment = hoisted_box.fragment.clone(); + positioning_context.push(hoisted_box); + Fragment::AbsoluteOrFixedPositioned(AbsoluteOrFixedPositionedFragment { + hoisted_fragment, + position, + }) + }, + }) + .collect::>(); + // There should be no more flex items + assert!(flex_item_fragments.next().is_none()); + fragments + }); + IndependentLayout { - fragments: Vec::new(), - content_block_size: Length::zero(), + fragments, + content_block_size: content_block_size_option_dance.unwrap(), + } + } +} + +/// Return one fragment for each flex item, in the provided order, and the used block-size. +fn layout<'context, 'boxes>( + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + flex_item_boxes: impl Iterator, +) -> (impl Iterator, Length) { + // FIXME: get actual min/max cross size for the flex container. + // We have access to style for the flex container in `containing_block.style`, + // but resolving percentages there requires access + // to the flex container’s own containing block which we don’t have. + // For now, use incorrect values instead of panicking: + let container_min_cross_size = Length::zero(); + let container_max_cross_size = None; + + let flex_container_position_style = containing_block.style.get_position(); + let flex_wrap = flex_container_position_style.flex_wrap; + let flex_direction = flex_container_position_style.flex_direction; + + // Column flex containers are not fully implemented yet, + // so give a different layout instead of panicking. + // FIXME: implement `todo!`s for FlexAxis::Column below, and remove this + let flex_direction = match flex_direction { + FlexDirection::Row | FlexDirection::Column => FlexDirection::Row, + FlexDirection::RowReverse | FlexDirection::ColumnReverse => FlexDirection::RowReverse, + }; + + let container_is_single_line = match containing_block.style.get_position().flex_wrap { + FlexWrap::Nowrap => true, + FlexWrap::Wrap | FlexWrap::WrapReverse => false, + }; + let flex_axis = FlexAxis::from(flex_direction); + let flex_wrap_reverse = match flex_wrap { + FlexWrap::Nowrap | FlexWrap::Wrap => false, + FlexWrap::WrapReverse => true, + }; + let mut flex_context = FlexContext { + layout_context, + positioning_context, + containing_block, + container_min_cross_size, + container_max_cross_size, + container_is_single_line, + flex_axis, + main_start_cross_start_sides_are: MainStartCrossStart::from( + flex_direction, + flex_wrap_reverse, + ), + // https://drafts.csswg.org/css-flexbox/#definite-sizes + container_definite_inner_size: flex_axis.vec2_to_flex_relative(Vec2 { + inline: Some(containing_block.inline_size), + block: containing_block.block_size.non_auto(), + }), + }; + + let mut flex_items = flex_item_boxes + .map(|(tree_rank, box_)| FlexItem::new(&flex_context, box_, tree_rank)) + .collect::>(); + + // “Determine the main size of the flex container” + // https://drafts.csswg.org/css-flexbox/#algo-main-container + let container_main_size = match flex_axis { + FlexAxis::Row => containing_block.inline_size, + FlexAxis::Column => { + // FIXME “using the rules of the formatting context in which it participates” + // but if block-level with `block-size: max-auto` that requires + // layout of the content to be fully done: + // https://github.com/w3c/csswg-drafts/issues/4905 + // Gecko reportedly uses `block-size: fit-content` in this case + // (which requires running another pass of the "full" layout algorithm) + todo!() + // Note: this panic shouldn’t happen since the start of `FlexContainer::layout` + // forces `FlexAxis::Row`. + }, + }; + + // “Resolve the flexible lengths of all the flex items to find their *used main size*.” + // https://drafts.csswg.org/css-flexbox/#algo-flex + let flex_lines = collect_flex_lines( + &mut flex_context, + container_main_size, + &mut flex_items, + |flex_context, mut line| line.layout(flex_context, container_main_size), + ); + + // https://drafts.csswg.org/css-flexbox/#algo-cross-container + let container_cross_size = flex_context + .container_definite_inner_size + .cross + .unwrap_or_else(|| { + flex_lines + .iter() + .map(|line| line.cross_size) + .sum::() + }) + .clamp_between_extremums( + flex_context.container_min_cross_size, + flex_context.container_max_cross_size, + ); + + // https://drafts.csswg.org/css-flexbox/#algo-line-align + let mut cross_start_position_cursor = Length::zero(); + let line_cross_start_positions = flex_lines + .iter() + .map(|line| { + // FIXME: “Align all flex lines per `align-content`.” + // For now we hard-code the behavior of `align-content: flex-start`. + let cross_start = cross_start_position_cursor; + let cross_end = cross_start + line.cross_size; + cross_start_position_cursor = cross_end; + cross_start + }) + .collect::>(); + + let content_block_size = match flex_context.flex_axis { + FlexAxis::Row => { + // `container_main_size` ends up unused here but in this case that’s fine + // since it was already excatly the one decided by the outer formatting context. + container_cross_size + }, + FlexAxis::Column => { + // FIXME: `container_cross_size` ends up unused here, which is a bug. + // It is meant to be the used inline-size, but the parent formatting context + // has already decided a possibly-different used inline-size. + // The spec is missing something to resolve this conflict: + // https://github.com/w3c/csswg-drafts/issues/5190 + // And we’ll need to change the signature of `IndependentFormattingContext::layout` + // to allow the inner formatting context to “negociate” a used inline-size + // with the outer one somehow. + container_main_size + }, + }; + let fragments = flex_lines + .into_iter() + .zip(line_cross_start_positions) + .flat_map(move |(mut line, line_cross_start_position)| { + let flow_relative_line_position = match (flex_axis, flex_wrap_reverse) { + (FlexAxis::Row, false) => Vec2 { + block: line_cross_start_position, + inline: Length::zero(), + }, + (FlexAxis::Row, true) => Vec2 { + block: container_cross_size - line_cross_start_position - line.cross_size, + inline: Length::zero(), + }, + (FlexAxis::Column, false) => Vec2 { + block: Length::zero(), + inline: line_cross_start_position, + }, + (FlexAxis::Column, true) => Vec2 { + block: Length::zero(), + inline: container_cross_size - line_cross_start_position - line.cross_size, + }, + }; + for fragment in &mut line.item_fragments { + fragment.content_rect.start_corner += &flow_relative_line_position + } + line.item_fragments + }) + .into_iter(); + (fragments, content_block_size) +} + +impl<'a> FlexItem<'a> { + fn new( + flex_context: &FlexContext, + box_: &'a mut IndependentFormattingContext, + tree_rank: usize, + ) -> Self { + let containing_block = flex_context.containing_block; + let box_style = box_.style(); + + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + containing_block.style.writing_mode, box_style.writing_mode, + "Mixed writing modes are not supported yet" + ); + + let container_is_horizontal = containing_block.style.writing_mode.is_horizontal(); + let item_is_horizontal = box_style.writing_mode.is_horizontal(); + let item_is_orthogonal = item_is_horizontal != container_is_horizontal; + let container_is_row = flex_context.flex_axis == FlexAxis::Row; + let cross_axis_is_item_block_axis = container_is_row ^ item_is_orthogonal; + + let pbm = box_style.padding_border_margin(containing_block); + let content_box_size = box_style.content_box_size(containing_block, &pbm); + let max_size = box_style.content_max_box_size(containing_block, &pbm); + let min_size = box_style.content_min_box_size(containing_block, &pbm); + + let min_size = min_size.auto_is(|| automatic_min_size(box_)); + let margin_auto_is_zero = pbm.margin.auto_is(Length::zero); + + let content_box_size = flex_context.vec2_to_flex_relative(content_box_size); + let content_max_size = flex_context.vec2_to_flex_relative(max_size); + let content_min_size = flex_context.vec2_to_flex_relative(min_size); + let margin_auto_is_zero = flex_context.sides_to_flex_relative(margin_auto_is_zero); + let margin = flex_context.sides_to_flex_relative(pbm.margin); + let padding = flex_context.sides_to_flex_relative(pbm.padding); + let border = flex_context.sides_to_flex_relative(pbm.border); + + let padding_border = padding.sum_by_axis() + border.sum_by_axis(); + let pbm_auto_is_zero = padding_border + margin_auto_is_zero.sum_by_axis(); + + let flex_base_size = flex_base_size( + flex_context, + box_, + cross_axis_is_item_block_axis, + content_box_size, + padding_border, + ); + + let hypothetical_main_size = + flex_base_size.clamp_between_extremums(content_min_size.main, content_max_size.main); + + Self { + box_, + tree_rank, + content_box_size, + content_min_size, + content_max_size, + padding, + border, + margin, + pbm_auto_is_zero, + flex_base_size, + hypothetical_main_size, + } + } +} + +/// https://drafts.csswg.org/css-flexbox/#min-size-auto +fn automatic_min_size(_box: &IndependentFormattingContext) -> Length { + // FIMXE: implement the actual algorithm + Length::zero() // Give an incorrect value rather than panicking +} + +/// https://drafts.csswg.org/css-flexbox/#algo-main-item +fn flex_base_size( + flex_context: &FlexContext, + flex_item: &mut IndependentFormattingContext, + cross_axis_is_item_block_axis: bool, + content_box_size: FlexRelativeVec2, + padding_border_sums: FlexRelativeVec2, +) -> Length { + let used_flex_basis = match &flex_item.style().get_position().flex_basis { + FlexBasis::Content => FlexBasis::Content, + FlexBasis::Size(Size::LengthPercentage(length_percentage)) => { + let apply_box_sizing = |length: Length| { + match flex_item.style().get_position().box_sizing { + BoxSizing::ContentBox => length, + BoxSizing::BorderBox => { + // This may make `length` negative, + // but it will be clamped in the hypothetical main size + length - padding_border_sums.main + }, + } + }; + // “For example, percentage values of flex-basis are resolved + // against the flex item’s containing block (i.e. its flex container);” + match flex_context.container_definite_inner_size.main { + Some(container_definite_main_size) => { + let length = length_percentage + .0 + .percentage_relative_to(container_definite_main_size); + FlexBasis::Size(apply_box_sizing(length)) + }, + None => { + if let Some(length) = length_percentage.0.to_length() { + FlexBasis::Size(apply_box_sizing(length)) + } else { + // “and if that containing block’s size is indefinite, + // the used value for `flex-basis` is `content`.” + // https://drafts.csswg.org/css-flexbox/#flex-basis-property + FlexBasis::Content + } + }, + } + }, + FlexBasis::Size(Size::Auto) => { + // “When specified on a flex item, the `auto` keyword retrieves + // the value of the main size property as the used `flex-basis`.” + match content_box_size.main { + LengthOrAuto::LengthPercentage(length) => FlexBasis::Size(length), + // “If that value is itself `auto`, then the used value is `content`.” + LengthOrAuto::Auto => FlexBasis::Content, + } + }, + }; + + // NOTE: at this point the flex basis is either `content` or a definite length. + // However when we add support for additional values for `width` and `height` + // from https://drafts.csswg.org/css-sizing/#preferred-size-properties, + // it could have those values too. + + match used_flex_basis { + FlexBasis::Size(length) => { + // Case A: definite flex basis + length + }, + FlexBasis::Content => { + // FIXME: implement cases B, C, D. + + // Case E: everything else + // “treating a value of content as max-content.” + if cross_axis_is_item_block_axis { + // The main axis is the inline axis + flex_item + .inline_content_sizes(flex_context.layout_context) + .max_content + } else { + // FIXME: block-axis content sizing requires another pass + // of "full" layout + todo!() + // Note: this panic shouldn’t happen since the start of `FlexContainer::layout` + // forces `FlexAxis::Row` and the `writing-mode` property is disabled. + } + }, + } +} + +// “Collect flex items into flex lines” +// https://drafts.csswg.org/css-flexbox/#algo-line-break +fn collect_flex_lines<'items, LineResult>( + flex_context: &mut FlexContext, + container_main_size: Length, + mut items: &'items mut [FlexItem<'items>], + mut each: impl FnMut(&mut FlexContext, FlexLine<'items>) -> LineResult, +) -> Vec { + if flex_context.container_is_single_line { + let line = FlexLine { + outer_hypothetical_main_sizes_sum: items + .iter() + .map(|item| item.hypothetical_main_size + item.pbm_auto_is_zero.main) + .sum(), + items, + }; + return vec![each(flex_context, line)]; + } else { + let mut lines = Vec::new(); + let mut line_size_so_far = Length::zero(); + let mut line_so_far_is_empty = true; + let mut index = 0; + while let Some(item) = items.get(index) { + let item_size = item.hypothetical_main_size + item.pbm_auto_is_zero.main; + let line_size_would_be = line_size_so_far + item_size; + let item_fits = line_size_would_be <= container_main_size; + if item_fits || line_so_far_is_empty { + line_size_so_far = line_size_would_be; + line_so_far_is_empty = false; + index += 1; + } else { + // We found something that doesn’t fit. This line ends *before* this item. + let (line_items, rest) = items.split_at_mut(index); + let line = FlexLine { + items: line_items, + outer_hypothetical_main_sizes_sum: line_size_so_far, + }; + items = rest; + lines.push(each(flex_context, line)); + // The next line has this item. + line_size_so_far = item_size; + index = 1; + } + } + // The last line is added even without finding an item that doesn’t fit + let line = FlexLine { + items, + outer_hypothetical_main_sizes_sum: line_size_so_far, + }; + lines.push(each(flex_context, line)); + lines + } +} + +impl FlexLine<'_> { + fn layout( + &mut self, + flex_context: &mut FlexContext, + container_main_size: Length, + ) -> FlexLineLayoutResult { + let (item_used_main_sizes, remaining_free_space) = + self.resolve_flexible_lengths(container_main_size); + + // https://drafts.csswg.org/css-flexbox/#algo-cross-item + let item_layout_results = self + .items + .iter_mut() + .zip(&item_used_main_sizes) + .map(|(item, &used_main_size)| item.layout(used_main_size, flex_context)) + .collect::>(); + + let hypothetical_cross_sizes = || { + item_layout_results + .iter() + .map(|item_result| item_result.hypothetical_cross_size) + }; + + // https://drafts.csswg.org/css-flexbox/#algo-cross-line + let line_cross_size = self.cross_size(hypothetical_cross_sizes(), &flex_context); + let line_size = FlexRelativeVec2 { + main: container_main_size, + cross: line_cross_size, + }; + + // FIXME: Handle `align-content: stretch` + // https://drafts.csswg.org/css-flexbox/#algo-line-stretch + + // FIXME: Collapse `visibility: collapse` items + // This involves “restart layout from the beginning” with a modified second round, + // which will make structuring the code… interesting. + // https://drafts.csswg.org/css-flexbox/#algo-visibility + + // Determine the used cross size of each flex item + // https://drafts.csswg.org/css-flexbox/#algo-stretch + // FIXME: Handle `align-self: stretch` + let item_used_cross_sizes = hypothetical_cross_sizes().collect::>(); + + // Distribute any remaining free space + // https://drafts.csswg.org/css-flexbox/#algo-main-align + let item_main_margins = self.resolve_auto_main_margins(remaining_free_space); + + // FIXME: “Align the items along the main-axis per justify-content.” + // For now we hard-code `justify-content` to `flex-start`. + + // https://drafts.csswg.org/css-flexbox/#algo-cross-margins + let item_cross_margins = self.items.iter().zip(&item_used_cross_sizes).map( + |(item, &item_cross_content_size)| { + item.resolve_auto_cross_margins( + &flex_context, + line_cross_size, + item_cross_content_size, + ) + }, + ); + + let item_margins = item_main_margins + .zip(item_cross_margins) + .map( + |((main_start, main_end), (cross_start, cross_end))| FlexRelativeSides { + main_start, + main_end, + cross_start, + cross_end, + }, + ) + .collect::>(); + // https://drafts.csswg.org/css-flexbox/#algo-main-align + let items_content_main_start_positions = + self.align_along_main_axis(&item_used_main_sizes, &item_margins); + + // https://drafts.csswg.org/css-flexbox/#algo-cross-align + let item_content_cross_start_posititons = self + .items + .iter() + .zip(&item_margins) + .map(|(item, margin)| item.align_along_cross_axis(margin)); + + let item_fragments = self + .items + .iter() + .zip(item_layout_results) + .zip( + item_used_main_sizes + .iter() + .zip(&item_used_cross_sizes) + .map(|(&main, &cross)| FlexRelativeVec2 { main, cross }) + .zip( + items_content_main_start_positions + .zip(item_content_cross_start_posititons) + .map(|(main, cross)| FlexRelativeVec2 { main, cross }), + ) + .map(|(size, start_corner)| FlexRelativeRect { size, start_corner }), + ) + .zip(&item_margins) + .map(|(((item, item_layout_result), content_rect), margin)| { + let content_rect = flex_context.rect_to_flow_relative(line_size, content_rect); + let margin = flex_context.sides_to_flow_relative(*margin); + let collapsed_margin = CollapsedBlockMargins::from_margin(&margin); + BoxFragment::new( + item.box_.tag(), + item.box_.style().clone(), + item_layout_result.fragments, + content_rect, + flex_context.sides_to_flow_relative(item.padding), + flex_context.sides_to_flow_relative(item.border), + margin, + collapsed_margin, + ) + }) + .collect(); + FlexLineLayoutResult { + cross_size: line_cross_size, + item_fragments, + } + } + + /// Return the *main size* of each item, and the line’s remainaing free space + /// https://drafts.csswg.org/css-flexbox/#resolve-flexible-lengths + fn resolve_flexible_lengths(&self, container_main_size: Length) -> (Vec, Length) { + let mut frozen = vec![false; self.items.len()]; + let mut target_main_sizes_vec = self + .items + .iter() + .map(|item| item.flex_base_size) + .collect::>(); + + // Using `Cell`s reconciles mutability with multiple borrows in closures + let target_main_sizes = Cell::from_mut(&mut *target_main_sizes_vec).as_slice_of_cells(); + let frozen = Cell::from_mut(&mut *frozen).as_slice_of_cells(); + let frozen_count = Cell::new(0); + + let grow = self.outer_hypothetical_main_sizes_sum < container_main_size; + let flex_factor = |item: &FlexItem| { + let position_style = item.box_.style().get_position(); + if grow { + position_style.flex_grow.0 + } else { + position_style.flex_shrink.0 + } + }; + let items = || self.items.iter().zip(target_main_sizes).zip(frozen); + + // “Size inflexible items” + for ((item, target_main_size), frozen) in items() { + let is_inflexible = flex_factor(item) == 0. || + if grow { + item.flex_base_size > item.hypothetical_main_size + } else { + item.flex_base_size < item.hypothetical_main_size + }; + if is_inflexible { + frozen_count.set(frozen_count.get() + 1); + frozen.set(true); + target_main_size.set(item.hypothetical_main_size); + } } + + let check_for_flexible_items = || frozen_count.get() < self.items.len(); + let free_space = || { + container_main_size - + items() + .map(|((item, target_main_size), frozen)| { + item.pbm_auto_is_zero.main + + if frozen.get() { + target_main_size.get() + } else { + item.flex_base_size + } + }) + .sum() + }; + // https://drafts.csswg.org/css-flexbox/#initial-free-space + let initial_free_space = free_space(); + let unfrozen_items = || { + items().filter_map(|(item_and_target_main_size, frozen)| { + if !frozen.get() { + Some(item_and_target_main_size) + } else { + None + } + }) + }; + loop { + // https://drafts.csswg.org/css-flexbox/#remaining-free-space + let mut remaining_free_space = free_space(); + if !check_for_flexible_items() { + return (target_main_sizes_vec, remaining_free_space); + } + let unfrozen_items_flex_factor_sum: f32 = + unfrozen_items().map(|(item, _)| flex_factor(item)).sum(); + // FIXME: I (Simon) transcribed the spec but I don’t yet understand why this algorithm + if unfrozen_items_flex_factor_sum < 1. { + let multiplied = initial_free_space * unfrozen_items_flex_factor_sum; + if multiplied.abs() < remaining_free_space.abs() { + remaining_free_space = multiplied + } + } + + // “Distribute free space proportional to the flex factors.” + // FIXME: is it a problem if floating point precision errors accumulate + // and we get not-quite-zero remaining free space when we should get zero here? + if remaining_free_space != Length::zero() { + if grow { + for (item, target_main_size) in unfrozen_items() { + let grow_factor = item.box_.style().get_position().flex_grow.0; + let ratio = grow_factor / unfrozen_items_flex_factor_sum; + target_main_size.set(item.flex_base_size + remaining_free_space * ratio); + } + } else { + // https://drafts.csswg.org/css-flexbox/#scaled-flex-shrink-factor + let scaled_shrink_factor = |item: &FlexItem| { + let shrink_factor = item.box_.style().get_position().flex_shrink.0; + item.flex_base_size * shrink_factor + }; + let scaled_shrink_factors_sum: Length = unfrozen_items() + .map(|(item, _)| scaled_shrink_factor(item)) + .sum(); + for (item, target_main_size) in unfrozen_items() { + let ratio = scaled_shrink_factor(item) / scaled_shrink_factors_sum; + target_main_size + .set(item.flex_base_size - remaining_free_space.abs() * ratio); + } + } + } + + // “Fix min/max violations.” + let violation = |(item, target_main_size): (&FlexItem, &Cell)| { + let size = target_main_size.get(); + let clamped = size.clamp_between_extremums( + item.content_min_size.main, + item.content_max_size.main, + ); + clamped - size + }; + + // “Freeze over-flexed items.” + let total_violation: Length = unfrozen_items().map(violation).sum(); + if total_violation == Length::zero() { + // “Freeze all items.” + // Return instead, as that’s what the next loop iteration would do. + let remaining_free_space = + container_main_size - target_main_sizes_vec.iter().cloned().sum(); + return (target_main_sizes_vec, remaining_free_space); + } else if total_violation > Length::zero() { + // “Freeze all the items with min violations.” + // “If the item’s target main size was made larger by [clamping], + // it’s a min violation.” + for (item_and_target_main_size, frozen) in items() { + if violation(item_and_target_main_size) > Length::zero() { + frozen_count.set(frozen_count.get() + 1); + frozen.set(true); + } + } + } else { + // Negative total violation + // “Freeze all the items with max violations.” + // “If the item’s target main size was made smaller by [clamping], + // it’s a max violation.” + for (item_and_target_main_size, frozen) in items() { + if violation(item_and_target_main_size) < Length::zero() { + frozen_count.set(frozen_count.get() + 1); + frozen.set(true); + } + } + } + } + } +} + +impl<'a> FlexItem<'a> { + // Return the hypothetical cross size together with laid out contents of the fragment. + // https://drafts.csswg.org/css-flexbox/#algo-cross-item + // “performing layout as if it were an in-flow block-level box + // with the used main size and the given available space, treating `auto` as `fit-content`.” + fn layout( + &mut self, + used_main_size: Length, + flex_context: &mut FlexContext, + ) -> FlexItemLayoutResult { + match flex_context.flex_axis { + FlexAxis::Row => { + // The main axis is the container’s inline axis + + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + flex_context.containing_block.style.writing_mode, + self.box_.style().writing_mode, + "Mixed writing modes are not supported yet" + ); + // … and also the item’s inline axis. + + match self.box_ { + IndependentFormattingContext::Replaced(replaced) => { + let pbm = replaced + .style + .padding_border_margin(flex_context.containing_block); + let size = replaced.contents.used_size_as_if_inline_element( + flex_context.containing_block, + &replaced.style, + &pbm, + ); + let cross_size = flex_context.vec2_to_flex_relative(size.clone()).cross; + let fragments = replaced.contents.make_fragments(&replaced.style, size); + FlexItemLayoutResult { + hypothetical_cross_size: cross_size, + fragments, + } + }, + IndependentFormattingContext::NonReplaced(non_replaced) => { + let item_as_containing_block = ContainingBlock { + inline_size: used_main_size, + block_size: self.content_box_size.cross, + style: &non_replaced.style, + }; + let IndependentLayout { + fragments, + content_block_size, + } = non_replaced.layout( + flex_context.layout_context, + flex_context.positioning_context, + &item_as_containing_block, + self.tree_rank, + ); + FlexItemLayoutResult { + hypothetical_cross_size: content_block_size, + fragments, + } + }, + } + }, + FlexAxis::Column => { + todo!() + // Note: this panic shouldn’t happen since the start of `FlexContainer::layout` + // forces `FlexAxis::Row`. + }, + } + } +} + +impl<'items> FlexLine<'items> { + /// https://drafts.csswg.org/css-flexbox/#algo-cross-line + fn cross_size( + &self, + hypothetical_cross_sizes: impl Iterator, + flex_context: &FlexContext, + ) -> Length { + if flex_context.container_is_single_line { + if let Some(size) = flex_context.container_definite_inner_size.cross { + return size; + } + } + let outer_hypothetical_cross_sizes = hypothetical_cross_sizes + .zip(&*self.items) + .map(|(hypothetical, item)| hypothetical + item.pbm_auto_is_zero.cross); + // FIXME: add support for `align-self: baseline` + // and computing the baseline of flex items. + // https://drafts.csswg.org/css-flexbox/#baseline-participation + let largest = outer_hypothetical_cross_sizes.fold(Length::zero(), Length::max); + if flex_context.container_is_single_line { + largest.clamp_between_extremums( + flex_context.container_min_cross_size, + flex_context.container_max_cross_size, + ) + } else { + largest + } + } + + // Return the main-start and main-end margin of each item in the line, + // with `auto` values resolved. + fn resolve_auto_main_margins( + &self, + remaining_free_space: Length, + ) -> impl Iterator + '_ { + let each_auto_margin = if remaining_free_space > Length::zero() { + let auto_margins_count = self + .items + .iter() + .map(|item| { + item.margin.main_start.is_auto() as u32 + item.margin.main_end.is_auto() as u32 + }) + .sum::(); + if auto_margins_count > 0 { + remaining_free_space / auto_margins_count as f32 + } else { + Length::zero() + } + } else { + Length::zero() + }; + self.items.iter().map(move |item| { + ( + item.margin.main_start.auto_is(|| each_auto_margin), + item.margin.main_end.auto_is(|| each_auto_margin), + ) + }) + } + + /// Return the coordinate of the main-start side of the content area of each item + fn align_along_main_axis<'a>( + &'a self, + item_used_main_sizes: &'a [Length], + item_margins: &'a [FlexRelativeSides], + ) -> impl Iterator + 'a { + // “Align the items along the main-axis” + // FIXME: “per justify-content.” + // For now we hard-code the behavior for `justify-content: flex-start`. + let mut main_position_cursor = Length::zero(); + self.items + .iter() + .zip(item_used_main_sizes) + .zip(item_margins) + .map(move |((item, &main_content_size), margin)| { + main_position_cursor += + margin.main_start + item.border.main_start + item.padding.main_start; + let content_main_start_position = main_position_cursor; + main_position_cursor += main_content_size + + item.padding.main_end + + item.border.main_end + + margin.main_end; + content_main_start_position + }) + } +} + +impl FlexItem<'_> { + /// Return the cross-start and cross-end margin, with `auto` values resolved. + /// https://drafts.csswg.org/css-flexbox/#algo-cross-margins + fn resolve_auto_cross_margins( + &self, + flex_context: &FlexContext, + line_cross_size: Length, + item_cross_content_size: Length, + ) -> (Length, Length) { + let auto_count = match (self.margin.cross_start, self.margin.cross_end) { + (LengthOrAuto::LengthPercentage(start), LengthOrAuto::LengthPercentage(end)) => { + return (start, end); + }, + (LengthOrAuto::Auto, LengthOrAuto::Auto) => 2., + _ => 1., + }; + let outer_size = self.pbm_auto_is_zero.cross + item_cross_content_size; + let available = line_cross_size - outer_size; + let start; + let end; + if available > Length::zero() { + let each_auto_margin = available / auto_count; + start = self.margin.cross_start.auto_is(|| each_auto_margin); + end = self.margin.cross_end.auto_is(|| each_auto_margin); + } else { + // “the block-start or inline-start margin (whichever is in the cross axis)” + // This margin is the cross-end on iff `flex-wrap` is `wrap-reverse`, + // cross-start otherwise. + // We know this because: + // https://drafts.csswg.org/css-flexbox/#flex-wrap-property + // “For the values that are not wrap-reverse, + // the cross-start direction is equivalent to + // either the inline-start or block-start direction of the current writing mode + // (whichever is in the cross axis) + // and the cross-end direction is the opposite direction of cross-start. + // When flex-wrap is wrap-reverse, + // the cross-start and cross-end directions are swapped.” + let flex_wrap = flex_context.containing_block.style.get_position().flex_wrap; + let flex_wrap_reverse = match flex_wrap { + FlexWrap::Nowrap | FlexWrap::Wrap => false, + FlexWrap::WrapReverse => true, + }; + // “if the block-start or inline-start margin (whichever is in the cross axis) is auto, + // set it to zero. Set the opposite margin so that the outer cross size of the item + // equals the cross size of its flex line.” + if flex_wrap_reverse { + start = self.margin.cross_start.auto_is(|| available); + end = self.margin.cross_end.auto_is(Length::zero); + } else { + start = self.margin.cross_start.auto_is(Length::zero); + end = self.margin.cross_end.auto_is(|| available); + } + } + (start, end) + } + + /// Return the coordinate of the cross-start side of the content area + fn align_along_cross_axis(&self, margin: &FlexRelativeSides) -> Length { + let outer_cross_start = + if self.margin.cross_start.is_auto() || self.margin.cross_end.is_auto() { + Length::zero() + } else { + // FIXME: “Align all flex items along the cross-axis per `align-self`” + // For now we hard-code the behavior of `flex-start`: + Length::zero() + }; + outer_cross_start + margin.cross_start + self.border.cross_start + self.padding.cross_start } } diff --git a/components/layout_2020/flexbox/mod.rs b/components/layout_2020/flexbox/mod.rs index 5dd0c476727a..f286acdd690f 100644 --- a/components/layout_2020/flexbox/mod.rs +++ b/components/layout_2020/flexbox/mod.rs @@ -5,9 +5,9 @@ use crate::cell::ArcRefCell; use crate::formatting_contexts::IndependentFormattingContext; use crate::positioned::AbsolutelyPositionedBox; -use crate::sizing::ContentSizes; mod construct; +mod geom; mod layout; #[derive(Debug, Serialize)] @@ -20,9 +20,3 @@ pub(crate) enum FlexLevelBox { FlexItem(IndependentFormattingContext), OutOfFlowAbsolutelyPositionedBox(ArcRefCell), } - -impl FlexContainer { - pub fn inline_content_sizes(&self) -> ContentSizes { - unimplemented!() - } -} diff --git a/components/layout_2020/formatting_contexts.rs b/components/layout_2020/formatting_contexts.rs index 4dcb38952c01..0ec2bf1144c3 100644 --- a/components/layout_2020/formatting_contexts.rs +++ b/components/layout_2020/formatting_contexts.rs @@ -134,6 +134,15 @@ impl IndependentFormattingContext { } } + pub fn inline_content_sizes(&mut self, layout_context: &LayoutContext) -> ContentSizes { + match self { + Self::NonReplaced(inner) => inner + .contents + .inline_content_sizes(layout_context, inner.style.writing_mode), + Self::Replaced(inner) => inner.contents.inline_content_sizes(&inner.style), + } + } + pub fn outer_inline_content_sizes( &mut self, layout_context: &LayoutContext, @@ -159,7 +168,9 @@ impl IndependentFormattingContext { let contents = &non_replaced.contents; sizing::outer_inline_and_percentages(&style, containing_block_writing_mode, || { content_sizes - .get_or_insert_with(|| contents.inline_content_sizes(layout_context, style.writing_mode)) + .get_or_insert_with(|| { + contents.inline_content_sizes(layout_context, style.writing_mode) + }) .clone() }) }, diff --git a/components/layout_2020/geom.rs b/components/layout_2020/geom.rs index 5ec5bc78bf39..ad8166296b27 100644 --- a/components/layout_2020/geom.rs +++ b/components/layout_2020/geom.rs @@ -81,6 +81,20 @@ where } } +impl Sub<&'_ flow_relative::Vec2> for &'_ flow_relative::Vec2 +where + T: Sub + Copy, +{ + type Output = flow_relative::Vec2; + + fn sub(self, other: &'_ flow_relative::Vec2) -> Self::Output { + flow_relative::Vec2 { + inline: self.inline - other.inline, + block: self.block - other.block, + } + } +} + impl AddAssign<&'_ flow_relative::Vec2> for flow_relative::Vec2 where T: AddAssign + Copy, diff --git a/components/style/values/computed/length.rs b/components/style/values/computed/length.rs index 61b8148bb65e..9a7e2cbb1a4d 100644 --- a/components/style/values/computed/length.rs +++ b/components/style/values/computed/length.rs @@ -307,6 +307,12 @@ impl ToCss for CSSPixelLength { } } +impl std::iter::Sum for CSSPixelLength { + fn sum>(iter: I) -> Self { + iter.fold(Length::zero(), Add::add) + } +} + impl Add for CSSPixelLength { type Output = Self; @@ -323,6 +329,15 @@ impl AddAssign for CSSPixelLength { } } +impl Div for CSSPixelLength { + type Output = CSSFloat; + + #[inline] + fn div(self, other: Self) -> CSSFloat { + self.px() / other.px() + } +} + impl Div for CSSPixelLength { type Output = Self;