implement flexbox main axis justification
This commit is contained in:
@@ -8,7 +8,8 @@ use crate::scene::{
|
||||
};
|
||||
use crate::text::TextSystem;
|
||||
use crate::tree::{
|
||||
AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style,
|
||||
AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, JustifyContent,
|
||||
Style,
|
||||
};
|
||||
|
||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||
@@ -971,7 +972,13 @@ fn layout_container_children(
|
||||
}
|
||||
|
||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||
let free_space = if flex_total > 0.0 { 0.0 } else { remaining_main };
|
||||
let justify = justify_offsets(
|
||||
element.style.justify_content,
|
||||
free_space,
|
||||
element.children.len(),
|
||||
);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction) + justify.leading;
|
||||
let mut children = Vec::with_capacity(element.children.len());
|
||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||
let child_main = if measured.is_flex {
|
||||
@@ -1019,7 +1026,7 @@ fn layout_container_children(
|
||||
layout_cache,
|
||||
clip_rect,
|
||||
));
|
||||
cursor += child_main.max(0.0) + element.style.gap;
|
||||
cursor += child_main.max(0.0) + element.style.gap + justify.between;
|
||||
}
|
||||
children
|
||||
}
|
||||
@@ -1226,7 +1233,13 @@ fn layout_interaction_skeleton_children(
|
||||
}
|
||||
|
||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||
let free_space = if flex_total > 0.0 { 0.0 } else { remaining_main };
|
||||
let justify = justify_offsets(
|
||||
element.style.justify_content,
|
||||
free_space,
|
||||
element.children.len(),
|
||||
);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction) + justify.leading;
|
||||
let mut children = Vec::with_capacity(element.children.len());
|
||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||
let child_main = if measured.is_flex {
|
||||
@@ -1269,7 +1282,7 @@ fn layout_interaction_skeleton_children(
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
));
|
||||
cursor += child_main.max(0.0) + element.style.gap;
|
||||
cursor += child_main.max(0.0) + element.style.gap + justify.between;
|
||||
}
|
||||
children
|
||||
}
|
||||
@@ -2246,15 +2259,197 @@ fn child_rect(
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjustment applied by `justify_content` to the leading offset of the first
|
||||
/// child and the extra space inserted between adjacent children (in addition
|
||||
/// to `gap`).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct JustifyOffsets {
|
||||
leading: f32,
|
||||
between: f32,
|
||||
}
|
||||
|
||||
fn justify_offsets(
|
||||
justify: JustifyContent,
|
||||
free_space: f32,
|
||||
child_count: usize,
|
||||
) -> JustifyOffsets {
|
||||
if child_count == 0 {
|
||||
return JustifyOffsets {
|
||||
leading: 0.0,
|
||||
between: 0.0,
|
||||
};
|
||||
}
|
||||
let free = free_space.max(0.0);
|
||||
match justify {
|
||||
JustifyContent::Start => JustifyOffsets {
|
||||
leading: 0.0,
|
||||
between: 0.0,
|
||||
},
|
||||
JustifyContent::End => JustifyOffsets {
|
||||
leading: free,
|
||||
between: 0.0,
|
||||
},
|
||||
JustifyContent::Center => JustifyOffsets {
|
||||
leading: free * 0.5,
|
||||
between: 0.0,
|
||||
},
|
||||
JustifyContent::SpaceBetween => {
|
||||
if child_count < 2 {
|
||||
JustifyOffsets {
|
||||
leading: 0.0,
|
||||
between: 0.0,
|
||||
}
|
||||
} else {
|
||||
JustifyOffsets {
|
||||
leading: 0.0,
|
||||
between: free / (child_count - 1) as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
JustifyContent::SpaceAround => {
|
||||
let segment = free / child_count as f32;
|
||||
JustifyOffsets {
|
||||
leading: segment * 0.5,
|
||||
between: segment,
|
||||
}
|
||||
}
|
||||
JustifyContent::SpaceEvenly => {
|
||||
let segment = free / (child_count + 1) as f32;
|
||||
JustifyOffsets {
|
||||
leading: segment,
|
||||
between: segment,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
LayoutCache, LayoutSnapshot, layout_scene, layout_snapshot, layout_snapshot_with_cache,
|
||||
LayoutCache, LayoutSnapshot, justify_offsets, layout_scene, layout_snapshot,
|
||||
layout_snapshot_with_cache,
|
||||
};
|
||||
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||
use crate::text::TextSystem;
|
||||
use crate::text::{TextStyle, TextWrap};
|
||||
use crate::tree::{Edges, Element, ElementId, FlexDirection};
|
||||
use crate::tree::{Edges, Element, ElementId, FlexDirection, JustifyContent};
|
||||
|
||||
#[test]
|
||||
fn justify_offsets_match_each_mode() {
|
||||
// 3 children, 60 units of free space.
|
||||
let cases = [
|
||||
(JustifyContent::Start, (0.0, 0.0)),
|
||||
(JustifyContent::End, (60.0, 0.0)),
|
||||
(JustifyContent::Center, (30.0, 0.0)),
|
||||
(JustifyContent::SpaceBetween, (0.0, 30.0)),
|
||||
(JustifyContent::SpaceAround, (10.0, 20.0)),
|
||||
(JustifyContent::SpaceEvenly, (15.0, 15.0)),
|
||||
];
|
||||
for (mode, (leading, between)) in cases {
|
||||
let off = justify_offsets(mode, 60.0, 3);
|
||||
assert_eq!(
|
||||
(off.leading, off.between),
|
||||
(leading, between),
|
||||
"mode {mode:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justify_offsets_handle_degenerate_inputs() {
|
||||
// No children: every mode collapses to zero offsets.
|
||||
for mode in [
|
||||
JustifyContent::Start,
|
||||
JustifyContent::End,
|
||||
JustifyContent::Center,
|
||||
JustifyContent::SpaceBetween,
|
||||
JustifyContent::SpaceAround,
|
||||
JustifyContent::SpaceEvenly,
|
||||
] {
|
||||
let off = justify_offsets(mode, 100.0, 0);
|
||||
assert_eq!((off.leading, off.between), (0.0, 0.0));
|
||||
}
|
||||
|
||||
// Single child: SpaceBetween pins to start, SpaceAround/SpaceEvenly center.
|
||||
let one_between = justify_offsets(JustifyContent::SpaceBetween, 60.0, 1);
|
||||
assert_eq!((one_between.leading, one_between.between), (0.0, 0.0));
|
||||
let one_around = justify_offsets(JustifyContent::SpaceAround, 60.0, 1);
|
||||
assert_eq!(one_around.leading, 30.0);
|
||||
let one_evenly = justify_offsets(JustifyContent::SpaceEvenly, 60.0, 1);
|
||||
assert_eq!(one_evenly.leading, 30.0);
|
||||
|
||||
// Negative free space is clamped to zero.
|
||||
let off = justify_offsets(JustifyContent::Center, -10.0, 3);
|
||||
assert_eq!((off.leading, off.between), (0.0, 0.0));
|
||||
}
|
||||
|
||||
/// End-to-end check that `justify_content` actually shifts child rectangles
|
||||
/// for every mode by laying out three 50-unit boxes in a 300-unit row with
|
||||
/// no padding or gap (so 150 units of free space).
|
||||
#[test]
|
||||
fn justify_content_positions_children_for_each_mode() {
|
||||
fn child_xs(mode: JustifyContent, child_count: usize) -> Vec<f32> {
|
||||
let children: Vec<Element> = (0..child_count)
|
||||
.map(|_| Element::new().width(50.0).background(Color::rgb(1, 0, 0)))
|
||||
.collect();
|
||||
let root = Element::row().justify_content(mode).children(children);
|
||||
let scene = layout_scene(1, UiSize::new(300.0, 100.0), &root);
|
||||
scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(q) => Some(q.rect.origin.x),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 3 children, free = 300 - 3*50 = 150.
|
||||
assert_eq!(child_xs(JustifyContent::Start, 3), vec![0.0, 50.0, 100.0]);
|
||||
assert_eq!(child_xs(JustifyContent::End, 3), vec![150.0, 200.0, 250.0]);
|
||||
assert_eq!(
|
||||
child_xs(JustifyContent::Center, 3),
|
||||
vec![75.0, 125.0, 175.0],
|
||||
);
|
||||
// SpaceBetween: between = 150/2 = 75.
|
||||
assert_eq!(
|
||||
child_xs(JustifyContent::SpaceBetween, 3),
|
||||
vec![0.0, 125.0, 250.0],
|
||||
);
|
||||
// SpaceAround: segment = 150/3 = 50, leading = 25.
|
||||
assert_eq!(
|
||||
child_xs(JustifyContent::SpaceAround, 3),
|
||||
vec![25.0, 125.0, 225.0],
|
||||
);
|
||||
// SpaceEvenly with 4 children to avoid sub-pixel positions:
|
||||
// free = 300 - 4*50 = 100, segment = 100/5 = 20.
|
||||
assert_eq!(
|
||||
child_xs(JustifyContent::SpaceEvenly, 4),
|
||||
vec![20.0, 90.0, 160.0, 230.0],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justify_content_no_op_when_flex_children_consume_remainder() {
|
||||
// A flex child eats all remaining space, so SpaceBetween should not
|
||||
// shift the fixed sibling away from its normal cursor position.
|
||||
let root = Element::row()
|
||||
.justify_content(JustifyContent::SpaceBetween)
|
||||
.children([
|
||||
Element::new().width(50.0).background(Color::rgb(1, 0, 0)),
|
||||
Element::new().flex(1.0).background(Color::rgb(0, 1, 0)),
|
||||
]);
|
||||
let scene = layout_scene(1, UiSize::new(300.0, 100.0), &root);
|
||||
let xs: Vec<f32> = scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(q) => Some(q.rect.origin.x),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(xs, vec![0.0, 50.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_layout_apportions_fixed_and_flex_children() {
|
||||
|
||||
@@ -53,7 +53,7 @@ pub use text::{
|
||||
};
|
||||
pub use tree::{
|
||||
AlignItems, Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element,
|
||||
ElementId, FlexDirection, ScrollbarStyle, Style,
|
||||
ElementId, FlexDirection, JustifyContent, ScrollbarStyle, Style,
|
||||
};
|
||||
pub use window::{
|
||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||
|
||||
@@ -32,6 +32,36 @@ pub enum AlignItems {
|
||||
End,
|
||||
}
|
||||
|
||||
/// Controls how children are positioned and how leftover space is distributed
|
||||
/// along the main axis (horizontal for `FlexDirection::Row`, vertical for
|
||||
/// `FlexDirection::Column`).
|
||||
///
|
||||
/// When a container has flex children (`flex_grow > 0`) all remaining space is
|
||||
/// consumed by them, so `justify_content` has no visible effect on the main
|
||||
/// axis distribution in that case. The container's `gap` is always honored as
|
||||
/// a minimum between adjacent children — `SpaceBetween`, `SpaceAround`, and
|
||||
/// `SpaceEvenly` distribute leftover space *in addition to* `gap`.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum JustifyContent {
|
||||
/// Pack children at the start of the main axis. This is the default and
|
||||
/// matches the behavior before `justify_content` existed.
|
||||
#[default]
|
||||
Start,
|
||||
/// Pack children at the end of the main axis.
|
||||
End,
|
||||
/// Pack children as a group, centered along the main axis.
|
||||
Center,
|
||||
/// Distribute children with the first child flush with the start, the last
|
||||
/// child flush with the end, and equal extra space between adjacent pairs.
|
||||
SpaceBetween,
|
||||
/// Distribute children with equal extra space around each child, so the
|
||||
/// space at the ends is half of the space between adjacent children.
|
||||
SpaceAround,
|
||||
/// Distribute children so the space between adjacent children and between
|
||||
/// the children and both edges of the container is identical.
|
||||
SpaceEvenly,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum CursorIcon {
|
||||
Default,
|
||||
@@ -192,6 +222,7 @@ pub struct Style {
|
||||
pub align_items: AlignItems,
|
||||
/// Per-element override of the parent container's `align_items`. `None` means inherit.
|
||||
pub align_self: Option<AlignItems>,
|
||||
pub justify_content: JustifyContent,
|
||||
pub width: Option<f32>,
|
||||
pub height: Option<f32>,
|
||||
pub min_width: Option<f32>,
|
||||
@@ -214,6 +245,7 @@ impl Default for Style {
|
||||
direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Stretch,
|
||||
align_self: None,
|
||||
justify_content: JustifyContent::Start,
|
||||
width: None,
|
||||
height: None,
|
||||
min_width: None,
|
||||
@@ -427,6 +459,11 @@ impl Element {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn justify_content(mut self, justify: JustifyContent) -> Self {
|
||||
self.style.justify_content = justify;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
||||
self.style.cursor = Some(cursor);
|
||||
self
|
||||
@@ -578,11 +615,18 @@ impl Hash for AlignItems {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for JustifyContent {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(*self as u8).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Style {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.direction.hash(state);
|
||||
self.align_items.hash(state);
|
||||
self.align_self.hash(state);
|
||||
self.justify_content.hash(state);
|
||||
self.width.map(f32::to_bits).hash(state);
|
||||
self.height.map(f32::to_bits).hash(state);
|
||||
self.min_width.map(f32::to_bits).hash(state);
|
||||
|
||||
Reference in New Issue
Block a user