diff --git a/examples/text_paragraph_demo/src/main.rs b/examples/text_paragraph_demo/src/main.rs index a5249d9..6f639cf 100644 --- a/examples/text_paragraph_demo/src/main.rs +++ b/examples/text_paragraph_demo/src/main.rs @@ -795,6 +795,7 @@ async fn main() -> Result<(), Box> { let LayoutSnapshot { scene, interaction_tree: next_interaction_tree, + .. } = build_snapshot( viewport, version, @@ -1077,6 +1078,7 @@ async fn main() -> Result<(), Box> { let LayoutSnapshot { scene, interaction_tree: next_interaction_tree, + .. } = build_snapshot( viewport, version, diff --git a/lib/ruin_app/example/00_bootstrap_and_counter.rs b/lib/ruin_app/example/00_bootstrap_and_counter.rs index 162af9c..9b639cd 100644 --- a/lib/ruin_app/example/00_bootstrap_and_counter.rs +++ b/lib/ruin_app/example/00_bootstrap_and_counter.rs @@ -1,4 +1,5 @@ use ruin_app::prelude::*; +use ruin_ui::{BoxShadow, BoxShadowKind, Point}; #[ruin_runtime::async_main] async fn main() -> ruin_app::Result<()> { @@ -9,6 +10,8 @@ async fn main() -> ruin_app::Result<()> { Window::new() .title(title) .app_id("dev.ruin.counter") + .base_color(Color::rgba(255, 255, 255, 80)) + .min_size(320.0, 240.0) .size(960.0, 640.0), ) .mount(view! { @@ -33,7 +36,7 @@ fn CounterApp(title: &'static str) -> impl IntoView { view! { column(gap = 16.0, padding = 24.0) { - text(role = TextRole::Heading(1), size = 32.0, weight = FontWeight::Semibold) { + text(role = TextRole::Heading(1), size = 32.0, weight = FontWeight::Semibold, color = Color::rgba(0, 0, 0, 255)) { title } @@ -42,12 +45,13 @@ fn CounterApp(title: &'static str) -> impl IntoView { block( padding = 16.0, gap = 8.0, - background = surfaces::raised(), + background = Color::rgba(255, 255, 255, 200), border_radius = 12.0, - border = Border::new(2.0, Color::rgb(0, 0, 0)) + border = Border::new(2.0, Color::rgba(0, 0, 0, 120)), + shadow = BoxShadow::new(Point::new(2.0, 2.0), 8.0, 2.0, Color::rgba(0, 0, 0, 60), BoxShadowKind::Outer), ) { - text(size = 18.0) { "count = "; count.clone() } - text(color = colors::muted()) { "double = "; doubled.clone() } + text(size = 18.0, color = Color::rgb(0, 0, 0)) { "count = "; count.clone() } + text(color = Color::rgba(0, 0, 0, 200)) { "double = "; doubled.clone() } } } } @@ -56,8 +60,10 @@ fn CounterApp(title: &'static str) -> impl IntoView { #[component] fn CounterActions(count: Signal) -> impl IntoView { view! { - row(gap = 8.0) { + row(gap = 16.0) { button( + background = Color::rgba(255, 255, 255, 90), + color = Color::rgba(0, 0, 0, 255), on_press = { let count = count.clone(); move |_| { @@ -69,6 +75,8 @@ fn CounterActions(count: Signal) -> impl IntoView { } button( + background = Color::rgba(255, 255, 255, 90), + color = Color::rgba(0, 0, 0, 255), on_press = { let count = count.clone(); move |_| { @@ -80,6 +88,8 @@ fn CounterActions(count: Signal) -> impl IntoView { } button( + background = Color::rgba(255, 255, 255, 90), + color = Color::rgba(0, 0, 0, 255), on_press = { let count = count.clone(); move |_| { diff --git a/lib/ruin_app/src/lib.rs b/lib/ruin_app/src/lib.rs index 7eafeae..acfc719 100644 --- a/lib/ruin_app/src/lib.rs +++ b/lib/ruin_app/src/lib.rs @@ -11,19 +11,19 @@ use std::collections::HashMap; use std::error::Error; use std::future::Future; use std::iter; -use std::time::Instant; use std::marker::PhantomData; use std::rc::Rc; +use std::time::Instant; use ruin_reactivity::effect; use ruin_runtime::queue_future; use ruin_ui::{ - Border, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, HitTarget, InteractionTree, - KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutCache, LayoutSnapshot, PlatformEvent, - PointerButton, PointerEvent, PointerEventKind, PointerRouter, Quad, RoutedPointerEvent, - RoutedPointerEventKind, ScrollbarStyle, TextFontFamily, TextSpan, TextSpanWeight, TextStyle, - TextSystem, TextWrap, UiSize, WindowController, WindowSpec, WindowUpdate, - layout_snapshot_with_cache, + Border, BoxShadow, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, HitTarget, + InteractionTree, KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutCache, LayoutSnapshot, + PlatformEvent, PointerButton, PointerEvent, PointerEventKind, PointerRouter, Quad, + RoutedPointerEvent, RoutedPointerEventKind, ScrollbarStyle, TextFontFamily, TextSpan, + TextSpanWeight, TextStyle, TextSystem, TextWrap, UiSize, WindowController, WindowSpec, + WindowUpdate, layout_snapshot_with_cache, }; use ruin_ui_platform_wayland::start_wayland_ui; @@ -59,6 +59,11 @@ impl Window { self } + pub fn min_size(mut self, min_width: f32, min_height: f32) -> Self { + self.spec = self.spec.min_inner_size(UiSize::new(min_width, min_height)); + self + } + pub fn base_color(mut self, color: Color) -> Self { self.spec = self.spec.base_color(color); self @@ -169,6 +174,8 @@ impl MountedApp { let bindings = Rc::new(RefCell::new(EventBindings::default())); let shortcuts = Rc::new(RefCell::new(Vec::::new())); let current_title = Rc::new(RefCell::new(None::)); + let last_min_size = Rc::new(RefCell::new(None::)); + let explicit_min_size = app_window.spec.min_inner_size; let mut input_state = InputState::new(); let mut pointer_router = PointerRouter::new(); @@ -181,6 +188,7 @@ impl MountedApp { let bindings = Rc::clone(&bindings); let shortcuts = Rc::clone(&shortcuts); let current_title = Rc::clone(¤t_title); + let last_min_size = Rc::clone(&last_min_size); let root = Rc::clone(&root); let render_state = Rc::clone(&render_state); let text_selection = Rc::clone(&input_state.text_selection); @@ -208,6 +216,7 @@ impl MountedApp { let LayoutSnapshot { mut scene, interaction_tree: next_interaction_tree, + root_min_size, } = layout_snapshot_with_cache( version, viewport, @@ -216,6 +225,21 @@ impl MountedApp { &mut layout_cache.borrow_mut(), ); let layout_us = t_layout.elapsed().as_micros(); + + // Compute and send min-size hint: max of layout min and explicit window min. + let desired_min = UiSize::new( + root_min_size + .width + .max(explicit_min_size.map_or(0.0, |s| s.width)), + root_min_size + .height + .max(explicit_min_size.map_or(0.0, |s| s.height)), + ); + let current_min = *last_min_size.borrow(); + if current_min != Some(desired_min) { + let _ = window.update(WindowUpdate::new().min_inner_size(Some(desired_min))); + *last_min_size.borrow_mut() = Some(desired_min); + } let effect_us = t_effect.elapsed().as_micros(); tracing::debug!( @@ -716,6 +740,16 @@ impl IntoBorder for (f32, Color) { } } +pub trait IntoShadow { + fn into_shadow(self) -> BoxShadow; +} + +impl IntoShadow for BoxShadow { + fn into_shadow(self) -> BoxShadow { + self + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TextRole { Body, @@ -862,6 +896,25 @@ impl ContainerBuilder { self } + pub fn shadow(mut self, shadow: impl IntoShadow) -> Self { + self.element = self.element.shadow(shadow.into_shadow()); + self + } + + pub fn min_width(mut self, min_width: f32) -> Self { + self.element = self.element.min_width(min_width); + self + } + + pub fn min_height(mut self, min_height: f32) -> Self { + self.element = self.element.min_height(min_height); + self + } + + pub fn min_size(self, min_width: f32, min_height: f32) -> Self { + self.min_width(min_width).min_height(min_height) + } + pub fn widget_ref(mut self, widget_ref: WidgetRef) -> Self { self.widget_ref = Some(widget_ref.element_id.clone()); self @@ -921,6 +974,20 @@ impl ScrollBoxBuilder { self } + pub fn min_width(mut self, min_width: f32) -> Self { + self.element = self.element.min_width(min_width); + self + } + + pub fn min_height(mut self, min_height: f32) -> Self { + self.element = self.element.min_height(min_height); + self + } + + pub fn min_size(self, min_width: f32, min_height: f32) -> Self { + self.min_width(min_width).min_height(min_height) + } + pub fn scrollbar_style(mut self, style: ScrollbarStyle) -> Self { self.element = self.element.scrollbar_style(style); self @@ -1121,6 +1188,8 @@ impl TextBuilder { #[derive(Default)] pub struct ButtonBuilder { on_press: Option, + background: Option, + color: Option, } impl ButtonBuilder { @@ -1129,26 +1198,43 @@ impl ButtonBuilder { self } + pub fn background(mut self, color: Color) -> Self { + self.background = Some(color); + self + } + + pub fn color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } + pub fn children(self, children: impl TextChildren) -> View { let id = allocate_element_id(); let label = children.into_text(); - let view = View::from_element( - Element::column() + let view = View::from_element({ + let elt = Element::column() .id(id) - .padding(Edges::symmetric(14.0, 10.0)) - .background(surfaces::interactive()) - .corner_radius(10.0) + .padding(Edges::symmetric(14.0, 10.0)); + + let elt = if let Some(background) = self.background { + elt.background(background) + } else { + elt.background(surfaces::interactive()) + }; + + elt.corner_radius(10.0) .cursor(CursorIcon::Pointer) .focusable(true) .child( Element::spans( [TextSpan::new(label).weight(TextSpanWeight::Medium)], - TextStyle::new(18.0, colors::text()).with_line_height(21.6), + TextStyle::new(18.0, self.color.unwrap_or(colors::text())) + .with_line_height(21.6), ) .pointer_events(false) .focusable(false), - ), - ); + ) + }); match self.on_press { Some(handler) => view.with_press_handler(id, handler), @@ -1988,8 +2074,9 @@ pub mod prelude { Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget, Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View, WidgetRef, Window, block, button, colors, column, component, context_provider, provide, - row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource, use_shortcut, - use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, view, + row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource, + use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, + view, }; pub use ruin_ui::{ Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton, diff --git a/lib/ui/src/layout.rs b/lib/ui/src/layout.rs index 02e8df9..efb8564 100644 --- a/lib/ui/src/layout.rs +++ b/lib/ui/src/layout.rs @@ -27,6 +27,11 @@ pub fn layout_scene_with_text_system( pub struct LayoutSnapshot { pub scene: SceneSnapshot, pub interaction_tree: InteractionTree, + /// Minimum size the root element needs in logical units. Accounts for + /// `min_width`/`min_height` constraints and the natural minimum of fixed- + /// size content. Scroll boxes contribute only their own explicit/min size + /// on the scroll axis, not their content. + pub root_min_size: UiSize, } #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -242,11 +247,13 @@ pub fn layout_snapshot_with_cache( "layout snapshot perf" ); } + let root_min_size = element_min_size(root); LayoutSnapshot { scene, interaction_tree: InteractionTree { root: interaction_root, }, + root_min_size, } } @@ -817,17 +824,26 @@ fn layout_container_children( let child_insets = content_insets(&child.style); let main_inset = main_axis_padding(child_insets, element.style.direction); let cross_inset = cross_axis_padding(child_insets, element.style.direction); - // style.width/height are content-box; add insets to get the outer (allocated) size. - let cross = child_cross_size(child, element.style.direction) + // min-size constraints (content-box) converted to outer. + let min_cross_outer = child_min_cross_size(child, element.style.direction) .map(|c| (c + cross_inset).max(0.0)) - .unwrap_or(available_cross) - .clamp(0.0, available_cross); + .unwrap_or(0.0); + let min_main_outer = child_min_main_size(child, element.style.direction) + .map(|m| (m + main_inset).max(0.0)) + .unwrap_or(0.0); + // style.width/height are content-box; add insets to get the outer (allocated) size. + // min-cross acts as floor; allow exceeding available_cross only when min demands it. + let cross = child_cross_size(child, element.style.direction) + .map(|c| (c + cross_inset).max(0.0).max(min_cross_outer)) + .unwrap_or(available_cross.max(min_cross_outer)) + .min(available_cross.max(min_cross_outer)); let explicit_main = child_main_size(child, element.style.direction) - .map(|main| (main + main_inset).max(0.0)); + .map(|main| (main + main_inset).max(0.0).max(min_main_outer)); let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0; let measured_main = explicit_main.unwrap_or_else(|| { if is_flex { - 0.0 + // Flex items get their share of remaining space later; record min floor now. + min_main_outer } else { perf_stats.intrinsic_calls += 1; if child.text_node().is_some() { @@ -848,7 +864,8 @@ fn layout_container_children( if let Some(intrinsic_started) = intrinsic_started { perf_stats.intrinsic_ms += intrinsic_started.elapsed().as_secs_f64() * 1_000.0; } - intrinsic + // Apply min-main floor to intrinsic result. + intrinsic.max(min_main_outer) } }); if is_flex { @@ -868,11 +885,18 @@ fn layout_container_children( 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 { - if flex_total <= 0.0 { + let allocated = if flex_total <= 0.0 { 0.0 } else { remaining_main * (child_flex_weight(child) / flex_total) - } + }; + // Apply min-main floor to flex allocation. + let child_insets = content_insets(&child.style); + let main_inset = main_axis_padding(child_insets, element.style.direction); + let min_main_outer = child_min_main_size(child, element.style.direction) + .map(|m| (m + main_inset).max(0.0)) + .unwrap_or(0.0); + allocated.max(min_main_outer) } else { measured.main }; @@ -1091,6 +1115,17 @@ fn intrinsic_size_inner( // available_size is the outer size the parent is offering. // All returned sizes are outer (content + insets). + let min_outer_w = element + .style + .min_width + .map(|mw| mw + horizontal_insets(insets)) + .unwrap_or(0.0); + let min_outer_h = element + .style + .min_height + .map(|mh| mh + vertical_insets(insets)) + .unwrap_or(0.0); + if let Some(text) = element.text_node() { // Measure at content dimensions: use explicit if set, else available minus insets. let content_w = element @@ -1109,16 +1144,18 @@ fn intrinsic_size_inner( ); // Return outer: explicit (content) + insets, or measured + insets for auto. return UiSize::new( - element.style.width.unwrap_or(measured.width) + horizontal_insets(insets), - element.style.height.unwrap_or(measured.height) + vertical_insets(insets), + (element.style.width.unwrap_or(measured.width) + horizontal_insets(insets)) + .max(min_outer_w), + (element.style.height.unwrap_or(measured.height) + vertical_insets(insets)) + .max(min_outer_h), ); } if let Some(image) = element.image_node() { let resolved = resolve_image_element_size(element, image.resource.size()); return UiSize::new( - resolved.width + horizontal_insets(insets), - resolved.height + vertical_insets(insets), + (resolved.width + horizontal_insets(insets)).max(min_outer_w), + (resolved.height + vertical_insets(insets)).max(min_outer_h), ); } @@ -1141,12 +1178,14 @@ fn intrinsic_size_inner( if element.children.is_empty() { return UiSize::new( - explicit_width + (explicit_width .map(|w| w + horizontal_insets(insets) + scroll_gutter) - .unwrap_or(horizontal_insets(insets) + scroll_gutter), - explicit_height + .unwrap_or(horizontal_insets(insets) + scroll_gutter)) + .max(min_outer_w), + (explicit_height .map(|h| h + vertical_insets(insets)) - .unwrap_or(vertical_insets(insets)), + .unwrap_or(vertical_insets(insets))) + .max(min_outer_h), ); } @@ -1159,12 +1198,159 @@ fn intrinsic_size_inner( ); UiSize::new( - explicit_width + (explicit_width .map(|w| w + horizontal_insets(insets) + scroll_gutter) - .unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter), - explicit_height + .unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter)) + .max(min_outer_w), + (explicit_height .map(|h| h + vertical_insets(insets)) - .unwrap_or(intrinsic_content.height + vertical_insets(insets)), + .unwrap_or(intrinsic_content.height + vertical_insets(insets))) + .max(min_outer_h), + ) +} + +/// Computes the minimum logical size that the element's content requires. +/// +/// Unlike [`intrinsic_size`], this function is designed for window minimum-size +/// propagation. The key difference is scroll boxes: a vertically-scrolling box +/// does not propagate its content height to the window minimum — only its own +/// explicit `height` or `min_height` contributes. Flex children without an +/// explicit size or `min_*` constraint contribute 0 on the main axis (they +/// can shrink). Text nodes with no explicit size contribute 0 (they wrap). +/// +/// The result is in the same coordinate space as [`intrinsic_size`] (outer / +/// including padding and border insets). +pub fn element_min_size(element: &Element) -> UiSize { + let insets = content_insets(&element.style); + let min_outer_w = element + .style + .min_width + .map(|mw| (mw + horizontal_insets(insets)).max(0.0)) + .unwrap_or(0.0); + let min_outer_h = element + .style + .min_height + .map(|mh| (mh + vertical_insets(insets)).max(0.0)) + .unwrap_or(0.0); + + // Text: can wrap to any width; only explicit size or min_* constrain the window. + if element.text_node().is_some() { + let w = element + .style + .width + .map(|w| w + horizontal_insets(insets)) + .unwrap_or(0.0) + .max(min_outer_w); + let h = element + .style + .height + .map(|h| h + vertical_insets(insets)) + .unwrap_or(0.0) + .max(min_outer_h); + return UiSize::new(w, h); + } + + // Image: natural size counts as a minimum. + if let Some(image) = element.image_node() { + let resolved = resolve_image_element_size(element, image.resource.size()); + return UiSize::new( + (resolved.width + horizontal_insets(insets)).max(min_outer_w), + (resolved.height + vertical_insets(insets)).max(min_outer_h), + ); + } + + let explicit_w = element.style.width; + let explicit_h = element.style.height; + + // Scroll box: content scrolls on the vertical axis; use only explicit/min height. + // Width still accumulates child widths (no horizontal scroll yet). + if element.scroll_box_node().is_some() { + let scroll_gutter = element + .scroll_box_node() + .map_or(0.0, |sb| sb.scrollbar.gutter_width.max(0.0)); + let w = explicit_w + .map(|w| w + horizontal_insets(insets) + scroll_gutter) + .unwrap_or(horizontal_insets(insets) + scroll_gutter) + .max(min_outer_w); + let h = explicit_h + .map(|h| h + vertical_insets(insets)) + .unwrap_or(vertical_insets(insets)) + .max(min_outer_h); + return UiSize::new(w, h); + } + + // Regular container: sum/max children's minimums. + if element.children.is_empty() { + return UiSize::new( + explicit_w + .map(|w| w + horizontal_insets(insets)) + .unwrap_or(horizontal_insets(insets)) + .max(min_outer_w), + explicit_h + .map(|h| h + vertical_insets(insets)) + .unwrap_or(vertical_insets(insets)) + .max(min_outer_h), + ); + } + + let gap_total = element.style.gap * element.children.len().saturating_sub(1) as f32; + let children_min = match element.style.direction { + FlexDirection::Column => { + let mut width: f32 = 0.0; + let mut height = gap_total; + for child in &element.children { + let child_min = element_min_size(child); + // Flex children with no explicit height contribute their min_height only; + // if that is also unset they contribute 0 (they can shrink freely). + let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none(); + width = width.max(child_min.width); + if !skip_main { + height += child_min.height; + } else { + // Still count the min_height floor even for flex children. + let child_insets = content_insets(&child.style); + let min_h = child + .style + .min_height + .map(|mh| mh + vertical_insets(child_insets)) + .unwrap_or(0.0); + height += min_h; + } + } + UiSize::new(width, height) + } + FlexDirection::Row => { + let mut width = gap_total; + let mut height: f32 = 0.0; + for child in &element.children { + let child_min = element_min_size(child); + let skip_main = child.style.flex_grow > 0.0 && child.style.width.is_none(); + if !skip_main { + width += child_min.width; + } else { + let child_insets = content_insets(&child.style); + let min_w = child + .style + .min_width + .map(|mw| mw + horizontal_insets(child_insets)) + .unwrap_or(0.0); + width += min_w; + } + height = height.max(child_min.height); + } + UiSize::new(width, height) + } + }; + + UiSize::new( + explicit_w + .map(|w| w + horizontal_insets(insets)) + .unwrap_or(children_min.width + horizontal_insets(insets)) + .max(min_outer_w), + explicit_h + .map(|h| h + vertical_insets(insets)) + .unwrap_or(children_min.height + vertical_insets(insets)) + .max(min_outer_h), ) } @@ -1597,6 +1783,20 @@ fn child_cross_size(child: &Element, direction: FlexDirection) -> Option { } } +fn child_min_main_size(child: &Element, direction: FlexDirection) -> Option { + match direction { + FlexDirection::Row => child.style.min_width, + FlexDirection::Column => child.style.min_height, + } +} + +fn child_min_cross_size(child: &Element, direction: FlexDirection) -> Option { + match direction { + FlexDirection::Row => child.style.min_height, + FlexDirection::Column => child.style.min_width, + } +} + fn child_flex_weight(child: &Element) -> f32 { if child.style.flex_grow > 0.0 { child.style.flex_grow diff --git a/lib/ui/src/lib.rs b/lib/ui/src/lib.rs index 69e6bd1..adff8aa 100644 --- a/lib/ui/src/lib.rs +++ b/lib/ui/src/lib.rs @@ -31,7 +31,8 @@ pub use interaction::{ pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers}; pub use layout::{ HitTarget, InteractionTree, LayoutCache, LayoutNode, LayoutPath, LayoutSnapshot, ScrollMetrics, - TextHitTarget, layout_snapshot, layout_snapshot_with_cache, layout_snapshot_with_text_system, + TextHitTarget, element_min_size, layout_snapshot, layout_snapshot_with_cache, + layout_snapshot_with_text_system, }; pub use layout::{layout_scene, layout_scene_with_text_system}; pub use platform::{ diff --git a/lib/ui/src/tree.rs b/lib/ui/src/tree.rs index 719ecb5..bfde6d8 100644 --- a/lib/ui/src/tree.rs +++ b/lib/ui/src/tree.rs @@ -182,6 +182,8 @@ pub struct Style { pub direction: FlexDirection, pub width: Option, pub height: Option, + pub min_width: Option, + pub min_height: Option, pub flex_grow: f32, pub gap: f32, pub padding: Edges, @@ -200,6 +202,8 @@ impl Default for Style { direction: FlexDirection::Column, width: None, height: None, + min_width: None, + min_height: None, flex_grow: 0.0, gap: 0.0, padding: Edges::ZERO, @@ -332,6 +336,20 @@ impl Element { self } + pub fn min_width(mut self, min_width: f32) -> Self { + self.style.min_width = Some(min_width.max(0.0)); + self + } + + pub fn min_height(mut self, min_height: f32) -> Self { + self.style.min_height = Some(min_height.max(0.0)); + self + } + + pub fn min_size(self, min_width: f32, min_height: f32) -> Self { + self.min_width(min_width).min_height(min_height) + } + pub fn flex(mut self, flex_grow: f32) -> Self { self.style.flex_grow = flex_grow.max(0.0); self @@ -532,6 +550,8 @@ impl Hash for Style { self.direction.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); + self.min_height.map(f32::to_bits).hash(state); self.flex_grow.to_bits().hash(state); self.gap.to_bits().hash(state); self.padding.hash(state);