bevy/crates/bevy_ui/src/layout/convert.rs
Nico Burns 363d0f0c7c
Add CSS Grid support to bevy_ui (#8026)
# Objective

An easy way to create 2D grid layouts

## Solution

Enable the `grid` feature in Taffy and add new style types for defining
grids.

## Notes

- ~I'm having a bit of trouble getting `#[derive(Reflect)]` to work
properly. Help with that would be appreciated (EDIT: got it to compile
by ignoring the problematic fields, but this presumably can't be
merged).~ This is now fixed
- ~The alignment types now have a `Normal` variant because I couldn't
get reflect to work with `Option`.~ I've decided to stick with the
flattened variant, as it saves a level of wrapping when authoring
styles. But I've renamed the variants from `Normal` to `Default`.
- ~This currently exposes a simplified API on top of grid. In particular
the following is not currently supported:~
   - ~Negative grid indices~ Now supported.
- ~Custom `end` values for grid placement (you can only use `start` and
`span`)~ Now supported
- ~`minmax()` track sizing functions~ minmax is now support through a
`GridTrack::minmax()` constructor
   - ~`repeat()`~ repeat is now implemented as `RepeatedGridTrack`

- ~Documentation still needs to be improved.~ An initial pass over the
documentation has been completed.

## Screenshot

<img width="846" alt="Screenshot 2023-03-10 at 17 56 21"
src="https://user-images.githubusercontent.com/1007307/224435332-69aa9eac-123d-4856-b75d-5449d3f1d426.png">

---

## Changelog

- Support for CSS Grid layout added to `bevy_ui`

---------

Co-authored-by: Rob Parrett <robparrett@gmail.com>
Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com>
2023-04-17 16:21:38 +00:00

667 lines
25 KiB
Rust

use taffy::style_helpers;
use crate::{
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, GridAutoFlow,
GridPlacement, GridTrack, GridTrackRepetition, JustifyContent, JustifyItems, JustifySelf,
MaxTrackSizingFunction, MinTrackSizingFunction, PositionType, RepeatedGridTrack, Size, Style,
UiRect, Val,
};
use super::LayoutContext;
impl Val {
fn into_length_percentage_auto(
self,
context: &LayoutContext,
) -> taffy::style::LengthPercentageAuto {
match self {
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.),
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(
(context.scale_factor * value as f64) as f32,
),
Val::VMin(value) => {
taffy::style::LengthPercentageAuto::Points(context.min_size * value / 100.)
}
Val::VMax(value) => {
taffy::style::LengthPercentageAuto::Points(context.max_size * value / 100.)
}
Val::Vw(value) => {
taffy::style::LengthPercentageAuto::Points(context.physical_size.x * value / 100.)
}
Val::Vh(value) => {
taffy::style::LengthPercentageAuto::Points(context.physical_size.y * value / 100.)
}
}
}
fn into_length_percentage(self, context: &LayoutContext) -> taffy::style::LengthPercentage {
match self.into_length_percentage_auto(context) {
taffy::style::LengthPercentageAuto::Auto => taffy::style::LengthPercentage::Points(0.0),
taffy::style::LengthPercentageAuto::Percent(value) => {
taffy::style::LengthPercentage::Percent(value)
}
taffy::style::LengthPercentageAuto::Points(value) => {
taffy::style::LengthPercentage::Points(value)
}
}
}
fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension {
self.into_length_percentage_auto(context).into()
}
}
impl UiRect {
fn map_to_taffy_rect<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Rect<T> {
taffy::geometry::Rect {
left: map_fn(self.left),
right: map_fn(self.right),
top: map_fn(self.top),
bottom: map_fn(self.bottom),
}
}
}
impl Size {
fn map_to_taffy_size<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Size<T> {
taffy::geometry::Size {
width: map_fn(self.width),
height: map_fn(self.height),
}
}
}
pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style {
taffy::style::Style {
display: style.display.into(),
position: style.position_type.into(),
flex_direction: style.flex_direction.into(),
flex_wrap: style.flex_wrap.into(),
align_items: style.align_items.into(),
justify_items: style.justify_items.into(),
align_self: style.align_self.into(),
justify_self: style.justify_self.into(),
align_content: style.align_content.into(),
justify_content: style.justify_content.into(),
inset: taffy::prelude::Rect {
left: style.left.into_length_percentage_auto(context),
right: style.right.into_length_percentage_auto(context),
top: style.top.into_length_percentage_auto(context),
bottom: style.bottom.into_length_percentage_auto(context),
},
margin: style
.margin
.map_to_taffy_rect(|m| m.into_length_percentage_auto(context)),
padding: style
.padding
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
border: style
.border
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
flex_grow: style.flex_grow,
flex_shrink: style.flex_shrink,
flex_basis: style.flex_basis.into_dimension(context),
size: style.size.map_to_taffy_size(|s| s.into_dimension(context)),
min_size: style
.min_size
.map_to_taffy_size(|s| s.into_dimension(context)),
max_size: style
.max_size
.map_to_taffy_size(|s| s.into_dimension(context)),
aspect_ratio: style.aspect_ratio,
gap: style
.gap
.map_to_taffy_size(|s| s.into_length_percentage(context)),
grid_auto_flow: style.grid_auto_flow.into(),
grid_template_rows: style
.grid_template_rows
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_template_columns: style
.grid_template_columns
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_rows: style
.grid_auto_rows
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_columns: style
.grid_auto_columns
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_row: style.grid_row.into(),
grid_column: style.grid_column.into(),
}
}
impl From<AlignItems> for Option<taffy::style::AlignItems> {
fn from(value: AlignItems) -> Self {
match value {
AlignItems::Default => None,
AlignItems::Start => taffy::style::AlignItems::Start.into(),
AlignItems::End => taffy::style::AlignItems::End.into(),
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart.into(),
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd.into(),
AlignItems::Center => taffy::style::AlignItems::Center.into(),
AlignItems::Baseline => taffy::style::AlignItems::Baseline.into(),
AlignItems::Stretch => taffy::style::AlignItems::Stretch.into(),
}
}
}
impl From<JustifyItems> for Option<taffy::style::JustifyItems> {
fn from(value: JustifyItems) -> Self {
match value {
JustifyItems::Default => None,
JustifyItems::Start => taffy::style::JustifyItems::Start.into(),
JustifyItems::End => taffy::style::JustifyItems::End.into(),
JustifyItems::Center => taffy::style::JustifyItems::Center.into(),
JustifyItems::Baseline => taffy::style::JustifyItems::Baseline.into(),
JustifyItems::Stretch => taffy::style::JustifyItems::Stretch.into(),
}
}
}
impl From<AlignSelf> for Option<taffy::style::AlignSelf> {
fn from(value: AlignSelf) -> Self {
match value {
AlignSelf::Auto => None,
AlignSelf::Start => taffy::style::AlignSelf::Start.into(),
AlignSelf::End => taffy::style::AlignSelf::End.into(),
AlignSelf::FlexStart => taffy::style::AlignSelf::FlexStart.into(),
AlignSelf::FlexEnd => taffy::style::AlignSelf::FlexEnd.into(),
AlignSelf::Center => taffy::style::AlignSelf::Center.into(),
AlignSelf::Baseline => taffy::style::AlignSelf::Baseline.into(),
AlignSelf::Stretch => taffy::style::AlignSelf::Stretch.into(),
}
}
}
impl From<JustifySelf> for Option<taffy::style::JustifySelf> {
fn from(value: JustifySelf) -> Self {
match value {
JustifySelf::Auto => None,
JustifySelf::Start => taffy::style::JustifySelf::Start.into(),
JustifySelf::End => taffy::style::JustifySelf::End.into(),
JustifySelf::Center => taffy::style::JustifySelf::Center.into(),
JustifySelf::Baseline => taffy::style::JustifySelf::Baseline.into(),
JustifySelf::Stretch => taffy::style::JustifySelf::Stretch.into(),
}
}
}
impl From<AlignContent> for Option<taffy::style::AlignContent> {
fn from(value: AlignContent) -> Self {
match value {
AlignContent::Default => None,
AlignContent::Start => taffy::style::AlignContent::Start.into(),
AlignContent::End => taffy::style::AlignContent::End.into(),
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart.into(),
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd.into(),
AlignContent::Center => taffy::style::AlignContent::Center.into(),
AlignContent::Stretch => taffy::style::AlignContent::Stretch.into(),
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween.into(),
AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround.into(),
AlignContent::SpaceEvenly => taffy::style::AlignContent::SpaceEvenly.into(),
}
}
}
impl From<JustifyContent> for Option<taffy::style::JustifyContent> {
fn from(value: JustifyContent) -> Self {
match value {
JustifyContent::Default => None,
JustifyContent::Start => taffy::style::JustifyContent::Start.into(),
JustifyContent::End => taffy::style::JustifyContent::End.into(),
JustifyContent::FlexStart => taffy::style::JustifyContent::FlexStart.into(),
JustifyContent::FlexEnd => taffy::style::JustifyContent::FlexEnd.into(),
JustifyContent::Center => taffy::style::JustifyContent::Center.into(),
JustifyContent::SpaceBetween => taffy::style::JustifyContent::SpaceBetween.into(),
JustifyContent::SpaceAround => taffy::style::JustifyContent::SpaceAround.into(),
JustifyContent::SpaceEvenly => taffy::style::JustifyContent::SpaceEvenly.into(),
}
}
}
impl From<Display> for taffy::style::Display {
fn from(value: Display) -> Self {
match value {
Display::Flex => taffy::style::Display::Flex,
Display::Grid => taffy::style::Display::Grid,
Display::None => taffy::style::Display::None,
}
}
}
impl From<FlexDirection> for taffy::style::FlexDirection {
fn from(value: FlexDirection) -> Self {
match value {
FlexDirection::Row => taffy::style::FlexDirection::Row,
FlexDirection::Column => taffy::style::FlexDirection::Column,
FlexDirection::RowReverse => taffy::style::FlexDirection::RowReverse,
FlexDirection::ColumnReverse => taffy::style::FlexDirection::ColumnReverse,
}
}
}
impl From<PositionType> for taffy::style::Position {
fn from(value: PositionType) -> Self {
match value {
PositionType::Relative => taffy::style::Position::Relative,
PositionType::Absolute => taffy::style::Position::Absolute,
}
}
}
impl From<FlexWrap> for taffy::style::FlexWrap {
fn from(value: FlexWrap) -> Self {
match value {
FlexWrap::NoWrap => taffy::style::FlexWrap::NoWrap,
FlexWrap::Wrap => taffy::style::FlexWrap::Wrap,
FlexWrap::WrapReverse => taffy::style::FlexWrap::WrapReverse,
}
}
}
impl From<GridAutoFlow> for taffy::style::GridAutoFlow {
fn from(value: GridAutoFlow) -> Self {
match value {
GridAutoFlow::Row => taffy::style::GridAutoFlow::Row,
GridAutoFlow::RowDense => taffy::style::GridAutoFlow::RowDense,
GridAutoFlow::Column => taffy::style::GridAutoFlow::Column,
GridAutoFlow::ColumnDense => taffy::style::GridAutoFlow::ColumnDense,
}
}
}
impl From<GridPlacement> for taffy::geometry::Line<taffy::style::GridPlacement> {
fn from(value: GridPlacement) -> Self {
let span = value.span.unwrap_or(1).max(1);
match (value.start, value.end) {
(Some(start), Some(end)) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::line(end),
},
(Some(start), None) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::span(span),
},
(None, Some(end)) => taffy::geometry::Line {
start: style_helpers::span(span),
end: style_helpers::line(end),
},
(None, None) => style_helpers::span(span),
}
}
}
impl MinTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MinTrackSizingFunction {
match self {
MinTrackSizingFunction::Px(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MinTrackSizingFunction::Percent(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MinTrackSizingFunction::Auto => taffy::style::MinTrackSizingFunction::Auto,
MinTrackSizingFunction::MinContent => taffy::style::MinTrackSizingFunction::MinContent,
MinTrackSizingFunction::MaxContent => taffy::style::MinTrackSizingFunction::MaxContent,
}
}
}
impl MaxTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction {
match self {
MaxTrackSizingFunction::Px(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Percent(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Auto => taffy::style::MaxTrackSizingFunction::Auto,
MaxTrackSizingFunction::MinContent => taffy::style::MaxTrackSizingFunction::MinContent,
MaxTrackSizingFunction::MaxContent => taffy::style::MaxTrackSizingFunction::MaxContent,
MaxTrackSizingFunction::FitContentPx(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Px(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::FitContentPercent(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Percent(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::Fraction(fraction) => {
taffy::style::MaxTrackSizingFunction::Fraction(fraction)
}
}
}
}
impl GridTrack {
fn into_taffy_track(
self,
context: &LayoutContext,
) -> taffy::style::NonRepeatedTrackSizingFunction {
let min = self.min_sizing_function.into_taffy(context);
let max = self.max_sizing_function.into_taffy(context);
taffy::style_helpers::minmax(min, max)
}
}
impl RepeatedGridTrack {
fn clone_into_repeated_taffy_track(
&self,
context: &LayoutContext,
) -> taffy::style::TrackSizingFunction {
if self.tracks.len() == 1 && self.repetition == GridTrackRepetition::Count(1) {
let min = self.tracks[0].min_sizing_function.into_taffy(context);
let max = self.tracks[0].max_sizing_function.into_taffy(context);
let taffy_track = taffy::style_helpers::minmax(min, max);
taffy::style::TrackSizingFunction::Single(taffy_track)
} else {
let taffy_tracks: Vec<_> = self
.tracks
.iter()
.map(|track| {
let min = track.min_sizing_function.into_taffy(context);
let max = track.max_sizing_function.into_taffy(context);
taffy::style_helpers::minmax(min, max)
})
.collect();
match self.repetition {
GridTrackRepetition::Count(count) => {
taffy::style_helpers::repeat(count, taffy_tracks)
}
GridTrackRepetition::AutoFit => taffy::style_helpers::repeat(
taffy::style::GridTrackRepetition::AutoFit,
taffy_tracks,
),
GridTrackRepetition::AutoFill => taffy::style_helpers::repeat(
taffy::style::GridTrackRepetition::AutoFill,
taffy_tracks,
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_from() {
use taffy::style_helpers as sh;
let bevy_style = crate::Style {
display: Display::Flex,
position_type: PositionType::Absolute,
left: Val::Px(0.),
right: Val::Percent(0.),
top: Val::Auto,
bottom: Val::Auto,
direction: crate::Direction::Inherit,
flex_direction: FlexDirection::ColumnReverse,
flex_wrap: FlexWrap::WrapReverse,
align_items: AlignItems::Baseline,
align_self: AlignSelf::Start,
align_content: AlignContent::SpaceAround,
justify_items: JustifyItems::Default,
justify_self: JustifySelf::Center,
justify_content: JustifyContent::SpaceEvenly,
margin: UiRect {
left: Val::Percent(0.),
right: Val::Px(0.),
top: Val::Auto,
bottom: Val::Auto,
},
padding: UiRect {
left: Val::Percent(0.),
right: Val::Px(0.),
top: Val::Percent(0.),
bottom: Val::Percent(0.),
},
border: UiRect {
left: Val::Px(0.),
right: Val::Px(0.),
top: Val::Auto,
bottom: Val::Px(0.),
},
flex_grow: 1.,
flex_shrink: 0.,
flex_basis: Val::Px(0.),
size: Size {
width: Val::Px(0.),
height: Val::Auto,
},
min_size: Size {
width: Val::Px(0.),
height: Val::Percent(0.),
},
max_size: Size {
width: Val::Auto,
height: Val::Px(0.),
},
aspect_ratio: None,
overflow: crate::Overflow::Hidden,
gap: Size {
width: Val::Px(0.),
height: Val::Percent(0.),
},
grid_auto_flow: GridAutoFlow::ColumnDense,
grid_template_rows: vec![
GridTrack::px(10.0),
GridTrack::percent(50.0),
GridTrack::fr(1.0),
],
grid_template_columns: RepeatedGridTrack::px(5, 10.0),
grid_auto_rows: vec![
GridTrack::fit_content_px(10.0),
GridTrack::fit_content_percent(25.0),
GridTrack::flex(2.0),
],
grid_auto_columns: vec![
GridTrack::auto(),
GridTrack::min_content(),
GridTrack::max_content(),
],
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
};
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let taffy_style = from_style(&viewport_values, &bevy_style);
assert_eq!(taffy_style.display, taffy::style::Display::Flex);
assert_eq!(taffy_style.position, taffy::style::Position::Absolute);
assert!(matches!(
taffy_style.inset.left,
taffy::style::LengthPercentageAuto::Points(_)
));
assert!(matches!(
taffy_style.inset.right,
taffy::style::LengthPercentageAuto::Percent(_)
));
assert!(matches!(
taffy_style.inset.top,
taffy::style::LengthPercentageAuto::Auto
));
assert!(matches!(
taffy_style.inset.bottom,
taffy::style::LengthPercentageAuto::Auto
));
assert_eq!(
taffy_style.flex_direction,
taffy::style::FlexDirection::ColumnReverse
);
assert_eq!(taffy_style.flex_wrap, taffy::style::FlexWrap::WrapReverse);
assert_eq!(
taffy_style.align_items,
Some(taffy::style::AlignItems::Baseline)
);
assert_eq!(taffy_style.align_self, Some(taffy::style::AlignSelf::Start));
assert_eq!(
taffy_style.align_content,
Some(taffy::style::AlignContent::SpaceAround)
);
assert_eq!(
taffy_style.justify_content,
Some(taffy::style::JustifyContent::SpaceEvenly)
);
assert_eq!(taffy_style.justify_items, None);
assert_eq!(
taffy_style.justify_self,
Some(taffy::style::JustifySelf::Center)
);
assert!(matches!(
taffy_style.margin.left,
taffy::style::LengthPercentageAuto::Percent(_)
));
assert!(matches!(
taffy_style.margin.right,
taffy::style::LengthPercentageAuto::Points(_)
));
assert!(matches!(
taffy_style.margin.top,
taffy::style::LengthPercentageAuto::Auto
));
assert!(matches!(
taffy_style.margin.bottom,
taffy::style::LengthPercentageAuto::Auto
));
assert!(matches!(
taffy_style.padding.left,
taffy::style::LengthPercentage::Percent(_)
));
assert!(matches!(
taffy_style.padding.right,
taffy::style::LengthPercentage::Points(_)
));
assert!(matches!(
taffy_style.padding.top,
taffy::style::LengthPercentage::Percent(_)
));
assert!(matches!(
taffy_style.padding.bottom,
taffy::style::LengthPercentage::Percent(_)
));
assert!(matches!(
taffy_style.border.left,
taffy::style::LengthPercentage::Points(_)
));
assert!(matches!(
taffy_style.border.right,
taffy::style::LengthPercentage::Points(_)
));
assert!(matches!(
taffy_style.border.top,
taffy::style::LengthPercentage::Points(_)
));
assert!(matches!(
taffy_style.border.bottom,
taffy::style::LengthPercentage::Points(_)
));
assert_eq!(taffy_style.flex_grow, 1.);
assert_eq!(taffy_style.flex_shrink, 0.);
assert!(matches!(
taffy_style.flex_basis,
taffy::style::Dimension::Points(_)
));
assert!(matches!(
taffy_style.size.width,
taffy::style::Dimension::Points(_)
));
assert!(matches!(
taffy_style.size.height,
taffy::style::Dimension::Auto
));
assert!(matches!(
taffy_style.min_size.width,
taffy::style::Dimension::Points(_)
));
assert!(matches!(
taffy_style.min_size.height,
taffy::style::Dimension::Percent(_)
));
assert!(matches!(
taffy_style.max_size.width,
taffy::style::Dimension::Auto
));
assert!(matches!(
taffy_style.max_size.height,
taffy::style::Dimension::Points(_)
));
assert_eq!(taffy_style.aspect_ratio, None);
assert_eq!(
taffy_style.gap.width,
taffy::style::LengthPercentage::Points(0.)
);
assert_eq!(
taffy_style.gap.height,
taffy::style::LengthPercentage::Percent(0.)
);
assert_eq!(
taffy_style.grid_auto_flow,
taffy::style::GridAutoFlow::ColumnDense
);
assert_eq!(
taffy_style.grid_template_rows,
vec![sh::points(10.0), sh::percent(0.5), sh::fr(1.0)]
);
assert_eq!(
taffy_style.grid_template_columns,
vec![sh::repeat(5, vec![sh::points(10.0)])]
);
assert_eq!(
taffy_style.grid_auto_rows,
vec![
sh::fit_content(taffy::style::LengthPercentage::Points(10.0)),
sh::fit_content(taffy::style::LengthPercentage::Percent(0.25)),
sh::minmax(sh::points(0.0), sh::fr(2.0)),
]
);
assert_eq!(
taffy_style.grid_auto_columns,
vec![sh::auto(), sh::min_content(), sh::max_content()]
);
assert_eq!(
taffy_style.grid_column,
taffy::geometry::Line {
start: sh::line(4),
end: sh::span(1)
}
);
assert_eq!(taffy_style.grid_row, sh::span(3));
}
#[test]
fn test_into_length_percentage() {
use taffy::style::LengthPercentage;
let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.));
let cases = [
(Val::Auto, LengthPercentage::Points(0.)),
(Val::Percent(1.), LengthPercentage::Percent(0.01)),
(Val::Px(1.), LengthPercentage::Points(2.)),
(Val::Vw(1.), LengthPercentage::Points(8.)),
(Val::Vh(1.), LengthPercentage::Points(6.)),
(Val::VMin(2.), LengthPercentage::Points(12.)),
(Val::VMax(2.), LengthPercentage::Points(16.)),
];
for (val, length) in cases {
assert!(match (val.into_length_percentage(&context), length) {
(LengthPercentage::Points(a), LengthPercentage::Points(b))
| (LengthPercentage::Percent(a), LengthPercentage::Percent(b)) =>
(a - b).abs() < 0.0001,
_ => false,
},);
}
}
}