Truncate dock tab labels that do not fit their tab

A tab squeezed below its natural width painted its whole label centred and only clipped it, so long names ran over the tab's edges and under its neighbours. The label is now cut with '…' inside the same padding the natural width is measured with, and a cut tab names its panel in full on hover.
This commit is contained in:
Marco Arnulfo committed 2026-10-13 00:48:41 +02:00
1 parent 6006abb757
commit 5c28add6cf
2 files changed
+59 -3

No files matched your search

+10 -3
View File
@@ -9,6 +9,9 @@ use crate::{
use super::{Action, DockError, DropTarget, Layout, Node, Permissions, Placement, resolve_drop};
/// Horizontal room around a tab's label, both when measuring its natural width and when cutting it.
const TAB_PADDING: f32 = 24.0;
/// Application-supplied chrome. Body renderers retain their own UI style and contents.
#[derive(Clone, Debug)]
pub struct DockStyle {
@@ -609,7 +612,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
.into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, self.style.font.clone())
.size()
.x
+ 24.0
+ TAB_PADDING
})
.collect();
let fit = tabs::fit(&widths, active, (strip.width() - header_width).max(0.0), 40.0, 22.0);
@@ -671,12 +674,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let policy = (self.permissions)(panel);
let label = (self.label)(panel);
let color = if active { self.style.text } else { self.style.inactive_text };
// A tab squeezed below its natural width cuts its label with '…' inside the padding its
// natural width was measured with, instead of letting it run under its neighbours.
let galley = self.content.label(ui, panel, &label, &self.style.font, color).into_galley(
ui,
Some(egui::TextWrapMode::Extend),
f32::INFINITY,
Some(egui::TextWrapMode::Truncate),
(rect.width() - TAB_PADDING).max(0.0),
self.style.font.clone(),
);
let elided = galley.elided;
let response = tabs::Tab::new(self.id.with(("tab", panel)), &label, active)
.sense(if policy.movable { Sense::click_and_drag() } else { Sense::click() })
.focus_stroke(Stroke::new(1.0, self.style.accent))
@@ -685,6 +691,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
painter.rect_filled(rect, 0.0, if active || response.hovered() { self.style.active_background } else { self.style.tab_background });
painter.galley(rect.center() - galley.size() * 0.5, galley.clone(), color);
});
let response = if elided { response.on_hover_text(&label) } else { response };
self.register_interaction(ui.ctx(), response.id);
if response.clicked() {
self.output.actions.push(Action::Activate { panel: panel.clone() });
+49
View File
@@ -1090,3 +1090,52 @@ fn popup_action_labels_use_custom_visual_text_and_logical_accessibility() {
);
}
}
#[test]
fn tab_labels_that_do_not_fit_are_truncated_inside_their_tab_and_named_in_a_tooltip() {
let ctx = Context::default();
ctx.all_styles_mut(|style| style.interaction.tooltip_delay = 0.0);
let layout = Layout { root: Some(tabs(&[1, 2, 3], 0)), floating: vec![] };
let label = |p: &u32| format!("Eigenschaften und Anpassungen {p}");
let run = |events: Vec<Event>| {
let input = RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(240.0, 200.0))), events, ..Default::default() };
let mut output = ctx.run_ui(input, |ui| {
DockArea::new(Id::new("truncate")).show(
ui,
&layout,
&DockStyle::from_ui(ui),
label,
|_| Permissions::default(),
|ui, _| {
ui.label("Body");
},
);
});
output.textures_delta.clear();
output.shapes
};
run(vec![]);
let shapes = run(vec![]);
let tab = |p: u32| ctx.read_response(Id::new("truncate").with(("tab", &p))).unwrap().rect;
for p in 1..=3 {
let tab = tab(p);
// The clip rect would hide an overflowing label; what is laid out must itself fit.
let painted: Vec<Rect> = shapes
.iter()
.filter(|shape| matches!(&shape.shape, egui::Shape::Text(_)))
.map(|shape| shape.shape.visual_bounding_rect())
.filter(|r| tab.contains(r.center()))
.collect();
assert_eq!(painted.len(), 1, "tab {p}: {painted:?}");
assert!(tab.contains_rect(painted[0]), "tab {p}: label {:?} overflows {tab:?}", painted[0]);
}
let at = tab(2).center();
run(vec![Event::PointerMoved(at)]);
let shapes = run(vec![]);
let full = label(&2);
// An elided galley still reports its whole text, so the cut tab label alone would match.
assert!(
shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Text(text) if text.galley.text() == full && !text.galley.elided)),
"a truncated tab names its panel in full on hover"
);
}