commit b74fd7c18961f2afe1d4b27a316278ae82de82be
parent da87d9d332051ebd8fcb702be7e6d1c5fa0b7d89
Author: Emilio Cobos Álvarez <emilio@crisal.io>
Date: Thu, 16 Oct 2025 20:35:26 +0000
Bug 1994673 - Adjust self-alignment properties for position-try tactics. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,dshin
I think StyleAdjuster is ok / nice for this, because it doesn't
interfere with anything else, and given we're not doing the interleaving
thing of resolving anchor() to pixels at style time, we can get away
with it for those properties even.
It felt cleaner than doing the property-swapping in one place and the
value-flipping in another. If we need to do that, it might be worth
figuring out the "final" transform upfront, since otherwise it gets
tricky as we add more properties.
Differential Revision: https://phabricator.services.mozilla.com/D268876
Diffstat:
5 files changed, 139 insertions(+), 97 deletions(-)
diff --git a/servo/components/style/properties/cascade.rs b/servo/components/style/properties/cascade.rs
@@ -384,7 +384,7 @@ where
if matches!(cascade_mode, CascadeMode::Unvisited { .. }) {
StyleAdjuster::new(&mut context.builder)
- .adjust(layout_parent_style.unwrap_or(inherited_style), element);
+ .adjust(layout_parent_style.unwrap_or(inherited_style), element, try_tactic);
}
if context.builder.modified_reset() || using_cached_reset_properties {
diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs
@@ -17,6 +17,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,
+};
#[cfg(feature = "gecko")]
use selectors::parser::PseudoElement;
@@ -800,7 +804,7 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
let parent_justify_items = self.style.get_parent_position().clone_justify_items();
- if !parent_justify_items.computed.contains(align::AlignFlags::LEGACY) {
+ if !parent_justify_items.computed.contains(AlignFlags::LEGACY) {
return;
}
@@ -904,14 +908,83 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
}
}
- /// Adjusts the style to account for various fixups that don't fit naturally
- /// into the cascade.
+ /// Performs adjustments for position-try-fallbacks. The properties that need adjustments here
+ /// are luckily not affected by previous adjustments nor by other computed-value-time effects,
+ /// so we can just perform them here.
///
- /// When comparing to Gecko, this is similar to the work done by
- /// `ComputedStyle::ApplyStyleFixups`, plus some parts of
- /// `nsStyleSet::GetContext`.
- pub fn adjust<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
- where
+ /// NOTE(emilio): If we ever perform the interleaving dance, this could / should probably move
+ /// around to the specific properties' to_computed_value implementations, but that seems
+ /// overkill for now.
+ fn adjust_for_try_tactic(&mut self, tactic: PositionTryFallbacksTryTactic) {
+ debug_assert!(!tactic.is_empty());
+ // TODO: Flip inset / margin / sizes percentages and anchor lookup sides as necessary.
+ for tactic in tactic.into_iter() {
+ match tactic {
+ PositionTryFallbacksTryTacticKeyword::None => break,
+ PositionTryFallbacksTryTacticKeyword::FlipBlock => {
+ self.flip_self_alignment(/* block = */ true);
+ },
+ PositionTryFallbacksTryTacticKeyword::FlipInline => {
+ self.flip_self_alignment(/* block = */ false);
+ },
+ PositionTryFallbacksTryTacticKeyword::FlipStart => {
+ self.flip_alignment_start();
+ },
+ }
+ }
+ }
+
+ fn flip_alignment_start(&mut self) {
+ let pos = self.style.get_position();
+ let align = pos.clone_align_self();
+ let mut justify = pos.clone_justify_self();
+ if align == justify {
+ return;
+ }
+
+ // Fix-up potential justify-self: {left, right} values which might end up as alignment
+ // values.
+ if matches!(justify.value(), AlignFlags::LEFT | AlignFlags::RIGHT) {
+ let left = justify.value() == AlignFlags::LEFT;
+ let ltr = self.style.writing_mode.is_bidi_ltr();
+ justify = justify.with_value(if left == ltr {
+ AlignFlags::SELF_START
+ } else {
+ AlignFlags::SELF_END
+ });
+ }
+
+ let pos = self.style.mutate_position();
+ pos.set_align_self(justify);
+ pos.set_justify_self(align);
+ }
+
+ fn flip_self_alignment(&mut self, block: bool) {
+ let pos = self.style.get_position();
+ let cur = if block {
+ pos.clone_align_self()
+ } else {
+ pos.clone_justify_self()
+ };
+ let flipped = cur.flip_position();
+ if flipped == cur {
+ return;
+ }
+ let pos = self.style.mutate_position();
+ if block {
+ pos.set_align_self(flipped);
+ } else {
+ pos.set_justify_self(flipped);
+ }
+ }
+
+ /// Adjusts the style to account for various fixups that don't fit naturally into the cascade.
+ pub fn adjust<E>(
+ &mut self,
+ layout_parent_style: &ComputedValues,
+ element: Option<E>,
+ try_tactic: PositionTryFallbacksTryTactic,
+ ) where
E: TElement,
{
if cfg!(debug_assertions) {
@@ -969,6 +1042,9 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
self.adjust_for_appearance(element);
self.adjust_for_marker_pseudo();
}
+ if !try_tactic.is_empty() {
+ self.adjust_for_try_tactic(try_tactic);
+ }
self.set_bits();
}
}
diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs
@@ -78,6 +78,13 @@ impl AlignFlags {
*self & !AlignFlags::FLAG_BITS
}
+ /// Returns an updated value with the same flags.
+ #[inline]
+ pub fn with_value(&self, value: AlignFlags) -> Self {
+ debug_assert!(!value.intersects(Self::FLAG_BITS));
+ value | self.flags()
+ }
+
/// Returns the flags stored in the upper 3 bits.
#[inline]
pub fn flags(&self) -> Self {
@@ -90,10 +97,9 @@ impl ToCss for AlignFlags {
where
W: Write,
{
- let extra_flags = *self & AlignFlags::FLAG_BITS;
+ let flags = self.flags();
let value = self.value();
-
- match extra_flags {
+ match flags {
AlignFlags::LEGACY => {
dest.write_str("legacy")?;
if value.is_empty() {
@@ -104,7 +110,7 @@ impl ToCss for AlignFlags {
AlignFlags::SAFE => dest.write_str("safe ")?,
AlignFlags::UNSAFE => dest.write_str("unsafe ")?,
_ => {
- debug_assert_eq!(extra_flags, AlignFlags::empty());
+ debug_assert_eq!(flags, AlignFlags::empty());
},
}
@@ -365,6 +371,43 @@ impl SelfAlignment {
list_overflow_position_keywords(f);
list_self_position_keywords(f, axis);
}
+
+ /// Performs a flip of the position, that is, for self-start we return self-end, for left
+ /// we return right, etc.
+ pub fn flip_position(self) -> Self {
+ let flipped_value = match self.0.value() {
+ AlignFlags::START => AlignFlags::END,
+ AlignFlags::END => AlignFlags::START,
+ AlignFlags::FLEX_START => AlignFlags::FLEX_END,
+ AlignFlags::FLEX_END => AlignFlags::FLEX_START,
+ AlignFlags::LEFT => AlignFlags::RIGHT,
+ AlignFlags::RIGHT => AlignFlags::LEFT,
+ AlignFlags::SELF_START => AlignFlags::SELF_END,
+ AlignFlags::SELF_END => AlignFlags::SELF_START,
+
+ AlignFlags::AUTO |
+ AlignFlags::NORMAL |
+ AlignFlags::BASELINE |
+ AlignFlags::LAST_BASELINE |
+ AlignFlags::STRETCH |
+ AlignFlags::CENTER |
+ AlignFlags::SPACE_BETWEEN |
+ AlignFlags::SPACE_AROUND |
+ AlignFlags::SPACE_EVENLY |
+ AlignFlags::ANCHOR_CENTER => return self,
+ _ => {
+ debug_assert!(false, "Unexpected alignment enumeration value");
+ return self;
+ }
+ };
+ self.with_value(flipped_value)
+ }
+
+ /// Returns a fixed-up alignment value.
+ #[inline]
+ pub fn with_value(self, value: AlignFlags) -> Self {
+ Self(self.0.with_value(value))
+ }
}
impl SpecifiedValueInfo for SelfAlignment {
diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs
@@ -609,11 +609,6 @@ impl PositionTryFallbacksTryTactic {
fn flip_start(id: LonghandId, wm: WritingMode) -> LonghandId {
use LonghandId::*;
let (physical_side, is_margin) = match id {
- // TODO(emilio): Needs some work to use the same cascade implementation for these.
- // Also to make justify-self: left | right map to align-self properly.
- // AlignSelf => return JustifySelf,
- // JustifySelf => return AlignSelf,
-
Width => return Height,
Height => return Width,
MinWidth => return MinHeight,
@@ -641,13 +636,19 @@ impl PositionTryFallbacksTryTactic {
}
}
+ /// Iterates over the fallbacks in order.
+ #[inline]
+ pub fn into_iter(&self) -> impl IntoIterator<Item = PositionTryFallbacksTryTacticKeyword> {
+ [self.0, self.1, self.2]
+ }
+
/// Applies a try tactic to a given property.
pub fn apply_to_property(&self, mut id: LonghandId, wm: WritingMode) -> LonghandId {
debug_assert!(!id.is_logical(), "Logical props should've been replaced already");
debug_assert!(!self.is_empty(), "Should have something to do");
// TODO(emilio): Consider building a LonghandIdSet to check for unaffected properties, and
// bailing out earlier?
- for tactic in [self.0, self.1, self.2] {
+ for tactic in self.into_iter() {
id = match tactic {
PositionTryFallbacksTryTacticKeyword::None => break,
PositionTryFallbacksTryTacticKeyword::FlipInline |
diff --git a/testing/web-platform/meta/css/css-anchor-position/try-tactic-alignment.html.ini b/testing/web-platform/meta/css/css-anchor-position/try-tactic-alignment.html.ini
@@ -1,78 +0,0 @@
-[try-tactic-alignment.html]
- [flip-inline, justify-self:start]
- expected: FAIL
-
- [flip-inline, justify-self:end]
- expected: FAIL
-
- [flip-inline, justify-self:self-start]
- expected: FAIL
-
- [flip-inline, justify-self:self-end]
- expected: FAIL
-
- [flip-inline, justify-self:flex-start]
- expected: FAIL
-
- [flip-inline, justify-self:flex-end]
- expected: FAIL
-
- [flip-inline, justify-self:left]
- expected: FAIL
-
- [flip-inline, justify-self:right]
- expected: FAIL
-
- [flip-block, align-self:start]
- expected: FAIL
-
- [flip-block, align-self:end]
- expected: FAIL
-
- [flip-block, align-self:self-start]
- expected: FAIL
-
- [flip-block, align-self:self-end]
- expected: FAIL
-
- [flip-block, align-self:flex-start]
- expected: FAIL
-
- [flip-block, align-self:flex-end]
- expected: FAIL
-
- [flip-inline, justify-self:start;align-self:start, justify-self:end;align-self:start, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-block, justify-self:start;align-self:start, justify-self:start;align-self:end, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-block flip-inline, justify-self:start;align-self:start, justify-self:end;align-self:end, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-start, justify-self:start;align-self:end, justify-self:end;align-self:start, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-block flip-start, justify-self:start;align-self:start, justify-self:end;align-self:start, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-inline flip-start, justify-self:start;align-self:start, justify-self:start;align-self:end, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-block flip-inline flip-start, justify-self:start;align-self:start, justify-self:end;align-self:end, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-inline, justify-self:left;align-self:start, justify-self:right;align-self:start, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-start, justify-self:left;align-self:end, justify-self:end;align-self:self-start, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-start, justify-self:right;align-self:start, justify-self:start;align-self:self-end, ltr, horizontal-tb]
- expected: FAIL
-
- [flip-start, justify-self:left;align-self:end, justify-self:end;align-self:self-start, ltr, vertical-rl]
- expected: FAIL
-
- [flip-start, justify-self:left;align-self:start, justify-self:start;align-self:self-end, rtl, horizontal-tb]
- expected: FAIL