GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(group): a member's detached label is in the hull
WidgetHost::detached_label_rect: where a detached label is drawn — the strip
above the content, as wide as the text, measured in the detached-label font.
A rect is the content's width, so a label wider than its control (a
StatusDot's) ran past the rect and a Group's wall cut through the text;
the hull now unions the label's box with the rect. No height change: the
strip was already in the rect. Test with a labelled StatusDot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/widget/container/group.rs | 38 +++++++++++++++++++++++++++++++++-----
src/widget/mod.rs | 7 +++++++
src/widget/model.rs | 12 ++++++++++++
3 files changed, 52 insertions(+), 5 deletions(-)
diff --git a/src/widget/container/group.rs b/src/widget/container/group.rs
index 3e07449..42c997a 100644
--- a/src/widget/container/group.rs
+++ b/src/widget/container/group.rs
@@ -142,10 +142,19 @@ impl Group {
// always its content plus this strip"), so the lasso wraps the label
// by taking the rect as is. Subtracting the strip here again pushed
// every labelled member's hull one strip too high, and the tab with it.
- hull = Some(match hull {
- None => (x, y, x + ww, y + hh),
- Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x + ww), y1.max(y + hh)),
- });
+ // Widthwise the rect is the content's: a label wider than its control
+ // (a StatusDot's) runs past it, so the label's own box joins the hull
+ // or the wall cuts through the text.
+ let mut grow = |x: f32, y: f32, w: f32, h: f32| {
+ hull = Some(match hull {
+ None => (x, y, x + w, y + h),
+ Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x + w), y1.max(y + h)),
+ });
+ };
+ grow(x, y, ww, hh);
+ if let Some(l) = w.detached_label_rect() {
+ grow(l.x, l.y, l.width, l.height);
+ }
}
hull.map(|(x0, y0, x1, y1)| Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 })
}
@@ -305,7 +314,7 @@ impl Input for Group {
#[cfg(test)]
mod tests {
use super::*;
- use crate::widget::{Button, Slider, WidgetHost};
+ use crate::widget::{Button, DotStatus, Slider, StatusDot, WidgetHost};
fn register(ctx: &mut UiContext, w: &mut dyn WidgetHost) -> WidgetId {
let id = w.base().id();
@@ -348,6 +357,25 @@ mod tests {
assert_eq!(f.body.height, 16.0 + strip + 20.0);
}
+ /// A label wider than its control's rect is in the hull: the frame closes past
+ /// the text, not through it.
+ #[test]
+ fn a_members_wide_label_is_in_the_hull() {
+ let mut ctx = UiContext::new();
+ let mut dot = StatusDot::new(DotStatus::Inactive).with_label("StatusDot (inactive)");
+ let strip = dot.label_strip();
+ let size = StatusDot::SIZE;
+ WidgetHost::set_rect(&mut dot, 100.0, 50.0, size, size + strip);
+ let label = dot.detached_label_rect().expect("a detached label");
+ assert!(label.width > size, "the label text is wider than the dot");
+ let ids = vec![register(&mut ctx, &mut dot)];
+ let g = Group::new(ids).with_padding(10.0);
+ let f = g.inner().frame(&ctx).unwrap();
+ assert_eq!(f.body.x, 90.0, "the left is the rect's");
+ assert_eq!(f.body.x + f.body.width, label.x + label.width + 10.0, "the right is the label's");
+ assert_eq!(f.body.height, size + strip + 20.0, "the label strip adds no height: it is in the rect");
+ }
+
/// Fit to plate: a side near the plate's edge takes it (one padding in), a
/// far side keeps the hull, and a corner on the plate's corner is concentric.
#[test]
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index fa939dc..03b9166 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -217,6 +217,13 @@ pub trait WidgetHost {
/// `layout::CONTROL_GAP` between the blocks.
fn label_strip(&self) -> f32 { self.base().label_offset() }
+ /// Where the detached label is drawn: the strip above the content, as wide as the
+ /// label's text. `None` for an unlabeled widget and for an inline label. The label
+ /// may be wider than the widget's rect (a StatusDot's, a Checkbox's) — the rect is
+ /// the content's width, and the text runs past it — so anything wrapping a widget
+ /// as a block (a `Group`'s hull) unions this with the rect.
+ fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { None }
+
fn mark_dirty(&mut self, ctx: &mut UiContext) {
let b = self.base_mut();
if b.dirty {
diff --git a/src/widget/model.rs b/src/widget/model.rs
index c1a1771..fb8fd1f 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1191,6 +1191,18 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() }
}
+ /// The detached label's box, as `base_label_fallback` places the text: at the
+ /// label inset on the strip above the content, measured in the detached-label font.
+ fn detached_label_rect(&self) -> Option<Rect> {
+ if Layout::inline_label(&self.inner) {
+ return None;
+ }
+ let label = self.base.label.as_deref()?;
+ let (fam, size) = crate::layout::control_label_font_detached_parsed();
+ let width = crate::widget::display::measure_text_width(label, &fam, size);
+ Some(Rect { x: self.base.x + Layout::detached_label_inset(&self.inner), y: self.base.y, width, height: self.label_strip() })
+ }
+
/// The `WidgetHost::measure` default, except the width consults the intrinsic size when the
/// widget opts in ([`Layout::intrinsic_measure_width`] — Dropdown's `auto_width`).
fn measure(&self, constraints: crate::widget::LayoutConstraints, _ctx: &UiContext) -> crate::widget::Size {