ui: truncate long workbook titles in the title bar

This commit is contained in:
ismaelezequiel
2026-10-15 12:43:48 -03:00
parent e6b6b34970
commit c6be911b99
2 changed files with 104 additions and 12 deletions
+103 -11
View File
@@ -112,17 +112,8 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) {
app.print_now(json!({}));
}
});
// Centered title.
let title = app.session.active().map(|d| d.display_title()).unwrap_or_default();
let dirty = app.session.active().is_some_and(|d| d.is_dirty());
ui.painter().text(
rect.center(),
Align2::CENTER_CENTER,
format!("{title}{}", if dirty { " •" } else { "" }),
theme::ui_bold(13.0),
t.text,
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let left_end = ui.min_rect().right();
let right = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(8.0);
let __k = icon_button(ui, Icon::Search, t.text_dim, "Search commands (⌘⇧Space)", vec2(28.0, ch));
control_keytip(app, ui, &__k, "commandSearch");
@@ -157,11 +148,58 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) {
act(app, "view.darkMode", json!({}));
}
theme.context_menu(|ui| theme_choices(app, ui));
ui.min_rect().left()
});
// Centred title, cut with "…" so it never runs under the controls on either side.
let title = app.session.active().map(|d| d.display_title()).unwrap_or_default();
let dirty = app.session.active().is_some_and(|d| d.is_dirty());
let max_w = title_max_width(rect.center().x, left_end, right.inner);
paint_title(ui, rect.center(), &title, dirty, max_w, t.text);
});
});
}
/// Lay out the title truncated to fit `max_w` together with the unsaved marker (when `dirty`).
fn title_galleys(
painter: &egui::Painter,
title: &str,
dirty: bool,
max_w: f32,
color: Color32,
) -> (std::sync::Arc<egui::Galley>, Option<std::sync::Arc<egui::Galley>>) {
let font = theme::ui_bold(13.0);
let dot = dirty.then(|| painter.layout_no_wrap(" •".to_string(), font.clone(), color));
let dot_w = dot.as_ref().map_or(0.0, |g| g.size().x);
let mut job = egui::text::LayoutJob::simple_singleline(title.to_string(), font, color);
job.wrap = egui::text::TextWrapping::truncate_at_width((max_w - dot_w).max(0.0));
(painter.layout_job(job), dot)
}
/// Gap kept between the title and the controls beside it.
const TITLE_GAP: f32 = 12.0;
/// The widest the centred title may be without reaching the controls that end at `left_end` or
/// start at `right_start`.
fn title_max_width(center_x: f32, left_end: f32, right_start: f32) -> f32 {
let half = (center_x - left_end).min(right_start - center_x) - TITLE_GAP;
(2.0 * half).max(0.0)
}
/// Paint the workbook title centred on `center`, truncated with "…" to `max_w`. The unsaved
/// marker stays visible however short the title gets.
fn paint_title(ui: &Ui, center: egui::Pos2, title: &str, dirty: bool, max_w: f32, color: Color32) {
let (galley, dot) = title_galleys(ui.painter(), title, dirty, max_w, color);
let dot_w = dot.as_ref().map_or(0.0, |g| g.size().x);
let w = galley.size().x + dot_w;
let left = center.x - w / 2.0;
let top = center.y - galley.size().y / 2.0;
let galley_w = galley.size().x;
ui.painter().galley(pos2(left, top), galley, color);
if let Some(dot) = dot {
ui.painter().galley(pos2(left + galley_w, top), dot, color);
}
}
pub fn show(app: &mut SheetApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let collapsed = app.ui.ribbon_collapsed;
@@ -2498,6 +2536,60 @@ mod title_bar_tests {
let lights_end = MAC_LIGHTS_LEFT + 2.0 * MAC_LIGHT_PITCH + MAC_LIGHT;
assert_eq!(MAC_CONTENT_LEFT - lights_end, margin);
}
#[test]
fn title_fits_between_the_controls() {
// Room limited by the nearer side, symmetric about the centre.
assert_eq!(title_max_width(500.0, 300.0, 900.0), 2.0 * (200.0 - TITLE_GAP));
assert_eq!(title_max_width(500.0, 100.0, 650.0), 2.0 * (150.0 - TITLE_GAP));
// Controls overlapping the centre leave no room rather than a negative width.
assert_eq!(title_max_width(500.0, 520.0, 900.0), 0.0);
}
/// Lay out a title in a real egui frame, returning (title text, title width, dot width).
fn layout_title(title: &str, dirty: bool, max_w: f32) -> (String, f32, f32) {
let ctx = egui::Context::default();
ctx.set_fonts(theme::font_definitions());
let mut out = (String::new(), 0.0, 0.0);
let mut frame = ctx.run_ui(egui::RawInput::default(), |ui| {
let (galley, dot) = title_galleys(ui.painter(), title, dirty, max_w, Color32::WHITE);
// The glyphs actually drawn: `galley.text()` is the untruncated source.
let shown = galley.rows.iter().flat_map(|r| r.glyphs.iter().map(|g| g.chr)).collect();
out = (shown, galley.size().x, dot.map_or(0.0, |d| d.size().x));
});
frame.textures_delta.clear(); // nothing consumes the atlas in a headless test
out
}
#[test]
fn long_title_is_truncated_with_an_ellipsis() {
let long = format!("upload-{}-cleaned (1).xlsx", "3FMyNTYg2gDaANiAAACq20AAAAQMBqug".repeat(4));
let (text, w, _) = layout_title(&long, false, 300.0);
assert!(text.ends_with('…'), "truncated title ends with an ellipsis: {text:?}");
assert!(text.chars().count() < long.chars().count());
assert!(w <= 300.0, "title stays within its room: {w}");
}
#[test]
fn short_title_is_left_whole() {
let (text, _, _) = layout_title("Book1.xlsx", false, 300.0);
assert_eq!(text, "Book1.xlsx");
}
#[test]
fn unsaved_dot_survives_truncation() {
let long = "a very long workbook name ".repeat(20);
let (text, w, dot_w) = layout_title(&long, true, 200.0);
assert!(dot_w > 0.0, "the unsaved marker is laid out");
assert!(text.ends_with('…'));
assert!(w + dot_w <= 200.0, "title and marker fit together: {}", w + dot_w);
}
#[test]
fn no_room_draws_nothing_but_does_not_crash() {
let (_, w, _) = layout_title("Book1.xlsx", true, 0.0);
assert!(w <= 20.0, "a zero-width slot leaves at most the ellipsis: {w}");
}
}
#[cfg(test)]
+1 -1
View File
@@ -477,7 +477,7 @@ fn kept_vml_drawings_are_charged_to_the_kept_total() {
+ wb.sheets.iter().filter_map(|s| s.preserved.as_ref()).map(|p| p.byte_len()).sum::<usize>();
assert!(kept <= one + VML.len() / 2, "kept {kept} bytes");
assert!(wb.sheet(0).unwrap().preserved.as_ref().is_some_and(|p| p.vml.is_some()));
assert!(!wb.sheet(1).unwrap().preserved.as_ref().is_some_and(|p| p.vml.is_some()));
assert!(wb.sheet(1).unwrap().preserved.as_ref().is_none_or(|p| p.vml.is_none()));
assert!(rep.warnings.iter().any(|w| w.contains("\"Other\"") && w.contains("too large")), "{:?}", rep.warnings);
// Both sheets keep their drawings when there is room.
let (wb, _) = read_xlsx(&rich_package_with(CONTENT_TYPES, true)).unwrap();