diff --git a/lib/ruin_app/example/00_bootstrap_and_counter.rs b/lib/ruin_app/example/00_bootstrap_and_counter.rs index 1dff49f..37fa129 100644 --- a/lib/ruin_app/example/00_bootstrap_and_counter.rs +++ b/lib/ruin_app/example/00_bootstrap_and_counter.rs @@ -41,7 +41,7 @@ fn CounterApp(title: &'static str) -> impl IntoView { }; view! { - column(gap = 16.0, padding = 24.0, text_style = text_style) { + column(gap = 16.0, padding = 24.0, align_items = AlignItems::Center, text_style = text_style) { text(role = TextRole::Heading(1), size = 32.0, weight = FontWeight::Semibold, color = Color::rgba(0, 0, 0, 255)) { title } @@ -51,6 +51,7 @@ fn CounterApp(title: &'static str) -> impl IntoView { block( padding = 16.0, gap = 8.0, + align_self = AlignItems::Stretch, background = Color::rgba(255, 255, 255, 200), border_radius = 12.0, border = Border::new(2.0, Color::rgba(0, 0, 0, 120)), diff --git a/lib/ruin_app/src/lib.rs b/lib/ruin_app/src/lib.rs index affdab2..86febdd 100644 --- a/lib/ruin_app/src/lib.rs +++ b/lib/ruin_app/src/lib.rs @@ -68,9 +68,9 @@ pub mod prelude { view, }; pub use ruin_ui::{ - Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton, - PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind, ScrollbarStyle, - TextFontFamily, TextStyle, TextWrap, UiSize, + AlignItems, Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, + PointerButton, PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind, + ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize, }; } diff --git a/lib/ruin_app/src/primitives/container.rs b/lib/ruin_app/src/primitives/container.rs index a65397a..148f71f 100644 --- a/lib/ruin_app/src/primitives/container.rs +++ b/lib/ruin_app/src/primitives/container.rs @@ -1,4 +1,4 @@ -use ruin_ui::{Color, Element, ElementId}; +use ruin_ui::{AlignItems, Color, Element, ElementId}; use crate::context::allocate_element_id; use crate::converters::{IntoBorder, IntoEdges, IntoShadow}; @@ -82,6 +82,16 @@ impl ContainerProps { self } + pub fn align_items(mut self, align: AlignItems) -> Self { + self.element = self.element.align_items(align); + self + } + + pub fn align_self(mut self, align: AlignItems) -> Self { + self.element = self.element.align_self(align); + self + } + pub fn shadow(mut self, shadow: impl IntoShadow) -> Self { self.element = self.element.shadow(shadow.into_shadow()); self @@ -118,6 +128,16 @@ pub fn block() -> ContainerBuilder { impl ContainerBuilder { impl_props_methods!(); + pub fn align_items(mut self, align: AlignItems) -> Self { + self.props = self.props.align_items(align); + self + } + + pub fn align_self(mut self, align: AlignItems) -> Self { + self.props = self.props.align_self(align); + self + } + pub fn shadow(mut self, shadow: impl IntoShadow) -> Self { self.props = self.props.shadow(shadow); self diff --git a/lib/ui/src/layout.rs b/lib/ui/src/layout.rs index df19ee9..bc6158f 100644 --- a/lib/ui/src/layout.rs +++ b/lib/ui/src/layout.rs @@ -7,7 +7,7 @@ use crate::scene::{ UiSize, }; use crate::text::TextSystem; -use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style}; +use crate::tree::{AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style}; pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot { let mut text_system = TextSystem::new(); @@ -793,7 +793,11 @@ fn point_in_rounded_rect(point: crate::scene::Point, rect: Rect, radius: f32) -> #[derive(Clone, Copy, Debug)] struct MeasuredChild { + /// Offered cross-axis size (full available_cross for stretch; explicit value otherwise). cross: f32, + /// Natural cross-axis size: set when parent uses non-stretch alignment, child has no + /// explicit cross size, and the child is not flex. `None` means use `cross`. + natural_cross: Option, main: f32, is_flex: bool, } @@ -870,6 +874,31 @@ fn layout_container_children( intrinsic.max(min_main_outer) } }); + // For non-stretch alignment, measure the child's natural cross size so it is not + // forcibly stretched to fill the container's cross axis. Only done for non-flex + // children with no explicit cross-axis style, since flex main size is unknown here. + // align_self on the child overrides the parent's align_items for this child. + let effective_align = child.style.align_self.unwrap_or(element.style.align_items); + let natural_cross = + if effective_align != AlignItems::Stretch + && !is_flex + && child_cross_size(child, element.style.direction).is_none() + { + let offered = match element.style.direction { + FlexDirection::Row => UiSize::new(measured_main, available_cross), + FlexDirection::Column => UiSize::new(available_cross, measured_main), + }; + let natural = + intrinsic_size(child, offered, text_system, perf_stats, layout_cache); + Some( + cross_axis_size(natural, element.style.direction) + .max(min_cross_outer) + .min(available_cross.max(min_cross_outer)), + ) + } else { + None + }; + if is_flex { flex_total += child_flex_weight(child); } else { @@ -877,6 +906,7 @@ fn layout_container_children( } measured_children.push(MeasuredChild { cross, + natural_cross, main: measured_main, is_flex, }); @@ -902,12 +932,24 @@ fn layout_container_children( } else { measured.main }; + // For non-stretch alignment, use the child's natural cross size; otherwise stretch. + // align_self on the child overrides the parent's align_items for this child. + let effective_align = child.style.align_self.unwrap_or(element.style.align_items); + let actual_cross = measured.natural_cross.unwrap_or(measured.cross); + let cross_origin = { + let base = cross_axis_origin(content, element.style.direction); + match effective_align { + AlignItems::Stretch | AlignItems::Start => base, + AlignItems::Center => base + (available_cross - actual_cross) * 0.5, + AlignItems::End => base + available_cross - actual_cross, + } + }; let child_rect = child_rect( - content, + cross_origin, element.style.direction, cursor, child_main.max(0.0), - measured.cross, + actual_cross, ); children.push(layout_element( child, @@ -1865,16 +1907,23 @@ fn main_axis_origin(rect: Rect, direction: FlexDirection) -> f32 { } } +fn cross_axis_origin(content: Rect, direction: FlexDirection) -> f32 { + match direction { + FlexDirection::Row => content.origin.y, + FlexDirection::Column => content.origin.x, + } +} + fn child_rect( - content: Rect, + cross_origin: f32, direction: FlexDirection, main_origin: f32, main_size: f32, cross_size: f32, ) -> Rect { match direction { - FlexDirection::Row => Rect::new(main_origin, content.origin.y, main_size, cross_size), - FlexDirection::Column => Rect::new(content.origin.x, main_origin, cross_size, main_size), + FlexDirection::Row => Rect::new(main_origin, cross_origin, main_size, cross_size), + FlexDirection::Column => Rect::new(cross_origin, main_origin, cross_size, main_size), } } diff --git a/lib/ui/src/lib.rs b/lib/ui/src/lib.rs index adff8aa..f453fda 100644 --- a/lib/ui/src/lib.rs +++ b/lib/ui/src/lib.rs @@ -50,8 +50,8 @@ pub use text::{ TextStyle, TextSystem, TextWrap, }; pub use tree::{ - Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, ElementId, - FlexDirection, ScrollbarStyle, Style, + AlignItems, Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, + ElementId, FlexDirection, ScrollbarStyle, Style, }; pub use window::{ DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate, diff --git a/lib/ui/src/tree.rs b/lib/ui/src/tree.rs index bfde6d8..2848a44 100644 --- a/lib/ui/src/tree.rs +++ b/lib/ui/src/tree.rs @@ -23,6 +23,15 @@ pub enum FlexDirection { Column, } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum AlignItems { + #[default] + Stretch, + Start, + Center, + End, +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum CursorIcon { Default, @@ -180,6 +189,9 @@ impl Default for ScrollbarStyle { #[derive(Clone, Debug, PartialEq)] pub struct Style { pub direction: FlexDirection, + pub align_items: AlignItems, + /// Per-element override of the parent container's `align_items`. `None` means inherit. + pub align_self: Option, pub width: Option, pub height: Option, pub min_width: Option, @@ -200,6 +212,8 @@ impl Default for Style { fn default() -> Self { Self { direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + align_self: None, width: None, height: None, min_width: None, @@ -400,6 +414,16 @@ impl Element { self } + pub fn align_items(mut self, align: AlignItems) -> Self { + self.style.align_items = align; + self + } + + pub fn align_self(mut self, align: AlignItems) -> Self { + self.style.align_self = Some(align); + self + } + pub fn cursor(mut self, cursor: CursorIcon) -> Self { self.style.cursor = Some(cursor); self @@ -545,9 +569,17 @@ impl Hash for ScrollbarStyle { } } +impl Hash for AlignItems { + fn hash(&self, state: &mut H) { + (*self as u8).hash(state); + } +} + impl Hash for Style { fn hash(&self, state: &mut H) { self.direction.hash(state); + self.align_items.hash(state); + self.align_self.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);