Min size layout constraints
This commit is contained in:
@@ -795,6 +795,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let LayoutSnapshot {
|
||||
scene,
|
||||
interaction_tree: next_interaction_tree,
|
||||
..
|
||||
} = build_snapshot(
|
||||
viewport,
|
||||
version,
|
||||
@@ -1077,6 +1078,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let LayoutSnapshot {
|
||||
scene,
|
||||
interaction_tree: next_interaction_tree,
|
||||
..
|
||||
} = build_snapshot(
|
||||
viewport,
|
||||
version,
|
||||
|
||||
@@ -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<i32>) -> 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<i32>) -> 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<i32>) -> impl IntoView {
|
||||
}
|
||||
|
||||
button(
|
||||
background = Color::rgba(255, 255, 255, 90),
|
||||
color = Color::rgba(0, 0, 0, 255),
|
||||
on_press = {
|
||||
let count = count.clone();
|
||||
move |_| {
|
||||
|
||||
@@ -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(¤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<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,
|
||||
|
||||
@@ -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<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn child_min_main_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
match direction {
|
||||
FlexDirection::Row => child.style.min_width,
|
||||
FlexDirection::Column => child.style.min_height,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_min_cross_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
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
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -182,6 +182,8 @@ pub struct Style {
|
||||
pub direction: FlexDirection,
|
||||
pub width: Option<f32>,
|
||||
pub height: Option<f32>,
|
||||
pub min_width: Option<f32>,
|
||||
pub min_height: Option<f32>,
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user