commit fc08f7bd773bed1c5ea4afb7dc7365107238551c parent d7ca8234cec177461791e4b3f241c1d5b0262fdb Author: Emilio Cobos Álvarez <emilio@crisal.io> Date: Wed, 29 Oct 2025 20:02:54 +0000 Bug 1994674 - Adjust margins / insets for flips. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,dshin The flip-start logic is still not quite right, but the set-up is easy. Differential Revision: https://phabricator.services.mozilla.com/D270350 Diffstat:
12 files changed, 219 insertions(+), 197 deletions(-)
diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs @@ -1628,9 +1628,25 @@ pub enum PhysicalSide { } impl PhysicalSide { - fn orthogonal_to(self, other: Self) -> bool { + /// Returns whether one physical side is parallel to another. + pub fn parallel_to(self, other: Self) -> bool { + !self.orthogonal_to(other) + } + + /// Returns whether one physical side is orthogonal to another. + pub fn orthogonal_to(self, other: Self) -> bool { matches!(self, Self::Top | Self::Bottom) != matches!(other, Self::Top | Self::Bottom) } + + /// Returns the opposite side. + pub fn opposite_side(self) -> Self { + match self { + Self::Top => Self::Bottom, + Self::Right => Self::Left, + Self::Bottom => Self::Top, + Self::Left => Self::Right, + } + } } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs @@ -18,10 +18,10 @@ use crate::properties::longhands::{ overflow_x::computed_value::T as Overflow, }; use crate::properties::{self, ComputedValues, StyleBuilder}; -use crate::values::specified::align::AlignFlags; -use crate::values::specified::position::{ - PositionTryFallbacksTryTactic, PositionTryFallbacksTryTacticKeyword, +use crate::values::computed::position::{ + PositionTryFallbacksTryTactic, PositionTryFallbacksTryTacticKeyword, TryTacticAdjustment, }; +use crate::values::specified::align::AlignFlags; #[cfg(feature = "gecko")] use selectors::parser::PseudoElement; @@ -985,8 +985,8 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { if a == b { return; } - let a = a.clone(); - let b = b.clone(); + let a = a.clone().try_tactic_adjustment(a_side, b_side); + let b = b.clone().try_tactic_adjustment(b_side, a_side); let pos = self.style.mutate_position(); pos.set_inset(a_side, b); pos.set_inset(b_side, a); @@ -1000,8 +1000,8 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { if a == b { return; } - let a = a.clone(); - let b = b.clone(); + let a = a.clone().try_tactic_adjustment(a_side, b_side); + let b = b.clone().try_tactic_adjustment(b_side, a_side); let margin = self.style.mutate_margin(); margin.set_margin(a_side, b); margin.set_margin(b_side, a); @@ -1049,44 +1049,11 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { fn flip_insets_and_margins(&mut self, horizontal: bool) { if horizontal { - // TODO: Avoid the clone here? - flip_property!( - self, - get_position, - mutate_position, - clone_left, - set_left, - clone_right, - set_right - ); - flip_property!( - self, - get_margin, - mutate_margin, - clone_margin_left, - set_margin_left, - clone_margin_right, - set_margin_right - ); + self.swap_insets(PhysicalSide::Left, PhysicalSide::Right); + self.swap_margins(PhysicalSide::Left, PhysicalSide::Right); } else { - flip_property!( - self, - get_position, - mutate_position, - clone_top, - set_top, - clone_bottom, - set_bottom - ); - flip_property!( - self, - get_margin, - mutate_margin, - clone_margin_top, - set_margin_top, - clone_margin_bottom, - set_margin_bottom - ); + self.swap_insets(PhysicalSide::Top, PhysicalSide::Bottom); + self.swap_margins(PhysicalSide::Top, PhysicalSide::Bottom); } } diff --git a/servo/components/style/values/computed/length.rs b/servo/components/style/values/computed/length.rs @@ -5,8 +5,10 @@ //! `<length>` computed values, and related ones. use super::{Context, Number, ToComputedValue}; +use crate::logical_geometry::PhysicalSide; use crate::values::animated::{Context as AnimatedContext, ToAnimatedValue}; -use crate::values::computed::{NonNegativeNumber, Zoom}; +use crate::values::computed::position::TryTacticAdjustment; +use crate::values::computed::{NonNegativeNumber, Percentage, Zoom}; use crate::values::generics::length as generics; use crate::values::generics::length::{ GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, @@ -558,3 +560,37 @@ pub fn resolve_anchor_size( /// A computed type for `margin` properties. pub type Margin = generics::GenericMargin<LengthPercentage>; + +impl TryTacticAdjustment for Percentage { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + if old_side.parallel_to(new_side) { + return Self(1.0 - self.0) + } + self + } +} + +impl TryTacticAdjustment for LengthPercentage { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + if let Some(p) = self.to_percentage() { + return Self::new_percent(p.try_tactic_adjustment(old_side, new_side)); + } + self + } +} + +impl TryTacticAdjustment for Margin { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + match self { + Self::Auto => self, + Self::LengthPercentage(lp) => { + Self::LengthPercentage(lp.try_tactic_adjustment(old_side, new_side)) + }, + Self::AnchorSizeFunction(anchor) => { + Self::AnchorSizeFunction(anchor.try_tactic_adjustment(old_side, new_side)) + }, + // TODO + Self::AnchorContainingCalcFunction(..) => self, + } + } +} diff --git a/servo/components/style/values/computed/position.rs b/servo/components/style/values/computed/position.rs @@ -21,7 +21,8 @@ use crate::values::generics::position::{AspectRatio as GenericAspectRatio, Gener pub use crate::values::specified::position::{ AnchorName, AnchorScope, DashedIdentAndOrTryTactic, PositionAnchor, PositionArea, PositionAreaAxis, PositionAreaKeyword, PositionAreaType, PositionTryFallbacks, - PositionTryOrder, PositionVisibility, + PositionTryFallbacksTryTactic, PositionTryFallbacksTryTacticKeyword, PositionTryOrder, + PositionVisibility, }; pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow}; use crate::Zero; @@ -101,8 +102,69 @@ impl AnchorFunction { } } +/// Perform the adjustment of a given value for a given try tactic, as per: +/// https://drafts.csswg.org/css-anchor-position-1/#swap-due-to-a-try-tactic +pub(crate) trait TryTacticAdjustment { + /// Performs the adjustments necessary given an old side we're relative to, and a new side + /// we're relative to. + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self; +} + +impl<T: TryTacticAdjustment> TryTacticAdjustment for Box<T> { + fn try_tactic_adjustment(mut self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + *self = (*self).try_tactic_adjustment(old_side, new_side); + self + } +} + +impl TryTacticAdjustment for GenericAnchorSide<Percentage> { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + match self { + Self::Percentage(p) => Self::Percentage(p.try_tactic_adjustment(old_side, new_side)), + Self::Keyword(side) => Self::Keyword(side.try_tactic_adjustment(old_side, new_side)), + } + } +} + +impl<Fallback: TryTacticAdjustment> TryTacticAdjustment + for GenericAnchorFunction<Percentage, Fallback> +{ + fn try_tactic_adjustment(mut self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + self.side = self.side.try_tactic_adjustment(old_side, new_side); + self.fallback = self + .fallback + .map(|f| f.try_tactic_adjustment(old_side, new_side)); + self + } +} + /// A computed type for `inset` properties. pub type Inset = GenericInset<Percentage, LengthPercentage>; +impl TryTacticAdjustment for Inset { + // https://drafts.csswg.org/css-anchor-position-1/#swap-due-to-a-try-tactic: + // + // For inset properties, change the specified side in anchor() functions to maintain the + // same relative relationship to the new direction that they had to the old. + // + // If a <percentage> is used, and directions are opposing, change it to 100% minus the + // original percentage. + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + match self { + Self::Auto => self, + Self::LengthPercentage(lp) => { + Self::LengthPercentage(lp.try_tactic_adjustment(old_side, new_side)) + }, + Self::AnchorFunction(anchor) => { + Self::AnchorFunction(anchor.try_tactic_adjustment(old_side, new_side)) + }, + Self::AnchorSizeFunction(anchor) => { + Self::AnchorSizeFunction(anchor.try_tactic_adjustment(old_side, new_side)) + }, + // TODO + Self::AnchorContainingCalcFunction(..) => self, + } + } +} impl Position { /// `50% 50%` diff --git a/servo/components/style/values/generics/length.rs b/servo/components/style/values/generics/length.rs @@ -4,7 +4,9 @@ //! Generic types for CSS values related to length. +use crate::logical_geometry::PhysicalSide; use crate::parser::{Parse, ParserContext}; +use crate::values::computed::position::TryTacticAdjustment; use crate::values::generics::box_::PositionProperty; use crate::values::generics::Optional; use crate::values::DashedIdent; @@ -402,6 +404,16 @@ pub struct GenericAnchorSizeFunction<Fallback> { pub fallback: Optional<Fallback>, } +impl<Fallback: TryTacticAdjustment> TryTacticAdjustment for GenericAnchorSizeFunction<Fallback> { + fn try_tactic_adjustment(mut self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + self.size = self.size.try_tactic_adjustment(old_side, new_side); + self.fallback = self + .fallback + .map(|f| f.try_tactic_adjustment(old_side, new_side)); + self + } +} + impl<Fallback> ToCss for GenericAnchorSizeFunction<Fallback> where Fallback: ToCss, @@ -554,6 +566,23 @@ pub enum AnchorSizeKeyword { SelfInline, } +impl TryTacticAdjustment for AnchorSizeKeyword { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + if old_side.parallel_to(new_side) { + return self; + } + match self { + Self::None => Self::None, + Self::Width => Self::Height, + Self::Height => Self::Width, + Self::Block => Self::Inline, + Self::Inline => Self::Block, + Self::SelfBlock => Self::SelfInline, + Self::SelfInline => Self::SelfBlock, + } + } +} + /// Specified type for `margin` properties, which allows /// the use of the `anchor-size()` function. #[derive( diff --git a/servo/components/style/values/generics/mod.rs b/servo/components/style/values/generics/mod.rs @@ -267,6 +267,14 @@ impl<T> Optional<T> { Self::None => None, } } + + /// See Option::map. + pub fn map<U>(self, map: impl FnOnce(T) -> U) -> Optional<U> { + match self { + Self::Some(v) => Optional::Some(map(v)), + Self::None => Optional::None, + } + } } impl<T> From<Option<T>> for Optional<T> { diff --git a/servo/components/style/values/generics/position.rs b/servo/components/style/values/generics/position.rs @@ -13,6 +13,7 @@ use style_traits::ToCss; use crate::logical_geometry::PhysicalSide; use crate::values::animated::ToAnimatedZero; +use crate::values::computed::position::TryTacticAdjustment; use crate::values::generics::box_::PositionProperty; use crate::values::generics::length::GenericAnchorSizeFunction; use crate::values::generics::ratio::Ratio; @@ -440,6 +441,59 @@ pub enum AnchorSideKeyword { } impl AnchorSideKeyword { + fn from_physical_side(side: PhysicalSide) -> Self { + match side { + PhysicalSide::Top => Self::Top, + PhysicalSide::Right => Self::Right, + PhysicalSide::Bottom => Self::Bottom, + PhysicalSide::Left => Self::Left, + } + } + + fn physical_side(self) -> Option<PhysicalSide> { + Some(match self { + Self::Top => PhysicalSide::Top, + Self::Right => PhysicalSide::Right, + Self::Bottom => PhysicalSide::Bottom, + Self::Left => PhysicalSide::Left, + _ => return None, + }) + } +} + +impl TryTacticAdjustment for AnchorSideKeyword { + fn try_tactic_adjustment(self, old_side: PhysicalSide, new_side: PhysicalSide) -> Self { + if !old_side.parallel_to(new_side) { + if let Some(s) = self.physical_side() { + return Self::from_physical_side(if s == new_side { + old_side + } else if s == old_side { + new_side + } else if s == new_side.opposite_side() { + old_side.opposite_side() + } else { + debug_assert_eq!(s, old_side.opposite_side()); + new_side.opposite_side() + }); + } + return self; + } + + match self { + Self::Center | Self::Inside | Self::Outside => self, + Self::SelfStart => Self::SelfEnd, + Self::SelfEnd => Self::SelfStart, + Self::Start => Self::End, + Self::End => Self::Start, + Self::Top => Self::Bottom, + Self::Bottom => Self::Top, + Self::Left => Self::Right, + Self::Right => Self::Left, + } + } +} + +impl AnchorSideKeyword { fn valid_for(&self, side: PhysicalSide) -> bool { match self { Self::Left | Self::Right => matches!(side, PhysicalSide::Left | PhysicalSide::Right), diff --git a/testing/web-platform/meta/css/css-anchor-position/last-successful-pseudo-element-basic.html.ini b/testing/web-platform/meta/css/css-anchor-position/last-successful-pseudo-element-basic.html.ini @@ -1,9 +1,3 @@ [last-successful-pseudo-element-basic.html] - [Starts rendering with flip-block] - expected: FAIL - - [No successful position, keep flip-block] - expected: FAIL - [Base position without fallback now successful] expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/last-successful-pseudo-element-fallbacks.html.ini b/testing/web-platform/meta/css/css-anchor-position/last-successful-pseudo-element-fallbacks.html.ini @@ -1,9 +1,3 @@ [last-successful-pseudo-element-fallbacks.html] - [Starts rendering with flip-block] - expected: FAIL - - [No successful position, keep flip-block] - expected: FAIL - [No successful position, last successful invalidated by position-try-fallbacks change] expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/try-tactic-anchor.html.ini b/testing/web-platform/meta/css/css-anchor-position/try-tactic-anchor.html.ini @@ -7,84 +7,3 @@ [flip-start flip-block] expected: FAIL - - [Can transform a value post-var-substitution] - expected: FAIL - - [flip-start, right:anchor(left), bottom:anchor(top)] - expected: FAIL - - [flip-start, bottom:anchor(top), right:anchor(left)] - expected: FAIL - - [flip-inline flip-start, right:anchor(left), top:anchor(bottom)] - expected: FAIL - - [flip-start flip-inline, top:anchor(bottom), right:anchor(left)] - expected: FAIL - - [flip-start flip-block, left:anchor(right), bottom:anchor(top)] - expected: FAIL - - [flip-block flip-start, bottom:anchor(top), left:anchor(right)] - expected: FAIL - - [flip-start, left:anchor(right), top:anchor(bottom)] - expected: FAIL - - [flip-start, top:anchor(bottom), left:anchor(right)] - expected: FAIL - - [flip-block, bottom:anchor(top), top:anchor(bottom)] - expected: FAIL - - [flip-block, top:anchor(bottom), bottom:anchor(top)] - expected: FAIL - - [flip-inline, right:anchor(start), left:anchor(right)] - expected: FAIL - - [flip-inline, left:anchor(end), right:anchor(left)] - expected: FAIL - - [flip-inline flip-start, right:anchor(start), top:anchor(bottom)] - expected: FAIL - - [flip-start flip-inline, top:anchor(end), right:anchor(left)] - expected: FAIL - - [flip-start flip-block, left:anchor(end), bottom:anchor(top)] - expected: FAIL - - [flip-block flip-start, bottom:anchor(start), left:anchor(right)] - expected: FAIL - - [flip-block, bottom:anchor(start), top:anchor(bottom)] - expected: FAIL - - [flip-block, top:anchor(end), bottom:anchor(top)] - expected: FAIL - - [flip-inline, right:anchor(self-start), left:anchor(right)] - expected: FAIL - - [flip-inline, left:anchor(self-end), right:anchor(left)] - expected: FAIL - - [flip-inline flip-start, right:anchor(self-start), top:anchor(bottom)] - expected: FAIL - - [flip-start flip-inline, top:anchor(self-end), right:anchor(left)] - expected: FAIL - - [flip-start flip-block, left:anchor(self-end), bottom:anchor(top)] - expected: FAIL - - [flip-block flip-start, bottom:anchor(self-start), left:anchor(right)] - expected: FAIL - - [flip-block, bottom:anchor(self-start), top:anchor(bottom)] - expected: FAIL - - [flip-block, top:anchor(self-end), bottom:anchor(top)] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/try-tactic-basic-anchor.html.ini b/testing/web-platform/meta/css/css-anchor-position/try-tactic-basic-anchor.html.ini @@ -4,6 +4,3 @@ [Uses flip-inline] expected: FAIL - - [Uses flip-block flip-inline] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/try-tactic-percentage.html.ini b/testing/web-platform/meta/css/css-anchor-position/try-tactic-percentage.html.ini @@ -1,54 +0,0 @@ -[try-tactic-percentage.html] - [flip-inline, left:anchor(10%), right:anchor(90%)] - expected: FAIL - - [flip-inline, left:anchor(calc(10% + 20%)), right:anchor(70%)] - expected: FAIL - - [flip-inline, left:anchor(0%), right:anchor(100%)] - expected: FAIL - - [flip-inline, left:anchor(100%), right:anchor(0%)] - expected: FAIL - - [flip-inline, top:anchor(0%), top:anchor(0%)] - expected: FAIL - - [flip-inline, top:anchor(100%), top:anchor(100%)] - expected: FAIL - - [flip-block flip-inline, left:anchor(0%), right:anchor(100%)] - expected: FAIL - - [flip-block flip-inline, left:anchor(100%), right:anchor(0%)] - expected: FAIL - - [flip-block flip-inline, top:anchor(0%), bottom:anchor(100%)] - expected: FAIL - - [flip-block flip-inline, top:anchor(100%), bottom:anchor(0%)] - expected: FAIL - - [flip-inline flip-start, left:anchor(0%), bottom:anchor(100%)] - expected: FAIL - - [flip-inline flip-start, left:anchor(100%), bottom:anchor(0%)] - expected: FAIL - - [flip-inline flip-start, bottom:anchor(0%), right:anchor(0%)] - expected: FAIL - - [flip-inline flip-start, bottom:anchor(100%), right:anchor(100%)] - expected: FAIL - - [flip-block flip-inline flip-start, left:anchor(0%), bottom:anchor(100%)] - expected: FAIL - - [flip-block flip-inline flip-start, left:anchor(100%), bottom:anchor(0%)] - expected: FAIL - - [flip-block flip-inline flip-start, bottom:anchor(0%), left:anchor(100%)] - expected: FAIL - - [flip-block flip-inline flip-start, bottom:anchor(100%), left:anchor(0%)] - expected: FAIL