implement flexbox main axis justification
This commit is contained in:
@@ -52,6 +52,10 @@ path = "example/04_composition_and_context.rs"
|
|||||||
name = "05_async_runtime_io"
|
name = "05_async_runtime_io"
|
||||||
path = "example/05_async_runtime_io.rs"
|
path = "example/05_async_runtime_io.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "06_justify_content"
|
||||||
|
path = "example/06_justify_content.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "07_children_and_slots"
|
name = "07_children_and_slots"
|
||||||
path = "example/07_children_and_slots.rs"
|
path = "example/07_children_and_slots.rs"
|
||||||
|
|||||||
194
lib/ruin_app/example/06_justify_content.rs
Normal file
194
lib/ruin_app/example/06_justify_content.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
//! Visual demonstration of every supported `justify_content` mode.
|
||||||
|
//!
|
||||||
|
//! The window arranges one labeled cell per mode in a two-column grid. Each
|
||||||
|
//! cell is a flex container of the same fixed width with four
|
||||||
|
//! identically-sized children, so the only visible difference between cells is
|
||||||
|
//! how `justify_content` distributes the leftover space along the main axis.
|
||||||
|
|
||||||
|
use ruin_app::prelude::*;
|
||||||
|
|
||||||
|
const TRACK_WIDTH: f32 = 360.0;
|
||||||
|
const ITEM_SIZE: f32 = 44.0;
|
||||||
|
|
||||||
|
#[ruin_runtime::async_main]
|
||||||
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
App::new()
|
||||||
|
.window(
|
||||||
|
Window::new()
|
||||||
|
.title("RUIN justify_content")
|
||||||
|
.app_id("dev.ruin.justify-content")
|
||||||
|
.size(960.0, 620.0),
|
||||||
|
)
|
||||||
|
.mount(view! {
|
||||||
|
JustifyContentGallery() {}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn JustifyContentGallery() -> impl IntoView {
|
||||||
|
let canvas = Color::rgb(0x10, 0x16, 0x22);
|
||||||
|
let panel = Color::rgb(0x1A, 0x23, 0x33);
|
||||||
|
let accent = Color::rgb(0x71, 0xA7, 0xF7);
|
||||||
|
let text_color = Color::rgb(0xF5, 0xF7, 0xFB);
|
||||||
|
let muted = Color::rgb(0x9A, 0xA9, 0xC2);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
column(gap = 18.0, padding = 24.0, background = canvas) {
|
||||||
|
text(role = TextRole::Heading(1), size = 28.0, weight = FontWeight::Semibold, color = text_color) {
|
||||||
|
"justify_content"
|
||||||
|
}
|
||||||
|
text(color = muted, wrap = TextWrap::Word) {
|
||||||
|
"Each cell below is the same width and contains the same four boxes. The only \
|
||||||
|
thing that changes between cells is `justify_content`, which controls how the \
|
||||||
|
leftover space along the main axis is distributed."
|
||||||
|
}
|
||||||
|
|
||||||
|
row(gap = 16.0, align_items = AlignItems::Start) {
|
||||||
|
column(gap = 14.0, flex = 1.0) {
|
||||||
|
JustifyRow(
|
||||||
|
label = "Start",
|
||||||
|
description = "Pack children at the start of the main axis (default).",
|
||||||
|
justify = JustifyContent::Start,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
JustifyRow(
|
||||||
|
label = "End",
|
||||||
|
description = "Pack children at the end of the main axis.",
|
||||||
|
justify = JustifyContent::End,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
JustifyRow(
|
||||||
|
label = "Center",
|
||||||
|
description = "Pack children as a group, centered on the main axis.",
|
||||||
|
justify = JustifyContent::Center,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
column(gap = 14.0, flex = 1.0) {
|
||||||
|
JustifyRow(
|
||||||
|
label = "SpaceBetween",
|
||||||
|
description = "First child flush with the start, last with the end, equal gaps between.",
|
||||||
|
justify = JustifyContent::SpaceBetween,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
JustifyRow(
|
||||||
|
label = "SpaceAround",
|
||||||
|
description = "Equal space around each child; end gaps are half the gap between children.",
|
||||||
|
justify = JustifyContent::SpaceAround,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
JustifyRow(
|
||||||
|
label = "SpaceEvenly",
|
||||||
|
description = "Equal space between children and between children and both edges.",
|
||||||
|
justify = JustifyContent::SpaceEvenly,
|
||||||
|
panel = panel,
|
||||||
|
accent = accent,
|
||||||
|
text_color = text_color,
|
||||||
|
muted = muted,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn JustifyRow(
|
||||||
|
label: &'static str,
|
||||||
|
description: &'static str,
|
||||||
|
justify: JustifyContent,
|
||||||
|
panel: Color,
|
||||||
|
accent: Color,
|
||||||
|
text_color: Color,
|
||||||
|
muted: Color,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
column(
|
||||||
|
gap = 6.0,
|
||||||
|
padding = 12.0,
|
||||||
|
background = panel,
|
||||||
|
border_radius = 10.0,
|
||||||
|
border = (1.0, accent),
|
||||||
|
) {
|
||||||
|
row(gap = 10.0, align_items = AlignItems::Center) {
|
||||||
|
text(size = 16.0, weight = FontWeight::Semibold, color = text_color) { label }
|
||||||
|
text(color = muted) { description }
|
||||||
|
}
|
||||||
|
row(
|
||||||
|
width = TRACK_WIDTH,
|
||||||
|
height = ITEM_SIZE + 16.0,
|
||||||
|
padding = 8.0,
|
||||||
|
gap = 8.0,
|
||||||
|
background = Color::rgba(0xFF, 0xFF, 0xFF, 0x10),
|
||||||
|
border_radius = 6.0,
|
||||||
|
align_items = AlignItems::Center,
|
||||||
|
justify_content = justify,
|
||||||
|
) {
|
||||||
|
JustifyItem(label = "1", accent = accent, text_color = text_color) {}
|
||||||
|
JustifyItem(label = "2", accent = accent, text_color = text_color) {}
|
||||||
|
JustifyItem(label = "3", accent = accent, text_color = text_color) {}
|
||||||
|
JustifyItem(label = "4", accent = accent, text_color = text_color) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn JustifyItem(label: &'static str, accent: Color, text_color: Color) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
block(
|
||||||
|
width = ITEM_SIZE,
|
||||||
|
height = ITEM_SIZE,
|
||||||
|
background = accent,
|
||||||
|
border_radius = 6.0,
|
||||||
|
align_items = AlignItems::Center,
|
||||||
|
) {
|
||||||
|
row(
|
||||||
|
flex = 1.0,
|
||||||
|
align_items = AlignItems::Center,
|
||||||
|
justify_content = JustifyContent::Center,
|
||||||
|
) {
|
||||||
|
text(size = 18.0, weight = FontWeight::Semibold, color = text_color) { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gallery_renders_all_labels() {
|
||||||
|
let root = JustifyContentGallery::builder().children(());
|
||||||
|
let view = ruin_app::__render_mountable_for_test(&root);
|
||||||
|
let snapshot = ruin_ui::layout_snapshot(1, UiSize::new(960.0, 620.0), view.element());
|
||||||
|
|
||||||
|
for label in ["Start", "End", "Center", "SpaceBetween", "SpaceAround", "SpaceEvenly"] {
|
||||||
|
assert!(
|
||||||
|
snapshot.scene.items.iter().any(|item| matches!(
|
||||||
|
item,
|
||||||
|
ruin_ui::DisplayItem::Text(text) if text.text.contains(label)
|
||||||
|
)),
|
||||||
|
"expected `{label}` label to appear in the rendered scene",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,8 +71,8 @@ pub mod prelude {
|
|||||||
};
|
};
|
||||||
pub use ruin_ui::{
|
pub use ruin_ui::{
|
||||||
AlignItems, Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree,
|
AlignItems, Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree,
|
||||||
PointerButton, PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind,
|
JustifyContent, PointerButton, PointerEventKind, RoutedPointerEvent,
|
||||||
ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
|
RoutedPointerEventKind, ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ruin_ui::{AlignItems, Color, Element, ElementId};
|
use ruin_ui::{AlignItems, Color, Element, ElementId, JustifyContent};
|
||||||
|
|
||||||
use crate::context::allocate_element_id;
|
use crate::context::allocate_element_id;
|
||||||
use crate::converters::{IntoBorder, IntoEdges, IntoShadow};
|
use crate::converters::{IntoBorder, IntoEdges, IntoShadow};
|
||||||
@@ -92,6 +92,11 @@ impl ContainerProps {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn justify_content(mut self, justify: JustifyContent) -> Self {
|
||||||
|
self.element = self.element.justify_content(justify);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shadow(mut self, shadow: impl IntoShadow) -> Self {
|
pub fn shadow(mut self, shadow: impl IntoShadow) -> Self {
|
||||||
self.element = self.element.shadow(shadow.into_shadow());
|
self.element = self.element.shadow(shadow.into_shadow());
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ macro_rules! impl_props_methods {
|
|||||||
self.props = self.props.align_self(align);
|
self.props = self.props.align_self(align);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn justify_content(mut self, justify: ruin_ui::JustifyContent) -> Self {
|
||||||
|
self.props = self.props.justify_content(justify);
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn widget_ref<T>(mut self, widget_ref: $crate::hooks::WidgetRef<T>) -> Self {
|
pub fn widget_ref<T>(mut self, widget_ref: $crate::hooks::WidgetRef<T>) -> Self {
|
||||||
self.props = self.props.widget_ref(widget_ref);
|
self.props = self.props.widget_ref(widget_ref);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use crate::scene::{
|
|||||||
};
|
};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::tree::{
|
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 {
|
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 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());
|
let mut children = Vec::with_capacity(element.children.len());
|
||||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||||
let child_main = if measured.is_flex {
|
let child_main = if measured.is_flex {
|
||||||
@@ -1019,7 +1026,7 @@ fn layout_container_children(
|
|||||||
layout_cache,
|
layout_cache,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
));
|
));
|
||||||
cursor += child_main.max(0.0) + element.style.gap;
|
cursor += child_main.max(0.0) + element.style.gap + justify.between;
|
||||||
}
|
}
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
@@ -1226,7 +1233,13 @@ fn layout_interaction_skeleton_children(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
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());
|
let mut children = Vec::with_capacity(element.children.len());
|
||||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||||
let child_main = if measured.is_flex {
|
let child_main = if measured.is_flex {
|
||||||
@@ -1269,7 +1282,7 @@ fn layout_interaction_skeleton_children(
|
|||||||
perf_stats,
|
perf_stats,
|
||||||
layout_cache,
|
layout_cache,
|
||||||
));
|
));
|
||||||
cursor += child_main.max(0.0) + element.style.gap;
|
cursor += child_main.max(0.0) + element.style.gap + justify.between;
|
||||||
}
|
}
|
||||||
children
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
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::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::text::{TextStyle, TextWrap};
|
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]
|
#[test]
|
||||||
fn row_layout_apportions_fixed_and_flex_children() {
|
fn row_layout_apportions_fixed_and_flex_children() {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub use text::{
|
|||||||
};
|
};
|
||||||
pub use tree::{
|
pub use tree::{
|
||||||
AlignItems, Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element,
|
AlignItems, Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element,
|
||||||
ElementId, FlexDirection, ScrollbarStyle, Style,
|
ElementId, FlexDirection, JustifyContent, ScrollbarStyle, Style,
|
||||||
};
|
};
|
||||||
pub use window::{
|
pub use window::{
|
||||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||||
|
|||||||
@@ -32,6 +32,36 @@ pub enum AlignItems {
|
|||||||
End,
|
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)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum CursorIcon {
|
pub enum CursorIcon {
|
||||||
Default,
|
Default,
|
||||||
@@ -192,6 +222,7 @@ pub struct Style {
|
|||||||
pub align_items: AlignItems,
|
pub align_items: AlignItems,
|
||||||
/// Per-element override of the parent container's `align_items`. `None` means inherit.
|
/// Per-element override of the parent container's `align_items`. `None` means inherit.
|
||||||
pub align_self: Option<AlignItems>,
|
pub align_self: Option<AlignItems>,
|
||||||
|
pub justify_content: JustifyContent,
|
||||||
pub width: Option<f32>,
|
pub width: Option<f32>,
|
||||||
pub height: Option<f32>,
|
pub height: Option<f32>,
|
||||||
pub min_width: Option<f32>,
|
pub min_width: Option<f32>,
|
||||||
@@ -214,6 +245,7 @@ impl Default for Style {
|
|||||||
direction: FlexDirection::Column,
|
direction: FlexDirection::Column,
|
||||||
align_items: AlignItems::Stretch,
|
align_items: AlignItems::Stretch,
|
||||||
align_self: None,
|
align_self: None,
|
||||||
|
justify_content: JustifyContent::Start,
|
||||||
width: None,
|
width: None,
|
||||||
height: None,
|
height: None,
|
||||||
min_width: None,
|
min_width: None,
|
||||||
@@ -427,6 +459,11 @@ impl Element {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn justify_content(mut self, justify: JustifyContent) -> Self {
|
||||||
|
self.style.justify_content = justify;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
||||||
self.style.cursor = Some(cursor);
|
self.style.cursor = Some(cursor);
|
||||||
self
|
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 {
|
impl Hash for Style {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.direction.hash(state);
|
self.direction.hash(state);
|
||||||
self.align_items.hash(state);
|
self.align_items.hash(state);
|
||||||
self.align_self.hash(state);
|
self.align_self.hash(state);
|
||||||
|
self.justify_content.hash(state);
|
||||||
self.width.map(f32::to_bits).hash(state);
|
self.width.map(f32::to_bits).hash(state);
|
||||||
self.height.map(f32::to_bits).hash(state);
|
self.height.map(f32::to_bits).hash(state);
|
||||||
self.min_width.map(f32::to_bits).hash(state);
|
self.min_width.map(f32::to_bits).hash(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user