mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 08:01:36 +00:00
495 lines
16 KiB
Rust
495 lines
16 KiB
Rust
use taffy::{
|
|
AbsoluteAxis, AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignSelf,
|
|
AlignmentSafety, Direction, FlexWrap, Line, Point, Rect, Size, WritingMode,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum LogicalStaticEdge {
|
|
Start,
|
|
Center,
|
|
End,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum HorizontalStaticEdge {
|
|
Left,
|
|
Center,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum VerticalStaticEdge {
|
|
Top,
|
|
Center,
|
|
Bottom,
|
|
}
|
|
|
|
/// Physical static-position contract consumed by absolute positioning.
|
|
///
|
|
/// A point alone is insufficient: a centered point denotes the center of the
|
|
/// margin box, while an end point denotes its far edge. This is the same
|
|
/// distinction represented by Blink's `PhysicalStaticPosition`.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub(crate) struct PhysicalStaticPosition {
|
|
point: Point<f32>,
|
|
horizontal_edge: HorizontalStaticEdge,
|
|
vertical_edge: VerticalStaticEdge,
|
|
}
|
|
|
|
impl PhysicalStaticPosition {
|
|
pub(crate) const fn new(
|
|
point: Point<f32>,
|
|
horizontal_edge: HorizontalStaticEdge,
|
|
vertical_edge: VerticalStaticEdge,
|
|
) -> Self {
|
|
Self {
|
|
point,
|
|
horizontal_edge,
|
|
vertical_edge,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn relative_to(self, origin: Point<f32>) -> Self {
|
|
Self {
|
|
point: Point {
|
|
x: self.point.x - origin.x,
|
|
y: self.point.y - origin.y,
|
|
},
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub(crate) fn margin_box_origin(self, box_size: Size<f32>, margin: Rect<f32>) -> Point<f32> {
|
|
let x = match self.horizontal_edge {
|
|
HorizontalStaticEdge::Left => self.point.x + margin.left,
|
|
HorizontalStaticEdge::Center => {
|
|
self.point.x - box_size.width / 2.0 + (margin.left - margin.right) / 2.0
|
|
}
|
|
HorizontalStaticEdge::Right => self.point.x - box_size.width - margin.right,
|
|
};
|
|
let y = match self.vertical_edge {
|
|
VerticalStaticEdge::Top => self.point.y + margin.top,
|
|
VerticalStaticEdge::Center => {
|
|
self.point.y - box_size.height / 2.0 + (margin.top - margin.bottom) / 2.0
|
|
}
|
|
VerticalStaticEdge::Bottom => self.point.y - box_size.height - margin.bottom,
|
|
};
|
|
Point { x, y }
|
|
}
|
|
}
|
|
|
|
pub(crate) fn flex_main_axis_static_edge(
|
|
justify_content: Option<AlignContent>,
|
|
is_reverse: bool,
|
|
) -> LogicalStaticEdge {
|
|
match justify_content
|
|
.unwrap_or(AlignContent::FLEX_START)
|
|
.keyword()
|
|
{
|
|
AlignContentKeyword::FlexEnd => {
|
|
if is_reverse {
|
|
LogicalStaticEdge::Start
|
|
} else {
|
|
LogicalStaticEdge::End
|
|
}
|
|
}
|
|
AlignContentKeyword::Center
|
|
| AlignContentKeyword::SpaceAround
|
|
| AlignContentKeyword::SpaceEvenly => LogicalStaticEdge::Center,
|
|
AlignContentKeyword::Start => LogicalStaticEdge::Start,
|
|
AlignContentKeyword::End => LogicalStaticEdge::End,
|
|
AlignContentKeyword::FlexStart
|
|
| AlignContentKeyword::Stretch
|
|
| AlignContentKeyword::SpaceBetween => {
|
|
if is_reverse {
|
|
LogicalStaticEdge::End
|
|
} else {
|
|
LogicalStaticEdge::Start
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct FlexCrossAxisStaticContext {
|
|
pub(crate) align_self: Option<AlignSelf>,
|
|
pub(crate) align_items: Option<AlignItems>,
|
|
pub(crate) flex_wrap: FlexWrap,
|
|
pub(crate) child_writing_mode: WritingMode,
|
|
pub(crate) child_direction: Direction,
|
|
pub(crate) container_writing_mode: WritingMode,
|
|
pub(crate) container_direction: Direction,
|
|
pub(crate) physical_axis: AbsoluteAxis,
|
|
pub(crate) overflows: bool,
|
|
}
|
|
|
|
impl FlexCrossAxisStaticContext {
|
|
pub(crate) fn resolve(self) -> LogicalStaticEdge {
|
|
let alignment = self
|
|
.align_self
|
|
.or(self.align_items)
|
|
.unwrap_or(AlignItems::STRETCH);
|
|
let mut keyword = if alignment.safety == AlignmentSafety::Safe && self.overflows {
|
|
AlignItemsKeyword::Start
|
|
} else {
|
|
alignment.keyword()
|
|
};
|
|
|
|
keyword =
|
|
match keyword {
|
|
AlignItemsKeyword::Start => AlignItemsKeyword::FlexStart,
|
|
AlignItemsKeyword::End => AlignItemsKeyword::FlexEnd,
|
|
AlignItemsKeyword::SelfStart | AlignItemsKeyword::SelfEnd => {
|
|
let child_start_reversed = self
|
|
.child_writing_mode
|
|
.is_axis_flow_reversed(self.physical_axis, self.child_direction);
|
|
let container_start_reversed = self
|
|
.container_writing_mode
|
|
.is_axis_flow_reversed(self.physical_axis, self.container_direction);
|
|
let starts_match = child_start_reversed == container_start_reversed;
|
|
match (keyword, starts_match) {
|
|
(AlignItemsKeyword::SelfStart, true)
|
|
| (AlignItemsKeyword::SelfEnd, false) => AlignItemsKeyword::FlexStart,
|
|
(AlignItemsKeyword::SelfStart, false)
|
|
| (AlignItemsKeyword::SelfEnd, true) => AlignItemsKeyword::FlexEnd,
|
|
_ => unreachable!("self-relative alignment was matched above"),
|
|
}
|
|
}
|
|
keyword => keyword,
|
|
};
|
|
|
|
if self.flex_wrap == FlexWrap::WrapReverse {
|
|
keyword = match keyword {
|
|
AlignItemsKeyword::FlexStart => AlignItemsKeyword::FlexEnd,
|
|
AlignItemsKeyword::FlexEnd => AlignItemsKeyword::FlexStart,
|
|
keyword => keyword,
|
|
};
|
|
}
|
|
|
|
match keyword {
|
|
AlignItemsKeyword::Center => LogicalStaticEdge::Center,
|
|
AlignItemsKeyword::FlexEnd => LogicalStaticEdge::End,
|
|
AlignItemsKeyword::Stretch if self.flex_wrap == FlexWrap::WrapReverse => {
|
|
LogicalStaticEdge::End
|
|
}
|
|
AlignItemsKeyword::Start
|
|
| AlignItemsKeyword::End
|
|
| AlignItemsKeyword::FlexStart
|
|
| AlignItemsKeyword::SelfStart
|
|
| AlignItemsKeyword::SelfEnd
|
|
| AlignItemsKeyword::Baseline
|
|
| AlignItemsKeyword::Stretch => LogicalStaticEdge::Start,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum PhysicalAxisStaticEdge {
|
|
Min,
|
|
Center,
|
|
Max,
|
|
}
|
|
|
|
fn physical_axis_static_position(
|
|
min: f32,
|
|
size: f32,
|
|
edge: LogicalStaticEdge,
|
|
start_is_reversed: bool,
|
|
) -> (f32, PhysicalAxisStaticEdge) {
|
|
match (edge, start_is_reversed) {
|
|
(LogicalStaticEdge::Start, false) | (LogicalStaticEdge::End, true) => {
|
|
(min, PhysicalAxisStaticEdge::Min)
|
|
}
|
|
(LogicalStaticEdge::Center, _) => (min + size / 2.0, PhysicalAxisStaticEdge::Center),
|
|
(LogicalStaticEdge::End, false) | (LogicalStaticEdge::Start, true) => {
|
|
(min + size, PhysicalAxisStaticEdge::Max)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn physical_static_position_from_logical(
|
|
content_origin: Point<f32>,
|
|
content_size: Size<f32>,
|
|
writing_mode: WritingMode,
|
|
direction: Direction,
|
|
inline_edge: LogicalStaticEdge,
|
|
block_edge: LogicalStaticEdge,
|
|
) -> PhysicalStaticPosition {
|
|
let inline_axis = writing_mode.inline_axis();
|
|
let (inline_offset, inline_physical_edge) = physical_axis_static_position(
|
|
match inline_axis {
|
|
AbsoluteAxis::Horizontal => content_origin.x,
|
|
AbsoluteAxis::Vertical => content_origin.y,
|
|
},
|
|
content_size.get_abs(inline_axis),
|
|
inline_edge,
|
|
writing_mode.is_inline_flow_reversed(direction),
|
|
);
|
|
let block_axis = writing_mode.block_axis();
|
|
let (block_offset, block_physical_edge) = physical_axis_static_position(
|
|
match block_axis {
|
|
AbsoluteAxis::Horizontal => content_origin.x,
|
|
AbsoluteAxis::Vertical => content_origin.y,
|
|
},
|
|
content_size.get_abs(block_axis),
|
|
block_edge,
|
|
writing_mode.is_block_flow_reversed(),
|
|
);
|
|
|
|
match inline_axis {
|
|
AbsoluteAxis::Horizontal => PhysicalStaticPosition::new(
|
|
Point {
|
|
x: inline_offset,
|
|
y: block_offset,
|
|
},
|
|
match inline_physical_edge {
|
|
PhysicalAxisStaticEdge::Min => HorizontalStaticEdge::Left,
|
|
PhysicalAxisStaticEdge::Center => HorizontalStaticEdge::Center,
|
|
PhysicalAxisStaticEdge::Max => HorizontalStaticEdge::Right,
|
|
},
|
|
match block_physical_edge {
|
|
PhysicalAxisStaticEdge::Min => VerticalStaticEdge::Top,
|
|
PhysicalAxisStaticEdge::Center => VerticalStaticEdge::Center,
|
|
PhysicalAxisStaticEdge::Max => VerticalStaticEdge::Bottom,
|
|
},
|
|
),
|
|
AbsoluteAxis::Vertical => PhysicalStaticPosition::new(
|
|
Point {
|
|
x: block_offset,
|
|
y: inline_offset,
|
|
},
|
|
match block_physical_edge {
|
|
PhysicalAxisStaticEdge::Min => HorizontalStaticEdge::Left,
|
|
PhysicalAxisStaticEdge::Center => HorizontalStaticEdge::Center,
|
|
PhysicalAxisStaticEdge::Max => HorizontalStaticEdge::Right,
|
|
},
|
|
match inline_physical_edge {
|
|
PhysicalAxisStaticEdge::Min => VerticalStaticEdge::Top,
|
|
PhysicalAxisStaticEdge::Center => VerticalStaticEdge::Center,
|
|
PhysicalAxisStaticEdge::Max => VerticalStaticEdge::Bottom,
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Resolve auto margins in one physical axis of an absolutely positioned box.
|
|
///
|
|
/// CSS Positioned Layout only distributes auto margins when both insets in
|
|
/// the axis are definite. Inline-axis negative space preserves the dominant
|
|
/// start edge; block-axis negative space is shared between both margins.
|
|
pub(crate) fn resolve_absolute_axis_margins(
|
|
margin: Line<Option<f32>>,
|
|
inset: Line<Option<f32>>,
|
|
area_size: f32,
|
|
box_size: f32,
|
|
share_negative_space: bool,
|
|
start_is_dominant: bool,
|
|
) -> Line<f32> {
|
|
if inset.start.is_none() || inset.end.is_none() {
|
|
return Line {
|
|
start: margin.start.unwrap_or(0.0),
|
|
end: margin.end.unwrap_or(0.0),
|
|
};
|
|
}
|
|
|
|
let free_space = area_size
|
|
- inset.start.unwrap()
|
|
- inset.end.unwrap()
|
|
- box_size
|
|
- margin.start.unwrap_or(0.0)
|
|
- margin.end.unwrap_or(0.0);
|
|
match (margin.start, margin.end) {
|
|
(Some(start), Some(end)) => Line { start, end },
|
|
(None, Some(end)) => Line {
|
|
start: free_space,
|
|
end,
|
|
},
|
|
(Some(start), None) => Line {
|
|
start,
|
|
end: free_space,
|
|
},
|
|
(None, None) if free_space > 0.0 || share_negative_space => {
|
|
let start = free_space / 2.0;
|
|
Line {
|
|
start,
|
|
end: free_space - start,
|
|
}
|
|
}
|
|
(None, None) if start_is_dominant => Line {
|
|
start: 0.0,
|
|
end: free_space,
|
|
},
|
|
(None, None) => Line {
|
|
start: free_space,
|
|
end: 0.0,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const AUTO: Line<Option<f32>> = Line {
|
|
start: None,
|
|
end: None,
|
|
};
|
|
const ZERO_INSETS: Line<Option<f32>> = Line {
|
|
start: Some(0.0),
|
|
end: Some(0.0),
|
|
};
|
|
|
|
#[test]
|
|
fn centered_static_position_centers_the_margin_box() {
|
|
let position = PhysicalStaticPosition::new(
|
|
Point { x: 100.0, y: 50.0 },
|
|
HorizontalStaticEdge::Center,
|
|
VerticalStaticEdge::Center,
|
|
);
|
|
assert_eq!(
|
|
position.margin_box_origin(
|
|
Size {
|
|
width: 20.0,
|
|
height: 10.0,
|
|
},
|
|
Rect {
|
|
left: 4.0,
|
|
right: 8.0,
|
|
top: 2.0,
|
|
bottom: 6.0,
|
|
},
|
|
),
|
|
Point { x: 88.0, y: 43.0 }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn logical_static_position_respects_vertical_flow_and_rtl() {
|
|
let position = physical_static_position_from_logical(
|
|
Point { x: 20.0, y: 10.0 },
|
|
Size {
|
|
width: 160.0,
|
|
height: 80.0,
|
|
},
|
|
WritingMode::VerticalRl,
|
|
Direction::Rtl,
|
|
LogicalStaticEdge::Start,
|
|
LogicalStaticEdge::Start,
|
|
);
|
|
assert_eq!(
|
|
position,
|
|
PhysicalStaticPosition::new(
|
|
Point { x: 180.0, y: 90.0 },
|
|
HorizontalStaticEdge::Right,
|
|
VerticalStaticEdge::Bottom,
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn flex_static_edges_distinguish_flow_and_flex_relative_values() {
|
|
assert_eq!(
|
|
flex_main_axis_static_edge(Some(AlignContent::START), true),
|
|
LogicalStaticEdge::Start
|
|
);
|
|
assert_eq!(
|
|
flex_main_axis_static_edge(Some(AlignContent::FLEX_START), true),
|
|
LogicalStaticEdge::End
|
|
);
|
|
assert_eq!(
|
|
flex_main_axis_static_edge(Some(AlignContent::FLEX_END), true),
|
|
LogicalStaticEdge::Start
|
|
);
|
|
assert_eq!(
|
|
FlexCrossAxisStaticContext {
|
|
align_self: None,
|
|
align_items: None,
|
|
flex_wrap: FlexWrap::WrapReverse,
|
|
child_writing_mode: WritingMode::HorizontalTb,
|
|
child_direction: Direction::Ltr,
|
|
container_writing_mode: WritingMode::HorizontalTb,
|
|
container_direction: Direction::Ltr,
|
|
physical_axis: AbsoluteAxis::Vertical,
|
|
overflows: false,
|
|
}
|
|
.resolve(),
|
|
LogicalStaticEdge::End
|
|
);
|
|
assert_eq!(
|
|
FlexCrossAxisStaticContext {
|
|
align_self: Some(AlignItems::SAFE_CENTER),
|
|
align_items: None,
|
|
flex_wrap: FlexWrap::NoWrap,
|
|
child_writing_mode: WritingMode::HorizontalTb,
|
|
child_direction: Direction::Ltr,
|
|
container_writing_mode: WritingMode::HorizontalTb,
|
|
container_direction: Direction::Ltr,
|
|
physical_axis: AbsoluteAxis::Vertical,
|
|
overflows: true,
|
|
}
|
|
.resolve(),
|
|
LogicalStaticEdge::Start
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn positive_space_is_shared_even_when_the_box_is_wider_than_that_space() {
|
|
assert_eq!(
|
|
resolve_absolute_axis_margins(AUTO, ZERO_INSETS, 1440.0, 975.0, false, true),
|
|
Line {
|
|
start: 232.5,
|
|
end: 232.5,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inline_negative_space_preserves_the_dominant_start_edge() {
|
|
assert_eq!(
|
|
resolve_absolute_axis_margins(AUTO, ZERO_INSETS, 100.0, 150.0, false, true),
|
|
Line {
|
|
start: 0.0,
|
|
end: -50.0,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
resolve_absolute_axis_margins(AUTO, ZERO_INSETS, 100.0, 150.0, false, false),
|
|
Line {
|
|
start: -50.0,
|
|
end: 0.0,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn block_negative_space_is_shared() {
|
|
assert_eq!(
|
|
resolve_absolute_axis_margins(AUTO, ZERO_INSETS, 100.0, 120.0, true, true),
|
|
Line {
|
|
start: -10.0,
|
|
end: -10.0,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_auto_inset_forces_auto_margins_to_zero() {
|
|
assert_eq!(
|
|
resolve_absolute_axis_margins(
|
|
AUTO,
|
|
Line {
|
|
start: Some(0.0),
|
|
end: None,
|
|
},
|
|
100.0,
|
|
20.0,
|
|
false,
|
|
true,
|
|
),
|
|
Line {
|
|
start: 0.0,
|
|
end: 0.0,
|
|
}
|
|
);
|
|
}
|
|
}
|