Bug 1942715: Part 1 - Implement anchor resolution that takes the same code path as percentage resolution. r=firefox-style-system-reviewers,emilio

Differential Revision: https://phabricator.services.mozilla.com/D237094
This commit is contained in:
David Shin
2025-02-11 18:19:54 +00:00
parent d9ae455be0
commit 0e977d0dad
4 changed files with 262 additions and 68 deletions

View File

@@ -111,6 +111,7 @@ impl<ValueType: ColorComponentType> ColorComponent<ValueType> {
/// Resolve a [ColorComponent] into a float. None is "none".
pub fn resolve(&self, origin_color: Option<&AbsoluteColor>) -> Result<Option<ValueType>, ()> {
struct EmptyContext;
Ok(match self {
ColorComponent::None => None,
ColorComponent::Value(value) => Some(value.clone()),
@@ -123,7 +124,7 @@ impl<ValueType: ColorComponentType> ColorComponent<ValueType> {
},
ColorComponent::Calc(node) => {
let Ok(resolved_leaf) = node.resolve_map(
|leaf| {
|leaf, _| {
Ok(match leaf {
Leaf::ColorComponent(channel_keyword) => match origin_color {
Some(origin_color) => {
@@ -136,7 +137,8 @@ impl<ValueType: ColorComponentType> ColorComponent<ValueType> {
l => l.clone(),
})
},
|_| Err(()),
|_, _| Ok(None),
&mut EmptyContext,
) else {
return Err(());
};

View File

@@ -31,7 +31,7 @@ use crate::logical_geometry::PhysicalSide;
use crate::values::animated::{Animate, Context as AnimatedContext, Procedure, ToAnimatedValue, ToAnimatedZero};
use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
use crate::values::generics::calc::{
AnchorPositioningResolver, GenericCalcAnchorFunction, GenericCalcAnchorSizeFunction, CalcUnits,
AnchorPositioningResolver, CalcUnits, GenericCalcAnchorFunction, GenericCalcAnchorSizeFunction,
PositivePercentageBasis,
};
use crate::values::generics::length::AnchorResolutionResult;
@@ -492,7 +492,7 @@ impl LengthPercentage {
match self.unpack() {
Unpacked::Length(l) => l,
Unpacked::Percentage(p) => (basis * p.0).normalized(),
Unpacked::Calc(ref c) => c.resolve(basis),
Unpacked::Calc(ref c) => c.resolve_non_anchor(basis),
}
}
@@ -540,7 +540,7 @@ impl LengthPercentage {
Some(match self.unpack() {
Unpacked::Length(l) => Percentage(l.px() / basis.px()),
Unpacked::Percentage(p) => p,
Unpacked::Calc(ref c) => Percentage(c.resolve(basis).px() / basis.px()),
Unpacked::Calc(ref c) => Percentage(c.resolve_non_anchor(basis).px() / basis.px()),
})
}
@@ -942,41 +942,171 @@ pub struct CalcLengthPercentage {
node: CalcNode,
}
struct ResolveContext {
percentage_used: bool,
anchor_function_used: bool,
}
impl Default for ResolveContext {
fn default() -> Self {
Self {
percentage_used: false,
anchor_function_used: false,
}
}
}
fn leaf_to_output(
leaf: &CalcLengthPercentageLeaf,
basis: Length,
context: &mut ResolveContext,
) -> Result<CalcLengthPercentageLeaf, ()> {
Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf {
context.percentage_used = true;
CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0))
} else {
leaf.clone()
})
}
fn map_node(
node: &CalcNode,
basis: Length,
info: &CalcAnchorFunctionResolutionInfo,
context: &mut ResolveContext,
) -> Result<Option<CalcNode>, ()> {
match node {
CalcNode::Anchor(f) => {
context.anchor_function_used = true;
match f.resolve(info.side, info.position_property) {
AnchorResolutionResult::Invalid => return Err(()),
AnchorResolutionResult::Fallback(fb) => {
let mut inner_context = ResolveContext::default();
// TODO(dshin, bug 1923759): At least for now, fallbacks should always resolve, since they do not contain
// recursive anchor functions.
let resolved = fb
.resolve_map(
|leaf, percentage_used| leaf_to_output(leaf, basis, percentage_used),
|_, _| Ok(None),
&mut inner_context,
)
.expect("anchor() fallback should have been resolvable?");
context.percentage_used |= inner_context.percentage_used;
debug_assert!(
!inner_context.anchor_function_used,
"Nested anchor function used?"
);
Ok(Some(CalcNode::Leaf(resolved)))
},
AnchorResolutionResult::Resolved(v) => Ok(Some(*v.clone())),
}
},
CalcNode::AnchorSize(f) => {
context.anchor_function_used = true;
match f.resolve(info.position_property) {
AnchorResolutionResult::Invalid => return Err(()),
AnchorResolutionResult::Fallback(fb) => {
let mut inner_context = ResolveContext::default();
// TODO(dshin, bug 1923956): Equivalent to corresponding matching arm for `anchor()`.
let resolved = fb
.resolve_map(
|leaf, percentage_used| leaf_to_output(leaf, basis, percentage_used),
|_, _| Ok(None),
&mut inner_context,
)
.expect("anchor-size() fallbaack should have been resolvable?");
context.percentage_used |= inner_context.percentage_used;
debug_assert!(
!inner_context.anchor_function_used,
"Nested anchor function used?"
);
Ok(Some(CalcNode::Leaf(resolved)))
},
AnchorResolutionResult::Resolved(v) => Ok(Some(*v.clone())),
}
},
_ => Ok(None),
}
}
/// Information required for resolving anchor functions.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct CalcAnchorFunctionResolutionInfo {
/// Which side we're resolving anchor functions for.
/// This is only relevant for `anchor()`, which requires
/// the property using the function to be in the same axis
/// as the specified side [1].
/// [1]: https://drafts.csswg.org/css-anchor-position-1/#anchor-valid
pub side: PhysicalSide,
/// `position` property of the box for which this style is being resolved.
pub position_property: PositionProperty,
}
impl CalcAnchorFunctionResolutionInfo {
fn invalid() -> Self {
Self {
// Makes anchor functions always invalid
position_property: PositionProperty::Static,
// Doesn't matter
side: PhysicalSide::Left,
}
}
}
/// Result of resolving `CalcLengthPercentage`
pub struct CalcLengthPercentageResolution {
/// The resolved length.
pub result: Length,
/// Did the resolution of this calc node require resolving percentages?
pub percentage_used: bool,
}
impl CalcLengthPercentage {
/// Resolves the percentage.
/// Resolves the percentage, resolving anchor functions as specified in `resolve`.
pub fn resolve_non_anchor(&self, basis: Length) -> Length {
self.resolve(basis, None)
.expect("Non-anchor calc resolution returned None")
.result
}
/// Resolves the percentage and anchor functions, if provided. Otherwise, anchor functions
/// will be resolved as invalid.
#[inline]
pub fn resolve(&self, basis: Length) -> Length {
// unwrap() is fine because the conversion below is infallible.
if let CalcLengthPercentageLeaf::Length(px) = self
.node
.resolve_map(|leaf| {
Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf {
CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0))
pub fn resolve(
&self,
basis: Length,
anchor_resolution_info: Option<CalcAnchorFunctionResolutionInfo>,
) -> Option<CalcLengthPercentageResolution> {
let mut context = ResolveContext::default();
let info = anchor_resolution_info.unwrap_or(CalcAnchorFunctionResolutionInfo::invalid());
let result = self.node.resolve_map(
|leaf, context| leaf_to_output(leaf, basis, context),
|node, context| map_node(node, basis, &info, context),
&mut context,
);
match result {
Ok(r) => match r {
CalcLengthPercentageLeaf::Length(px) => Some(CalcLengthPercentageResolution{
result: Length::new(self.clamping_mode.clamp(px.px())).normalized(),
percentage_used: context.percentage_used,
}),
_ => unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number"),
},
Err(()) => {
if anchor_resolution_info.is_some() {
None
} else {
leaf.clone()
})
}, |node| {
match node {
CalcNode::Anchor(f) => {
if let Some(fallback) = f.fallback.as_ref() {
return Ok((**fallback).clone());
}
Ok(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(Length::zero())))
},
CalcNode::AnchorSize(f) => {
if let Some(fallback) = f.fallback.as_ref() {
return Ok((**fallback).clone());
}
Ok(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(Length::zero())))
}
_ => Err(()),
// TODO(dshin, bug 1923959): This should be an assert; we can't do that at the moment because size properties with anchor
// functions in calc node end up here. For now, return an invalid-but-reasonable-enough 0.
debug_assert!(context.anchor_function_used, "Anchor function not used but failed resolution?");
Some(CalcLengthPercentageResolution{
result: Length::zero(),
percentage_used: false,
})
}
})
.unwrap()
{
Length::new(self.clamping_mode.clamp(px.px())).normalized()
} else {
unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number");
},
}
}

View File

@@ -912,41 +912,61 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
/// Resolve this node into a value.
pub fn resolve(&self) -> Result<L, ()> {
self.resolve_map(|l| Ok(l.clone()), |_| Err(()))
struct EmptyContext;
self.resolve_map(
|l, _| Ok(l.clone()),
|_, _| Ok(None),
&mut EmptyContext,
)
}
/// Resolve this node into a value, given a function that maps the leaf values.
pub fn resolve_map<F, NF>(&self, mut leaf_to_output_fn: F, mut node_mapping_fn: NF) -> Result<L, ()>
pub fn resolve_map<F, NF, C>(
&self,
mut leaf_to_output_fn: F,
mut node_mapping_fn: NF,
context: &mut C,
) -> Result<L, ()>
where
F: FnMut(&L) -> Result<L, ()>,
NF: FnMut(&CalcNode<L>) -> Result<CalcNode<L>, ()>,
F: FnMut(&L, &mut C) -> Result<L, ()>,
NF: FnMut(&CalcNode<L>, &mut C) -> Result<Option<CalcNode<L>>, ()>,
{
self.resolve_internal(&mut leaf_to_output_fn, &mut node_mapping_fn)
self.resolve_internal(&mut leaf_to_output_fn, &mut node_mapping_fn, context)
}
fn resolve_internal<F, NF>(&self, leaf_to_output_fn: &mut F, node_mapping_fn: &mut NF) -> Result<L, ()>
fn resolve_internal<F, NF, C>(
&self,
leaf_to_output_fn: &mut F,
node_mapping_fn: &mut NF,
context: &mut C,
) -> Result<L, ()>
where
F: FnMut(&L) -> Result<L, ()>,
NF: FnMut(&CalcNode<L>) -> Result<CalcNode<L>, ()>,
F: FnMut(&L, &mut C) -> Result<L, ()>,
NF: FnMut(&CalcNode<L>, &mut C) -> Result<Option<CalcNode<L>>, ()>,
{
let node = node_mapping_fn(self).ok();
match node.as_ref().unwrap_or(self) {
Self::Leaf(l) => leaf_to_output_fn(l),
let result = node_mapping_fn(self, context)?;
let node = result.as_ref().unwrap_or(self);
match node {
Self::Leaf(l) => leaf_to_output_fn(l, context),
Self::Negate(child) => {
let mut result = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
result.map(|v| v.neg())?;
Ok(result)
},
Self::Invert(child) => {
let mut result = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
result.map(|v| 1.0 / v)?;
Ok(result)
},
Self::Sum(children) => {
let mut result = children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
for child in children.iter().skip(1) {
let right = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let right =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
// try_op will make sure we only sum leaves with the same type.
result = result.try_op(&right, |left, right| left + right)?;
}
@@ -954,10 +974,12 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
Ok(result)
},
Self::Product(children) => {
let mut result = children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
for child in children.iter().skip(1) {
let right = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let right =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
// Mutliply only allowed when either side is a number.
match result.as_number() {
Some(left) => {
@@ -983,14 +1005,16 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
Ok(result)
},
Self::MinMax(children, op) => {
let mut result = children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
if result.is_nan()? {
return Ok(result);
}
for child in children.iter().skip(1) {
let candidate = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let candidate =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
// Leaf types must match for each child.
if !result.is_same_unit_as(&candidate) {
@@ -1015,9 +1039,10 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
Ok(result)
},
Self::Clamp { min, center, max } => {
let min = min.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let center = center.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let max = max.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let min = min.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
let center =
center.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
let max = max.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
if !min.is_same_unit_as(&center) || !max.is_same_unit_as(&center) {
return Err(());
@@ -1050,8 +1075,9 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
value,
step,
} => {
let mut value = value.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let step = step.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut value =
value.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
let step = step.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
if !value.is_same_unit_as(&step) {
return Err(());
@@ -1136,8 +1162,10 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
divisor,
op,
} => {
let mut dividend = dividend.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let divisor = divisor.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut dividend =
dividend.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
let divisor =
divisor.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
if !dividend.is_same_unit_as(&divisor) {
return Err(());
@@ -1150,11 +1178,13 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
Ok(dividend)
},
Self::Hypot(children) => {
let mut result = children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result =
children[0].resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
result.map(|v| v.powi(2))?;
for child in children.iter().skip(1) {
let child_value = child.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let child_value =
child.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
if !result.is_same_unit_as(&child_value) {
return Err(());
@@ -1170,14 +1200,14 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
Ok(result)
},
Self::Abs(ref c) => {
let mut result = c.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let mut result = c.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
result.map(|v| v.abs())?;
Ok(result)
},
Self::Sign(ref c) => {
let result = c.resolve_internal(leaf_to_output_fn, node_mapping_fn)?;
let result = c.resolve_internal(leaf_to_output_fn, node_mapping_fn, context)?;
Ok(L::sign_from(&result)?)
},
Self::Anchor(_) | Self::AnchorSize(_) => Err(()),

View File

@@ -16,7 +16,6 @@ use selectors::matching::{ElementSelectorFlags, MatchingForInvalidation, Selecto
use selectors::{Element, OpaqueElement};
use servo_arc::{Arc, ArcBorrow};
use smallvec::SmallVec;
use style::values::generics::length::AnchorResolutionResult;
use std::collections::BTreeSet;
use std::fmt::Write;
use std::iter;
@@ -152,11 +151,13 @@ use style::values::computed::font::{
FamilyName, FontFamily, FontFamilyList, FontStretch, FontStyle, FontWeight, GenericFontFamily,
};
use style::values::computed::length::AnchorSizeFunction;
use style::values::computed::length_percentage::CalcAnchorFunctionResolutionInfo;
use style::values::computed::position::AnchorFunction;
use style::values::computed::{self, Context, PositionProperty, ToComputedValue};
use style::values::distance::ComputeSquaredDistance;
use style::values::generics::color::ColorMixFlags;
use style::values::generics::easing::BeforeFlag;
use style::values::generics::length::AnchorResolutionResult;
use style::values::resolved;
use style::values::specified::gecko::IntersectionObserverRootMargin;
use style::values::specified::source_size_list::SourceSizeList;
@@ -8359,7 +8360,38 @@ pub extern "C" fn Servo_ResolveCalcLengthPercentage(
calc: &computed::length_percentage::CalcLengthPercentage,
basis: f32,
) -> f32 {
calc.resolve(computed::Length::new(basis)).px()
calc.resolve(computed::Length::new(basis), None)
.unwrap()
.result
.px()
}
#[no_mangle]
pub extern "C" fn Servo_ResolveCalcLengthPercentageWithAnchorFunctions(
calc: &computed::length_percentage::CalcLengthPercentage,
basis: f32,
side: PhysicalSide,
position_property: PositionProperty,
result: &mut f32,
percentage_used: &mut bool,
) -> bool {
let resolved = calc.resolve(
computed::Length::new(basis),
Some(CalcAnchorFunctionResolutionInfo {
side,
position_property,
}),
);
let resolved = match resolved {
None => return false,
Some(v) => v,
};
*result = resolved.result.px();
*percentage_used = resolved.percentage_used;
true
}
#[no_mangle]