Implement the aspect-ratio property for replaced elements (#32800)

* Add WPT tests for box-sizing with aspect-ratio

Signed-off-by: valadaptive <valadaptive@protonmail.com>

* Implement `aspect-ratio` for replaced elements

There are two regressions because we don't implement `object-fit`, and
one because we don't properly represent non-available <img>s with `alt`
attributes.

Signed-off-by: valadaptive <valadaptive@protonmail.com>

---------

Signed-off-by: valadaptive <valadaptive@protonmail.com>
This commit is contained in:
valadaptive 2024-07-24 07:40:23 -04:00 committed by GitHub
parent e425ad0cb7
commit 60e65c175d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 314 additions and 62 deletions

View file

@ -14,6 +14,7 @@ use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder};
use pixels::Image;
use serde::Serialize;
use servo_arc::Arc as ServoArc;
use style::logical_geometry::Direction;
use style::properties::ComputedValues;
use style::servo::url::ComputedUrl;
use style::values::computed::image::Image as ComputedImage;
@ -359,12 +360,18 @@ impl ReplacedContent {
) -> LogicalVec2<Au> {
let mode = style.writing_mode;
let intrinsic_size = self.flow_relative_intrinsic_size(style);
let intrinsic_ratio = self.inline_size_over_block_size_intrinsic_ratio(style);
let intrinsic_ratio = style.preferred_aspect_ratio(
self.inline_size_over_block_size_intrinsic_ratio(style),
containing_block,
);
let box_size = box_size.unwrap_or(
style
.content_box_size(containing_block, pbm)
.map(|v| v.map(Au::from)),
// We need to clamp to zero here to obtain the proper aspect
// ratio when box-sizing is border-box and the inner box size
// would otherwise be negative.
.map(|v| v.map(|v| Au::from(v).max(Au::zero()))),
);
let max_box_size = style
.content_max_box_size(containing_block, pbm)
@ -393,8 +400,8 @@ impl ReplacedContent {
LogicalVec2 { inline, block }
},
(AuOrAuto::LengthPercentage(inline), AuOrAuto::Auto) => {
let block = if let Some(i_over_b) = intrinsic_ratio {
inline.scale_by(1.0 / i_over_b)
let block = if let Some(ratio) = intrinsic_ratio {
ratio.compute_dependent_size(Direction::Block, inline)
} else if let Some(block) = intrinsic_size.block {
block
} else {
@ -403,8 +410,8 @@ impl ReplacedContent {
LogicalVec2 { inline, block }
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(block)) => {
let inline = if let Some(i_over_b) = intrinsic_ratio {
block.scale_by(i_over_b)
let inline = if let Some(ratio) = intrinsic_ratio {
ratio.compute_dependent_size(Direction::Inline, block)
} else if let Some(inline) = intrinsic_size.inline {
inline
} else {
@ -416,10 +423,10 @@ impl ReplacedContent {
let inline_size =
match (intrinsic_size.inline, intrinsic_size.block, intrinsic_ratio) {
(Some(inline), _, _) => inline,
(None, Some(block), Some(i_over_b)) => {
(None, Some(block), Some(ratio)) => {
// “used height” in CSS 2 is always gonna be the intrinsic one,
// since it is available.
block.scale_by(i_over_b)
ratio.compute_dependent_size(Direction::Inline, block)
},
// FIXME
//
@ -442,9 +449,9 @@ impl ReplacedContent {
};
let block_size = if let Some(block) = intrinsic_size.block {
block
} else if let Some(i_over_b) = intrinsic_ratio {
} else if let Some(ratio) = intrinsic_ratio {
// “used width” in CSS 2 is what we just computed above
inline_size.scale_by(1.0 / i_over_b)
ratio.compute_dependent_size(Direction::Block, inline_size)
} else {
default_object_size().block
};
@ -459,7 +466,7 @@ impl ReplacedContent {
// https://drafts.csswg.org/css2/visudet.html#min-max-widths
// “However, for replaced elements with an intrinsic ratio and both
// 'width' and 'height' specified as 'auto', the algorithm is as follows”
if let (AuOrAuto::Auto, AuOrAuto::Auto, Some(i_over_b)) =
if let (AuOrAuto::Auto, AuOrAuto::Auto, Some(ratio)) =
(box_size.inline, box_size.block, intrinsic_ratio)
{
let LogicalVec2 {
@ -495,26 +502,28 @@ impl ReplacedContent {
// Row 2.
(Violation::Above(max_inline_size), Violation::None) => LogicalVec2 {
inline: max_inline_size,
block: max_inline_size
.scale_by(1.0 / i_over_b)
block: ratio
.compute_dependent_size(Direction::Block, max_inline_size)
.max(min_box_size.block),
},
// Row 3.
(Violation::Below(min_inline_size), Violation::None) => LogicalVec2 {
inline: min_inline_size,
block: min_inline_size
.scale_by(1.0 / i_over_b)
block: ratio
.compute_dependent_size(Direction::Block, min_inline_size)
.clamp_below_max(max_box_size.block),
},
// Row 4.
(Violation::None, Violation::Above(max_block_size)) => LogicalVec2 {
inline: max_block_size.scale_by(i_over_b).max(min_box_size.inline),
inline: ratio
.compute_dependent_size(Direction::Inline, max_block_size)
.max(min_box_size.inline),
block: max_block_size,
},
// Row 5.
(Violation::None, Violation::Below(min_block_size)) => LogicalVec2 {
inline: min_block_size
.scale_by(i_over_b)
inline: ratio
.compute_dependent_size(Direction::Inline, min_block_size)
.clamp_below_max(max_box_size.inline),
block: min_block_size,
},
@ -524,14 +533,16 @@ impl ReplacedContent {
// Row 6.
LogicalVec2 {
inline: max_inline_size,
block: max_inline_size
.scale_by(1.0 / i_over_b)
block: ratio
.compute_dependent_size(Direction::Block, max_inline_size)
.max(min_box_size.block),
}
} else {
// Row 7.
LogicalVec2 {
inline: max_block_size.scale_by(i_over_b).max(min_box_size.inline),
inline: ratio
.compute_dependent_size(Direction::Inline, max_block_size)
.max(min_box_size.inline),
block: max_block_size,
}
}
@ -541,8 +552,8 @@ impl ReplacedContent {
if min_inline_size.0 * block_size.0 <= min_block_size.0 * inline_size.0 {
// Row 8.
LogicalVec2 {
inline: min_block_size
.scale_by(i_over_b)
inline: ratio
.compute_dependent_size(Direction::Inline, min_block_size)
.clamp_below_max(max_box_size.inline),
block: min_block_size,
}
@ -550,8 +561,8 @@ impl ReplacedContent {
// Row 9.
LogicalVec2 {
inline: min_inline_size,
block: min_inline_size
.scale_by(1.0 / i_over_b)
block: ratio
.compute_dependent_size(Direction::Block, min_inline_size)
.clamp_below_max(max_box_size.block),
}
}

View file

@ -6,7 +6,7 @@ use app_units::Au;
use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode;
use style::computed_values::position::T as ComputedPosition;
use style::computed_values::transform_style::T as ComputedTransformStyle;
use style::logical_geometry::WritingMode;
use style::logical_geometry::{Direction, WritingMode};
use style::properties::longhands::backface_visibility::computed_value::T as BackfaceVisiblity;
use style::properties::longhands::box_sizing::computed_value::T as BoxSizing;
use style::properties::longhands::column_span::computed_value::T as ColumnSpan;
@ -15,7 +15,9 @@ use style::values::computed::image::Image as ComputedImageLayer;
use style::values::computed::{Length, LengthPercentage, NonNegativeLengthPercentage, Size};
use style::values::generics::box_::Perspective;
use style::values::generics::length::MaxSize;
use style::values::generics::position::{GenericAspectRatio, PreferredRatio};
use style::values::specified::{box_ as stylo, Overflow};
use style::values::CSSFloat;
use style::Zero;
use webrender_api as wr;
@ -143,6 +145,43 @@ impl PaddingBorderMargin {
}
}
/// Resolved `aspect-ratio` property with respect to a specific element. Depends
/// on that element's `box-sizing` (and padding and border, if that `box-sizing`
/// is `border-box`).
#[derive(Clone, Copy, Debug)]
pub(crate) struct AspectRatio {
/// If the element that this aspect ratio belongs to uses box-sizing:
/// border-box, and the aspect-ratio property does not contain "auto", then
/// the aspect ratio is in respect to the border box. This will then contain
/// the summed sizes of the padding and border. Otherwise, it's 0.
box_sizing_adjustment: LogicalVec2<Au>,
/// The ratio itself (inline over block).
i_over_b: CSSFloat,
}
impl AspectRatio {
/// Given one side length, compute the other one.
pub(crate) fn compute_dependent_size(
&self,
ratio_dependent_axis: Direction,
ratio_determining_size: Au,
) -> Au {
match ratio_dependent_axis {
// Calculate the inline size from the block size
Direction::Inline => {
(ratio_determining_size + self.box_sizing_adjustment.block).scale_by(self.i_over_b) -
self.box_sizing_adjustment.inline
},
// Calculate the block size from the inline size
Direction::Block => {
(ratio_determining_size + self.box_sizing_adjustment.inline)
.scale_by(1.0 / self.i_over_b) -
self.box_sizing_adjustment.block
},
}
}
}
pub(crate) trait ComputedValuesExt {
fn box_offsets(
&self,
@ -198,6 +237,11 @@ pub(crate) trait ComputedValuesExt {
&self,
fragment_flags: FragmentFlags,
) -> bool;
fn preferred_aspect_ratio(
&self,
natural_aspect_ratio: Option<CSSFloat>,
containing_block: &ContainingBlock,
) -> Option<AspectRatio>;
fn background_is_transparent(&self) -> bool;
fn get_webrender_primitive_flags(&self) -> wr::PrimitiveFlags;
}
@ -535,6 +579,75 @@ impl ComputedValuesExt for ComputedValues {
false
}
/// Resolve the preferred aspect ratio according to the given natural aspect
/// ratio and the `aspect-ratio` property.
/// See <https://drafts.csswg.org/css-sizing-4/#aspect-ratio>.
fn preferred_aspect_ratio(
&self,
natural_aspect_ratio: Option<CSSFloat>,
containing_block: &ContainingBlock,
) -> Option<AspectRatio> {
let GenericAspectRatio {
auto,
ratio: mut preferred_ratio,
} = self.clone_aspect_ratio();
// For all cases where a ratio is specified:
// "If the <ratio> is degenerate, the property instead behaves as auto."
if matches!(preferred_ratio, PreferredRatio::Ratio(ratio) if ratio.is_degenerate()) {
preferred_ratio = PreferredRatio::None;
}
match (auto, preferred_ratio) {
// The value `auto`. Either the ratio was not specified, or was
// degenerate and set to PreferredRatio::None above.
//
// "Replaced elements with a natural aspect ratio use that aspect
// ratio; otherwise the box has no preferred aspect ratio. Size
// calculations involving the aspect ratio work with the content box
// dimensions always."
(_, PreferredRatio::None) => natural_aspect_ratio.map(|natural_ratio| AspectRatio {
i_over_b: natural_ratio,
box_sizing_adjustment: LogicalVec2::zero(),
}),
// "If both auto and a <ratio> are specified together, the preferred
// aspect ratio is the specified ratio of width / height unless it
// is a replaced element with a natural aspect ratio, in which case
// that aspect ratio is used instead. In all cases, size
// calculations involving the aspect ratio work with the content box
// dimensions always."
(true, PreferredRatio::Ratio(preferred_ratio)) => match natural_aspect_ratio {
Some(natural_ratio) => Some(AspectRatio {
i_over_b: natural_ratio,
box_sizing_adjustment: LogicalVec2::zero(),
}),
None => Some(AspectRatio {
i_over_b: (preferred_ratio.0).0 / (preferred_ratio.1).0,
box_sizing_adjustment: LogicalVec2::zero(),
}),
},
// "The boxs preferred aspect ratio is the specified ratio of width
// / height. Size calculations involving the aspect ratio work with
// the dimensions of the box specified by box-sizing."
(false, PreferredRatio::Ratio(preferred_ratio)) => {
// If the `box-sizing` is `border-box`, use the padding and
// border when calculating the aspect ratio.
let box_sizing_adjustment = match self.clone_box_sizing() {
BoxSizing::ContentBox => LogicalVec2::zero(),
BoxSizing::BorderBox => {
self.padding_border_margin(containing_block)
.padding_border_sums
},
};
Some(AspectRatio {
i_over_b: (preferred_ratio.0).0 / (preferred_ratio.1).0,
box_sizing_adjustment,
})
},
}
}
/// Whether or not this style specifies a non-transparent background.
fn background_is_transparent(&self) -> bool {
let background = self.get_background();