From cea21bbd4aacaa86057fa8e33604d7a7d02c5c34 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Wed, 3 Apr 2019 15:30:11 +0200 Subject: [PATCH] Implement absolute positioning Shrink-to-fit size is missing, absolute positioning in inline formatting contexts is missing. There are probably off-by-some-amount-of-pixels bugs lying around. --- tests/reftests/abspos-ref.html | 6 + tests/reftests/abspos.html | 7 + victor/src/geom.rs | 15 + victor/src/layout/boxes/generation.rs | 71 +++- victor/src/layout/boxes/mod.rs | 4 + victor/src/layout/fragments/mod.rs | 26 +- victor/src/layout/mod.rs | 375 ++++++++++++++++++++- victor/src/style/properties/definitions.rs | 5 + victor/src/style/properties/mod.rs | 10 + victor/src/style/values/box_.rs | 41 ++- victor/src/style/values/length.rs | 9 + 11 files changed, 537 insertions(+), 32 deletions(-) create mode 100644 tests/reftests/abspos-ref.html create mode 100644 tests/reftests/abspos.html diff --git a/tests/reftests/abspos-ref.html b/tests/reftests/abspos-ref.html new file mode 100644 index 0000000..6a13707 --- /dev/null +++ b/tests/reftests/abspos-ref.html @@ -0,0 +1,6 @@ + + +
+
+
+ diff --git a/tests/reftests/abspos.html b/tests/reftests/abspos.html new file mode 100644 index 0000000..51cf323 --- /dev/null +++ b/tests/reftests/abspos.html @@ -0,0 +1,7 @@ + + + +
+
+
+ diff --git a/victor/src/geom.rs b/victor/src/geom.rs index 7d70da5..3f48e96 100644 --- a/victor/src/geom.rs +++ b/victor/src/geom.rs @@ -60,6 +60,7 @@ where } } } + impl physical::Vec2 { pub fn size_to_flow_relative(&self, mode: (WritingMode, Direction)) -> flow_relative::Vec2 { // https://drafts.csswg.org/css-writing-modes/#logical-to-physical @@ -75,6 +76,20 @@ impl physical::Vec2 { } } +impl Add<&'_ flow_relative::Vec2> for &'_ flow_relative::Vec2 +where + T: Add + Copy, +{ + type Output = flow_relative::Vec2; + + fn add(self, other: &'_ flow_relative::Vec2) -> Self::Output { + flow_relative::Vec2 { + inline: self.inline + other.inline, + block: self.block + other.block, + } + } +} + impl flow_relative::Vec2 { pub fn size_to_physical(&self, mode: (WritingMode, Direction)) -> physical::Vec2 { // https://drafts.csswg.org/css-writing-modes/#logical-to-physical diff --git a/victor/src/layout/boxes/generation.rs b/victor/src/layout/boxes/generation.rs index 13648fd..32553ce 100644 --- a/victor/src/layout/boxes/generation.rs +++ b/victor/src/layout/boxes/generation.rs @@ -2,7 +2,7 @@ use super::*; use crate::dom; use crate::fonts::BITSTREAM_VERA_SANS; use crate::layout::Take; -use crate::style::values::{Display, DisplayInside, DisplayOutside}; +use crate::style::values::{Display, DisplayInside, DisplayOutside, Position}; use crate::style::{style_for_element, StyleSet, StyleSetBuilder}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; @@ -38,6 +38,10 @@ enum IntermediateBlockLevelBox { style: Arc, contents: IntermediateBlockContainer, }, + AbsolutelyPositionedBox { + style: Arc, + element: dom::NodeId, + }, } /// A block container that may still have to be constructed. @@ -216,16 +220,28 @@ impl<'a> BlockContainerBuilder<'a> { descendant, parent_style.map(|style| &**style), ); - match descendant_style.box_.display { - Display::None => self.move_to_next_sibling(descendant), - Display::Other { - outside: DisplayOutside::Inline, - inside: DisplayInside::Flow, - } => self.handle_inline_level_element(descendant, descendant_style), - Display::Other { - outside: DisplayOutside::Block, - inside: DisplayInside::Flow, - } => self.handle_block_level_element(descendant, descendant_style), + match ( + descendant_style.box_.display, + descendant_style.box_.position, + ) { + (Display::None, _) => self.move_to_next_sibling(descendant), + (_, Position::Absolute) => { + self.handle_absolutely_positioned_element(descendant, descendant_style) + } + ( + Display::Other { + outside: DisplayOutside::Inline, + inside: DisplayInside::Flow, + }, + _, + ) => self.handle_inline_level_element(descendant, descendant_style), + ( + Display::Other { + outside: DisplayOutside::Block, + inside: DisplayInside::Flow, + }, + _, + ) => self.handle_block_level_element(descendant, descendant_style), } } @@ -318,6 +334,29 @@ impl<'a> BlockContainerBuilder<'a> { self.move_to_next_sibling(descendant) } + fn handle_absolutely_positioned_element( + &mut self, + descendant: dom::NodeId, + descendant_style: Arc, + ) -> Option { + if self + .ongoing_inline_formatting_context + .inline_level_boxes + .is_empty() + && self.ongoing_inline_level_box_stack.is_empty() + { + self.block_level_boxes + .push(IntermediateBlockLevelBox::AbsolutelyPositionedBox { + style: descendant_style, + element: descendant, + }) + } else { + // FIXME(nox): Handle absolutely-positioned elements in + // inline boxes. + } + self.move_to_next_sibling(descendant) + } + fn move_to_next_sibling(&mut self, descendant: dom::NodeId) -> Option { let mut descendant_node = &self.context.document[descendant]; if let Some(next_sibling) = descendant_node.next_sibling { @@ -411,6 +450,16 @@ impl IntermediateBlockLevelBox { style, } } + IntermediateBlockLevelBox::AbsolutelyPositionedBox { style, element } => { + BlockLevelBox::AbsolutelyPositionedBox { + contents: BlockFormattingContext(BlockContainerBuilder::build( + context, + element, + Some(&style), + )), + style: style, + } + } } } } diff --git a/victor/src/layout/boxes/mod.rs b/victor/src/layout/boxes/mod.rs index 45c0918..23fb042 100644 --- a/victor/src/layout/boxes/mod.rs +++ b/victor/src/layout/boxes/mod.rs @@ -31,6 +31,10 @@ pub(super) enum BlockLevelBox { style: Arc, contents: BlockContainer, }, + AbsolutelyPositionedBox { + style: Arc, + contents: BlockFormattingContext, + }, // Other { // style: Arc, // contents: FormattingContext, diff --git a/victor/src/layout/fragments/mod.rs b/victor/src/layout/fragments/mod.rs index 98e8bff..b5944ac 100644 --- a/victor/src/layout/fragments/mod.rs +++ b/victor/src/layout/fragments/mod.rs @@ -1,4 +1,4 @@ -use crate::geom::flow_relative::{Rect, Sides}; +use crate::geom::flow_relative::{Rect, Sides, Vec2}; use crate::geom::Length; use crate::style::ComputedValues; use crate::text::ShapedSegment; @@ -30,6 +30,30 @@ pub(crate) struct TextFragment { } impl BoxFragment { + pub fn zero_sized(style: Arc) -> Self { + let zero_vec = Vec2 { + inline: Length::zero(), + block: Length::zero(), + }; + let zero_sides = Sides { + inline_start: Length::zero(), + inline_end: Length::zero(), + block_start: Length::zero(), + block_end: Length::zero(), + }; + Self { + style, + children: vec![], + content_rect: Rect { + start_corner: zero_vec.clone(), + size: zero_vec, + }, + padding: zero_sides.clone(), + border: zero_sides.clone(), + margin: zero_sides, + } + } + pub fn border_rect(&self) -> Rect { self.content_rect .inflate(&self.padding) diff --git a/victor/src/layout/mod.rs b/victor/src/layout/mod.rs index fb00fec..c1ec088 100644 --- a/victor/src/layout/mod.rs +++ b/victor/src/layout/mod.rs @@ -3,11 +3,12 @@ pub(crate) mod fragments; use self::boxes::*; use self::fragments::*; -use crate::geom::flow_relative::{Rect, Vec2}; +use crate::geom::flow_relative::{Rect, Sides, Vec2}; use crate::geom::Length; use crate::style::values::{Direction, LengthOrPercentage, LengthOrPercentageOrAuto, WritingMode}; use crate::style::ComputedValues; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use parking_lot::Mutex; +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use std::sync::Arc; impl crate::dom::Document { @@ -19,43 +20,82 @@ impl crate::dom::Document { layout_document(&box_tree, viewport) } } + fn layout_document( box_tree: &BoxTreeRoot, viewport: crate::primitives::Size, ) -> Vec { + let inline_size = Length { px: viewport.width }; + // FIXME: use the document’s mode: // https://drafts.csswg.org/css-writing-modes/#principal-flow let initial_containing_block = ContainingBlock { - inline_size: Length { px: viewport.width }, + inline_start_from_absolute_containing_block: Length::zero(), + inline_size, block_size: Some(Length { px: viewport.height, }), mode: (WritingMode::HorizontalTb, Direction::Ltr), }; - let (fragments, _) = box_tree.layout(&initial_containing_block); + let (mut fragments, absolutely_positioned_fragments, _) = + box_tree.layout(&initial_containing_block, &initial_containing_block, 0); + fragments.extend( + absolutely_positioned_fragments + .into_iter() + .map(|fragment| Fragment::Box(fragment.contents)), + ); fragments } +struct AbsoluteContext<'a> { + containing_block: &'a ContainingBlock, + absolutely_positioned_fragments: Mutex>, +} + +struct AbsolutelyPositionedFragment { + index: usize, + uses_static_block_position: bool, + contents: BoxFragment, +} + struct ContainingBlock { + inline_start_from_absolute_containing_block: Length, inline_size: Length, block_size: Option, mode: (WritingMode, Direction), } impl BlockFormattingContext { - fn layout(&self, containing_block: &ContainingBlock) -> (Vec, Length) { - self.0.layout(containing_block) + fn layout( + &self, + absolute_containing_block: &ContainingBlock, + containing_block: &ContainingBlock, + index: usize, + ) -> (Vec, Vec, Length) { + self.0 + .layout(absolute_containing_block, containing_block, index) } } impl BlockContainer { - fn layout(&self, containing_block: &ContainingBlock) -> (Vec, Length) { + fn layout( + &self, + absolute_containing_block: &ContainingBlock, + containing_block: &ContainingBlock, + index: usize, + ) -> (Vec, Vec, Length) { match self { BlockContainer::BlockLevelBoxes(child_boxes) => { + let mut absolute_context = AbsoluteContext { + containing_block: absolute_containing_block, + absolutely_positioned_fragments: Default::default(), + }; + let mut child_fragments = child_boxes .par_iter() - .map(|child| child.layout(containing_block)) + .enumerate() + .map(|(index, child)| child.layout(&absolute_context, containing_block, index)) .collect::>(); let mut block_size = Length::zero(); @@ -72,27 +112,70 @@ impl BlockContainer { + child.content_rect.size.block; } - (child_fragments, block_size) + let mut absolutely_positioned_fragments = absolute_context + .absolutely_positioned_fragments + .get_mut() + .take(); + absolutely_positioned_fragments.sort_by_key(|fragment| fragment.index); + for abspos_fragment in &mut absolutely_positioned_fragments { + if abspos_fragment.uses_static_block_position { + let child_fragment = match &child_fragments[abspos_fragment.index] { + Fragment::Box(b) => b, + _ => unreachable!(), + }; + abspos_fragment.contents.content_rect.start_corner.block += + child_fragment.content_rect.start_corner.block; + } + abspos_fragment.index = index; + } + + (child_fragments, absolutely_positioned_fragments, block_size) + } + BlockContainer::InlineFormattingContext(ifc) => { + let (child_fragments, block_size) = ifc.layout(containing_block); + // FIXME(nox): Handle abspos in inline. + (child_fragments, vec![], block_size) } - BlockContainer::InlineFormattingContext(ifc) => ifc.layout(containing_block), } } } impl BlockLevelBox { - fn layout(&self, containing_block: &ContainingBlock) -> Fragment { + fn layout( + &self, + absolute_context: &AbsoluteContext, + containing_block: &ContainingBlock, + index: usize, + ) -> Fragment { match self { BlockLevelBox::SameFormattingContextBlock { style, contents } => { - same_formatting_context_block(style, contents, containing_block) + same_formatting_context_block( + absolute_context, + containing_block, + index, + style, + contents, + ) + } + BlockLevelBox::AbsolutelyPositionedBox { style, contents } => { + absolutely_positioned_box( + absolute_context, + containing_block, + index, + style, + contents, + ) } } } } fn same_formatting_context_block( - style: &Arc, - contents: &BlockContainer, + absolute_context: &AbsoluteContext, containing_block: &ContainingBlock, + index: usize, + style: &Arc, + contents: &boxes::BlockContainer, ) -> Fragment { let cbis = containing_block.inline_size; let zero = Length::zero(); @@ -142,6 +225,8 @@ fn same_formatting_context_block( LengthOrPercentage::Percentage(p) => containing_block.block_size.map(|cbbs| cbbs * p), }); let containing_block_for_children = ContainingBlock { + inline_start_from_absolute_containing_block: pbm.inline_start + + containing_block.inline_start_from_absolute_containing_block, inline_size, block_size, mode: style.writing_mode(), @@ -151,7 +236,18 @@ fn same_formatting_context_block( containing_block.mode, containing_block_for_children.mode, "Mixed writing modes are not supported yet" ); - let (children, content_block_size) = contents.layout(&containing_block_for_children); + let (children, absolutely_positioned_fragments, content_block_size) = contents.layout( + absolute_context.containing_block, + &containing_block_for_children, + index, + ); + if !absolutely_positioned_fragments.is_empty() { + absolute_context + .absolutely_positioned_fragments + .lock() + .extend(absolutely_positioned_fragments); + } + let block_size = block_size.unwrap_or(content_block_size); let content_rect = Rect { start_corner: pbm.start_corner(), @@ -170,6 +266,255 @@ fn same_formatting_context_block( }) } +fn absolutely_positioned_box( + absolute_context: &AbsoluteContext, + containing_block: &ContainingBlock, + index: usize, + style: &Arc, + contents: &BlockFormattingContext, +) -> Fragment { + let cbis = absolute_context.containing_block.inline_size; + let padding = style.padding().map(|v| v.percentage_relative_to(cbis)); + let border = style.border_width().map(|v| v.percentage_relative_to(cbis)); + let pb = &padding + &border; + let box_size = style.box_size(); + + let computed_physical_margin = style + .physical_margin() + .map(|v| v.non_auto().map(|v| v.percentage_relative_to(cbis))); + let computed_margin = style + .margin() + .map(|v| v.non_auto().map(|v| v.percentage_relative_to(cbis))); + + let computed_inline_size = box_size + .inline + .non_auto() + .map(|v| v.percentage_relative_to(cbis)); + let computed_block_size = box_size.block.non_auto().and_then(|b| match b { + LengthOrPercentage::Length(l) => Some(l), + LengthOrPercentage::Percentage(p) => absolute_context + .containing_block + .block_size + .map(|cbbs| cbbs * p), + }); + + struct Solution { + margin_start: Length, + margin_end: Length, + strategy: Strategy, + } + + enum Strategy { + FromStart { + start: Option, + size: Option, + }, + FromEnd { + end: Length, + }, + } + + fn solve_axis( + containing_block_inline_size: Length, + physical_start: Option, + physical_end: Option, + computed_margin_start: Option, + computed_margin_end: Option, + solve_margins: impl FnOnce(Length) -> (Length, Length), + padding_border_sum: Length, + size: Option, + ) -> Solution { + let cbis_minus_pb = containing_block_inline_size - padding_border_sum; + let zero = Length::zero(); + + let mut margin_start = computed_margin_start.unwrap_or(zero); + let mut margin_end = computed_margin_end.unwrap_or(zero); + + let strategy = match (physical_start, size, physical_end) { + (start, size, None) => Strategy::FromStart { start, size }, + (Some(start), Some(size), Some(end)) => { + let margins = cbis_minus_pb - start - size - end; + + match (computed_margin_start, computed_margin_end) { + (None, None) => { + let (s, e) = solve_margins(margins); + margin_start = s; + margin_end = e; + } + (None, Some(end)) => { + margin_start = margins - end; + margin_end = end; + } + (Some(start), _) => { + margin_start = start; + margin_end = margins - start; + } + } + + Strategy::FromStart { + start: Some(start), + size: Some(size), + } + } + (None, None, Some(end)) => Strategy::FromEnd { end }, + (None, Some(size), Some(end)) => { + let start = cbis_minus_pb - size - end - margin_start - margin_end; + Strategy::FromStart { + start: Some(start), + size: Some(size), + } + } + (Some(start), None, Some(end)) => { + // FIXME(nox): Wait, what happens when that is negative? + let size = cbis_minus_pb - start - end - margin_start - margin_end; + Strategy::FromStart { + start: Some(start), + size: Some(size), + } + } + }; + + Solution { + margin_start, + margin_end, + strategy, + } + } + + // https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-width + let inline_solution = solve_axis( + cbis, + computed_physical_margin.inline_start, + computed_physical_margin.inline_end, + computed_margin.inline_start, + computed_margin.inline_end, + |margins| { + if margins.px >= 0. { + (margins / 2., margins / 2.) + } else { + (Length::zero(), margins) + } + }, + pb.inline_sum(), + computed_inline_size, + ); + + let inline_size; + let inline_start; + match inline_solution.strategy { + Strategy::FromStart { start, size } => { + inline_start = + start.unwrap_or(containing_block.inline_start_from_absolute_containing_block); + inline_size = size.unwrap_or_else(|| { + let available_size = + cbis - inline_start - inline_solution.margin_start - inline_solution.margin_end; + // FIXME(nox): shrink-to-fit inline size. + available_size + }); + } + Strategy::FromEnd { end } => { + inline_start = Length::zero(); + let available_size = + cbis - end - inline_solution.margin_start - inline_solution.margin_end; + // FIXME(nox): shrink-to-fit inline size. + inline_size = available_size; + } + } + + // https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-height + let block_solution = solve_axis( + cbis, + computed_physical_margin.block_start, + computed_physical_margin.block_end, + computed_margin.block_start, + computed_margin.block_end, + |margins| (margins / 2., margins / 2.), + pb.block_sum(), + computed_block_size, + ); + + let block_size = match block_solution.strategy { + Strategy::FromStart { size, .. } => size, + Strategy::FromEnd { .. } => None, + }; + + let containing_block_for_children = ContainingBlock { + inline_start_from_absolute_containing_block: Length::zero(), + inline_size, + block_size, + mode: style.writing_mode(), + }; + + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + absolute_context.containing_block.mode, containing_block.mode, + "Mixed writing modes are not supported yet" + ); + assert_eq!( + containing_block.mode, containing_block_for_children.mode, + "Mixed writing modes are not supported yet" + ); + + let (mut children, absolutely_positioned_fragments, content_block_size) = contents.layout( + &containing_block_for_children, + &containing_block_for_children, + 0, + ); + children.extend( + absolutely_positioned_fragments + .into_iter() + .map(|fragment| Fragment::Box(fragment.contents)), + ); + + let block_size = block_size.unwrap_or(content_block_size); + let (block_start, uses_static_block_position) = match block_solution.strategy { + Strategy::FromStart { start: None, .. } => (Length::zero(), true), + Strategy::FromStart { + start: Some(start), .. + } => (start, false), + Strategy::FromEnd { end } => (cbis - end - block_size, false), + }; + + let margin_start_corner = Vec2 { + block: block_start, + inline: inline_start, + }; + let margin = Sides { + inline_start: inline_solution.margin_start, + inline_end: inline_solution.margin_end, + block_start: block_solution.margin_start, + block_end: block_solution.margin_end, + }; + let pbm = &pb + &margin; + + let content_rect = Rect { + start_corner: &margin_start_corner + &pbm.start_corner(), + size: Vec2 { + block: block_size, + inline: inline_size, + }, + }; + + let absolutely_positioned_fragment = AbsolutelyPositionedFragment { + index, + uses_static_block_position, + contents: BoxFragment { + style: style.clone(), + children, + content_rect, + padding, + border, + margin, + }, + }; + absolute_context + .absolutely_positioned_fragments + .lock() + .push(absolutely_positioned_fragment); + + Fragment::Box(BoxFragment::zero_sized(style.clone())) +} + struct InlineFormattingContextLayoutState<'a> { remaining_boxes: std::slice::Iter<'a, InlineLevelBox>, fragments_so_far: Vec, diff --git a/victor/src/style/properties/definitions.rs b/victor/src/style/properties/definitions.rs index ba0039a..8984a82 100644 --- a/victor/src/style/properties/definitions.rs +++ b/victor/src/style/properties/definitions.rs @@ -14,6 +14,7 @@ properties! { } reset struct box_ { + @early position { "position", Position, initial = Position::Static } display { "display", Display, @@ -22,6 +23,10 @@ properties! { inside: DisplayInside::Flow, } } + top { "top", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } + left { "left", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } + bottom { "bottom", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } + right { "right", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } width { "width", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } height { "height", LengthOrPercentageOrAuto, initial = LengthOrPercentageOrAuto::Auto } } diff --git a/victor/src/style/properties/mod.rs b/victor/src/style/properties/mod.rs index 4b03ac1..41ba0ca 100644 --- a/victor/src/style/properties/mod.rs +++ b/victor/src/style/properties/mod.rs @@ -32,6 +32,16 @@ impl ComputedValues { (WritingMode::HorizontalTb, Direction::Ltr) } + pub(crate) fn physical_margin(&self) -> flow_relative::Sides { + physical::Sides { + top: self.box_.top, + left: self.box_.left, + bottom: self.box_.bottom, + right: self.box_.right, + } + .to_flow_relative(self.writing_mode()) + } + pub(crate) fn box_size(&self) -> flow_relative::Vec2 { physical::Vec2 { x: self.box_.width, diff --git a/victor/src/style/values/box_.rs b/victor/src/style/values/box_.rs index 58b77d2..b2f84ea 100644 --- a/victor/src/style/values/box_.rs +++ b/victor/src/style/values/box_.rs @@ -1,9 +1,9 @@ use crate::style::errors::PropertyParseError; -use crate::style::values::Parse; +use crate::style::values::{CascadeContext, FromSpecified, SpecifiedValue}; use cssparser::Parser; /// https://drafts.csswg.org/css-display-3/#the-display-properties -#[derive(Copy, Clone, SpecifiedAsComputed)] +#[derive(Copy, Clone, Eq, PartialEq)] pub(crate) enum Display { None, Other { @@ -12,18 +12,42 @@ pub(crate) enum Display { }, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] pub(crate) enum DisplayOutside { Inline, Block, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] pub(crate) enum DisplayInside { Flow, } -impl Parse for Display { +impl SpecifiedValue for Display { + type SpecifiedValue = Display; +} + +impl FromSpecified for Display { + /// https://drafts.csswg.org/css2/visuren.html#dis-pos-flo + fn from_specified(specified: &Display, context: &CascadeContext) -> Self { + match (specified, context.this.position()) { + (Display::None, _) => Display::None, + ( + Display::Other { + outside: DisplayOutside::Inline, + inside, + }, + Position::Absolute, + ) => Display::Other { + outside: DisplayOutside::Block, + inside: *inside, + }, + (other, _) => *other, + } + } +} + +impl super::Parse for Display { fn parse<'i, 't>(parser: &mut Parser<'i, 't>) -> Result> { let ident = parser.expect_ident()?; match &**ident { @@ -43,3 +67,10 @@ impl Parse for Display { } } } + +/// https://drafts.csswg.org/css-position-3/#position-property +#[derive(Copy, Clone, Eq, Parse, PartialEq, SpecifiedAsComputed)] +pub(crate) enum Position { + Static, + Absolute, +} diff --git a/victor/src/style/values/length.rs b/victor/src/style/values/length.rs index 31eb729..38de414 100644 --- a/victor/src/style/values/length.rs +++ b/victor/src/style/values/length.rs @@ -187,6 +187,15 @@ impl LengthOrPercentage { } } +impl From for LengthOrPercentageOrAuto { + fn from(value: LengthOrPercentage) -> Self { + match value { + LengthOrPercentage::Length(l) => LengthOrPercentageOrAuto::Length(l), + LengthOrPercentage::Percentage(p) => LengthOrPercentageOrAuto::Percentage(p), + } + } +} + impl LengthOrPercentageOrAuto { pub(crate) fn auto_is(&self, auto_value: impl FnOnce() -> Length) -> LengthOrPercentage { match *self {