mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 14:32:14 +00:00
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:
1 parent
6006abb757
commit
5c28add6cf
2 files changed
+59
-3
No files matched your search
@@ -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() });
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user