git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit9da0c19d8fb2bae7b295a4eca66a82e8f894235d
parent3f133ce924
authorClaude <noreply@anthropic.com>
date2026-10-05 06:57
fix(parameters_bg): cut an inline label only when it does not fit

The pane sizes its label column to the widest label by
measure_text_width, then cut each label to as many characters as the
column holds M's. In a proportional face a label is far narrower than
that many M's, so a label the column was sized for lost its tail.
Under the default Berkeley Mono (every glyph an M wide) it never showed;
it surfaced in CI, where the fallback face's "M" is a pixel wider than
in the container the workflow was checked in: "Count", 29 px in a
72 px column, became "C..." and
a_hover_lifts_the_label_and_lights_only_its_own_carves could not find
its label. Locally it held with no room to spare (5 columns, 5 chars).

A label is now drawn whole when it measures within the column, by the
same measure the column is sized with, and otherwise cut to its longest
head with "..." that fits (fit_tail).

an_inline_label_is_cut_only_when_it_does_not_fit: twelve i's fit a
102 px column and the old code drew "iiiiiii..."; a label too long for
any column is cut and what is left fits. Fails on the old truncation,
passes on the new; suite green with default and all features under
-D warnings, and with CCE_NO_FALLBACK_FONTS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwZ8nBQYEej9ic3tVC5v7Z

 src/widget/container/parameters_bg.rs | 51 ++++++++++++++++++++++++++++++++---
 1 file changed, 47 insertions(+), 4 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index dc3c0b3..4b87517 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1175,11 +1175,11 @@ impl ParametersBg {
             if self.inline_row(i) {
                 // The pane's own label, in the column beside an unlabelled
                 // control: vertically centred on the row, tail-truncated to
-                // the column.
-                let em = crate::widget::display::measure_text_width("M", &family, size).max(1.0);
-                let cols = ((lw - Self::LABEL_GAP) / em).floor().max(0.0) as usize;
+                // the column — by the measure the column is sized with, so
+                // a label it was sized to hold is never cut (it was cut to
+                // as many chars as the column holds M's).
                 labels.push(TextLabel {
-                    text: crate::widget::display::truncate_tail(name, cols),
+                    text: fit_tail(name, lw - Self::LABEL_GAP, &family, size),
                     x: r.0,
                     y: r.1 + (r.3 - size) * 0.5,
                     font_size: size,
@@ -3466,6 +3466,21 @@ fn parse_slider_range(ptype: &str) -> (f32, f32) {
     (0.0, 2.0)
 }
 
+/// `text` whole if it measures within `avail` px, else its longest head
+/// with "..." that does.
+fn fit_tail(text: &str, avail: f32, family: &str, size: f32) -> String {
+    use crate::widget::display::{measure_text_width, truncate_tail};
+    let fits = |t: &str| measure_text_width(t, family, size) <= avail;
+    if fits(text) {
+        return text.to_string();
+    }
+    (0..text.chars().count())
+        .rev()
+        .map(|n| truncate_tail(text, n))
+        .find(|t| fits(t))
+        .unwrap_or_default()
+}
+
 fn parse_hex_to_rgb(s: &str) -> Option<[u8; 3]> {
     crate::color::parse_hex_bytes(s).map(|[r, g, b, _]| [r, g, b])
 }
@@ -4702,6 +4717,34 @@ mod tests {
         assert_eq!(p.hover_row, None);
     }
 
+    /// An inline label is cut only when it does not fit its column, by the
+    /// measure the column is sized with. It used to be cut to as many
+    /// characters as the column holds M's: in a proportional face a label
+    /// of narrow letters is far narrower than that many M's, so a label the
+    /// column was sized to hold lost its tail. On a runner whose fallback
+    /// face drew "M" a pixel wider, "Count" became "C..." in a column with
+    /// 30 px to spare.
+    #[test]
+    fn an_inline_label_is_cut_only_when_it_does_not_fit() {
+        use crate::widget::display::measure_text_width;
+        let narrow = "iiiiiiiiiiii";
+        let long = "A parameter name far too long for any label column of this pane";
+        let mut p = panel_with(&[(narrow, "x", "text"), ("Count", "3", "spinbox:0:10"), (long, "y", "text")]);
+        WidgetHost::set_rect(&mut p, 0.0, 0.0, 300.0, 400.0);
+        let (family, size) = ParametersBg::inline_label_font();
+        let avail = p.label_col_w() - ParametersBg::LABEL_GAP;
+        assert!(avail > 0.0, "the pane lays its labels out inline");
+        let texts: Vec<String> = p.own_text_labels().into_iter().map(|l| l.text).collect();
+        for name in [narrow, "Count"] {
+            if measure_text_width(name, &family, size) <= avail {
+                assert!(texts.iter().any(|t| t == name), "{name:?} fits its {avail} px column and is drawn whole: {texts:?}");
+            }
+        }
+        assert!(measure_text_width(long, &family, size) > avail, "the long label cannot fit");
+        let cut = texts.iter().find(|t| t.starts_with("A p") && t.ends_with("...")).expect("the long label is cut");
+        assert!(measure_text_width(cut, &family, size) <= avail, "and what is left fits: {cut:?}");
+    }
+
     /// The hover cue is the row's LABEL and the row's OWN carves lit through
     /// the tint channel — and nothing else in the pane's paint changes with
     /// it. A hover adds no geometry: it can neither close the pane plate's