GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(layout): a labeled control's preferred height counts the label its rect eats; Toggle::with_slide
Slider, RangeSlider, Spinbox, Dropdown, TextBox and Slider2D keep the rect they are
assigned and draw their detached label INSIDE it (`inflates_label_rect` false), so a
layout strategy that sized them by `preferred_height` — the bare
`style.control.<name>.height` — left the control the label's leftovers: a 16px slider
became a 2px line under an 18px label strip. `Adapted::preferred_height` now adds the
label strip for those widgets, so a child assigned its preferred height ends up with
its full intrinsic content height under either label convention (the inflating kind
grows past the rect on set_rect instead, and is unchanged). Float3 drops the strip
from its own intrinsic size accordingly. Test: preferred_height_of_a_labeled_control_
leaves_the_content_height_intact.
A multiline TextBox declares no intrinsic height — the host sizes it — instead of the
single-row height that made strategies squash it to one line.
Toggle gains `with_slide(bool)`, the per-widget override of the config-wide slide
style, the way Slider's `with_band` overrides `style.control.slider.style`; the
gallery uses it to show both looks side by side.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/widget/display/float3.rs | 4 +++-
src/widget/input/checkbox.rs | 25 +++++++++++++++++++++----
src/widget/input/text_box.rs | 5 +++++
src/widget/model.rs | 42 +++++++++++++++++++++++++++++++++++++++++-
4 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 5d3e351..6380636 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -194,8 +194,10 @@ impl Layout for Float3 {
4.0
}
+ /// The three rows alone: the adapter adds the detached-label strip itself
+ /// (`Adapted::preferred_height`), as it does for every non-inflating widget.
fn intrinsic_size(&self) -> Option<Size> {
- Some(Size::new(0.0, Float3::preferred_height(self.label.is_some())))
+ Some(Size::new(0.0, Float3::preferred_height(false)))
}
fn rect_assigned(&mut self, rect: Rect) {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index e97038d..5f986b9 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -206,6 +206,9 @@ pub struct Toggle {
/// (`set_toggled`, `set_value_string`) snap it, so only user interaction
/// animates.
slide_t: f32,
+ /// Per-widget slide-style override; `None` follows the DE config
+ /// (`style.control.toggle.style`).
+ slide_override: Option<bool>,
}
impl Toggle {
@@ -219,9 +222,17 @@ impl Toggle {
justify: Justification::Center,
raised: crate::layout::control_relief(),
slide_t: 0.0,
+ slide_override: None,
})
}
+ /// The slide style (config `style.control.toggle.style = "slide"`, or
+ /// `with_slide` per widget): a half-width button gliding between the
+ /// ends instead of the rocker.
+ pub fn slide(&self) -> bool {
+ self.slide_override.unwrap_or_else(crate::layout::toggle_slide)
+ }
+
pub fn set_label(&mut self, label: &str) {
self.label = Some(label.to_string());
}
@@ -240,7 +251,7 @@ impl Toggle {
/// right (on) ends by the animated `slide_t`. `None` when the style is
/// off — legacy-view hosts fall back to `rocker_reliefs`.
pub fn slide_button(&self, rect: Rect) -> Option<Rect> {
- if !crate::layout::toggle_slide() {
+ if !self.slide() {
return None;
}
let bw = rect.width * 0.5;
@@ -284,7 +295,7 @@ impl Toggle {
/// style, so on a flat host these overlays plus [`Toggle::flat_carves`]
/// are the ENTIRE control; without them the row was a bare label.
pub fn flat_faces(&self, rect: Rect) -> Vec<(Rect, f32, (bool, bool, bool, bool), [f32; 4])> {
- if crate::layout::toggle_slide() {
+ if self.slide() {
return Vec::new();
}
let radius = crate::layout::toggle_corner_radius();
@@ -395,6 +406,12 @@ impl Adapted<Toggle> {
self.raised = raised;
self
}
+
+ /// Slide style for this widget regardless of the config (see `Toggle::slide`).
+ pub fn with_slide(mut self, slide: bool) -> Self {
+ self.slide_override = Some(slide);
+ self
+ }
}
impl Layout for Toggle {
@@ -435,7 +452,7 @@ impl Paint for Toggle {
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
- let slide = crate::layout::toggle_slide();
+ let slide = self.slide();
// A toggle paints NO fill of its own, in any style: it is worked out
// of the plate it sits on, so the plate's own material shows through
@@ -532,7 +549,7 @@ impl Input for Toggle {
/// (~90ms exponential settle). Programmatic syncs snap instead — see
/// `set_toggled` / `set_value_string` — so only user interaction animates.
fn tick(&mut self, dt: f32, _rect: Rect) -> bool {
- if !crate::layout::toggle_slide() {
+ if !self.slide() {
return false;
}
let target = if self.toggled { 1.0 } else { 0.0 };
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index c1737a3..891b176 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1299,7 +1299,12 @@ impl Layout for TextBox {
4.0
}
+ /// One row for a single-line box; a multiline box has no natural height of its own —
+ /// the host sizes it, and a layout strategy leaves its assigned rect alone.
fn intrinsic_size(&self) -> Option<Size> {
+ if self.multiline {
+ return None;
+ }
Some(Size::new(0.0, crate::layout::textbox_height()))
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index bdf7aca..fa23c57 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1168,8 +1168,20 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
}
}
+ /// The height a layout should allot: the widget's intrinsic content height, plus the
+ /// detached-label strip for widgets whose label eats INTO the assigned rect
+ /// ([`Layout::inflates_label_rect`] false — Slider, Spinbox, Dropdown, ...). Inflating
+ /// widgets grow past the assigned rect on `set_rect` instead, so their strip is not
+ /// counted here. Either way, a widget assigned its preferred height ends up with its
+ /// intrinsic content height — which is what makes the two conventions lay out alike.
fn preferred_height(&self) -> Option<f32> {
- Layout::intrinsic_size(&self.inner).map(|s| s.height)
+ let size = Layout::intrinsic_size(&self.inner)?;
+ let strip = if Layout::inline_label(&self.inner) || Layout::inflates_label_rect(&self.inner) {
+ 0.0
+ } else {
+ self.base.label_offset()
+ };
+ Some(size.height + strip)
}
/// The `WidgetHost::measure` default, except the width consults the intrinsic size when the
@@ -1678,6 +1690,34 @@ mod tests {
Rect { x, y, width: w, height: h }
}
+ /// A labeled control assigned its `preferred_height` keeps its full intrinsic content
+ /// height whichever label convention it follows: the inflating kind (ProgressBar) grows
+ /// past the assigned rect, the eating kind (Slider, Spinbox, Dropdown) has the strip
+ /// counted into the preferred height instead — so a strategy sizing children by
+ /// `preferred_height` never squashes a track to the label's leftovers.
+ #[test]
+ fn preferred_height_of_a_labeled_control_leaves_the_content_height_intact() {
+ use crate::widget::{Dropdown, ProgressBar, Slider, Spinbox};
+ let mut slider = Slider::new().with_label("Gain");
+ let mut spinbox = Spinbox::new(1, 0, 9, 1).with_label("Count");
+ let mut dropdown = Dropdown::new(vec!["a".into()], 0).with_label("Pick");
+ let mut bar = ProgressBar::new(0.5).with_label("Load");
+ let strip = slider.base.label_offset();
+ assert!(strip > 0.0, "a detached label has a strip above the content");
+
+ for (name, w, content) in [
+ ("slider", &mut slider as &mut dyn WidgetHost, crate::layout::slider_height()),
+ ("spinbox", &mut spinbox, crate::layout::spinbox_height()),
+ ("dropdown", &mut dropdown, crate::layout::dropdown_height()),
+ ("progress bar", &mut bar, crate::layout::progressbar_height()),
+ ] {
+ let pref = w.preferred_height().expect(name);
+ w.set_rect(0.0, 0.0, 100.0, pref);
+ let painted = w.rect().3 - crate::widget::label_offset(w);
+ assert!((painted - content).abs() < 0.01, "{name}: content {painted} after preferred {pref}, wanted {content}");
+ }
+ }
+
#[test]
fn narrow_widget_lays_out_and_paints_through_the_adapter() {
// A pure narrow-trait widget tree (Col + two Dots), wrapped in `Adapted`, is laid out by