Min size layout constraints

This commit is contained in:
2026-03-23 19:43:14 -04:00
parent e327516e5e
commit 5f67c06083
6 changed files with 365 additions and 45 deletions

View File

@@ -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<M: Mountable> MountedApp<M> {
let bindings = Rc::new(RefCell::new(EventBindings::default()));
let shortcuts = Rc::new(RefCell::new(Vec::<ShortcutBinding>::new()));
let current_title = Rc::new(RefCell::new(None::<String>));
let last_min_size = Rc::new(RefCell::new(None::<UiSize>));
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<M: Mountable> MountedApp<M> {
let bindings = Rc::clone(&bindings);
let shortcuts = Rc::clone(&shortcuts);
let current_title = Rc::clone(&current_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<M: Mountable> MountedApp<M> {
let LayoutSnapshot {
mut scene,
interaction_tree: next_interaction_tree,
root_min_size,
} = layout_snapshot_with_cache(
version,
viewport,
@@ -216,6 +225,21 @@ impl<M: Mountable> MountedApp<M> {
&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<T>(mut self, widget_ref: WidgetRef<T>) -> 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<PressHandler>,
background: Option<Color>,
color: Option<Color>,
}
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,