Fix #746: document tabs that don't fit are reached from a » list

With more documents open than the tab strip holds, the tabs past its
right edge were cut off with no way to reach them. As in Illustrator, a
» button at the strip's right end now lists every open document (the
active one checked) and picks one, and the strip always shows the active
tab, starting later when it has to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-09 19:06:40 +07:00
1 parent dd1a32bb8e
commit e8255f2bd5
8 files changed
+73 -11

No files matched your search

+66 -11
View File
@@ -323,26 +323,55 @@ fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bool) -> Stri
/// Linux and the web).
const CLOSE_BEFORE_TITLE: bool = cfg!(target_os = "macos");
/// The first of tabs `widths` wide that the strip shows in `room`: the first one, or a later one so
/// that the `active` tab fits.
fn first_tab_shown(widths: &[f32], active: Option<usize>, room: f32) -> usize {
let Some(a) = active else { return 0 };
let mut first = 0;
while first < a && widths.get(first..=a).map_or(0.0, |w| w.iter().sum::<f32>()) > room {
first += 1;
}
first
}
/// Document tab strip: "Name* @ 66.67% (RGB/Preview)". User Interface › Large Tabs makes the tabs
/// taller, with larger titles.
/// taller, with larger titles. Tabs that don't fit are reached from a » button at the strip's
/// right end, which lists every open document, and the active tab is always in view (#746).
pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
/// The width of the » button.
const MORE: f32 = 30.0;
let t = Tokens::get(ui.ctx());
let (height, title_size) = if app.session.prefs.large_tabs { (44.0, 14.0) } else { (35.0, 12.5) };
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
// On the Home screen no tab is the current one.
let active = app.session.active_index().filter(|_| app.ui.home.is_none());
let tabs: Vec<(String, std::sync::Arc<egui::Galley>)> = app
.session
.documents()
.iter()
.enumerate()
.map(|(i, d)| {
let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0);
let title = tab_title(d, zoom, app.ui.view.outline);
let color = if Some(i) == active { t.text_strong } else { t.text_dim };
(title.clone(), ui.painter().layout_no_wrap(title, theme::semibold(title_size), color))
})
.collect();
let widths: Vec<f32> = tabs.iter().map(|(_, g)| g.size().x + 50.0).collect();
let overflow = widths.iter().sum::<f32>() > strip.width();
let room = strip.width() - if overflow { MORE } else { 0.0 };
let first = if overflow { first_tab_shown(&widths, active, room) } else { 0 };
let mut x = strip.left();
let mut activate = None;
let mut close = None;
let active = app.session.active_index();
for (i, d) in app.session.documents().iter().enumerate() {
let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0);
let title = tab_title(d, zoom, app.ui.view.outline);
// On the Home screen no tab is the current one.
let is_active = Some(i) == active && app.ui.home.is_none();
let galley = ui.painter().layout_no_wrap(title, theme::semibold(title_size), if is_active { t.text_strong } else { t.text_dim });
let w = galley.size().x + 50.0;
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(w, strip.height() - 1.0));
for (i, ((_, galley), w)) in tabs.iter().zip(&widths).enumerate().skip(first) {
if x + w > strip.left() + room && Some(i) != active {
break;
}
let is_active = Some(i) == active;
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(*w, strip.height() - 1.0));
let resp = ui.interact(r, ui.id().with(("tab", i)), Sense::click());
if is_active {
ui.painter().rect_filled(r, 0.0, t.panel);
@@ -356,7 +385,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
let xr = egui::Rect::from_center_size(egui::pos2(x_center, r.center().y), vec2(12.0, 12.0));
let xresp = ui.interact(xr.expand(3.0), ui.id().with(("tabx", i)), Sense::click());
icons::paint(ui, "x", xr, if xresp.hovered() { t.text_strong } else { t.text });
ui.painter().galley(egui::pos2(title_left, r.center().y - galley.size().y / 2.0), galley, t.text);
ui.painter().galley(egui::pos2(title_left, r.center().y - galley.size().y / 2.0), galley.clone(), t.text);
if xresp.clicked() {
close = Some(i);
} else if resp.clicked() {
@@ -364,6 +393,22 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
}
x += w;
}
// The » button: every open document, the active one checked.
if overflow {
let more = egui::Rect::from_min_max(egui::pos2(strip.right() - MORE, strip.top()), egui::pos2(strip.right(), strip.bottom() - 1.0));
let resp = ui.interact(more, ui.id().with("tabs-more"), Sense::click());
ui.painter().rect_filled(more, 0.0, if resp.hovered() { t.hover.gamma_multiply(0.4) } else { t.tab_strip });
icons::paint(ui, "chevrons-right", egui::Rect::from_center_size(more.center(), vec2(14.0, 14.0)), t.text);
let resp = resp.on_hover_text(tl!("Show all open documents"));
egui::Popup::menu(&resp).show(|ui| {
for (i, (title, _)) in tabs.iter().enumerate() {
if widgets::menu_item_name(ui, title, true, Some(i) == active) {
activate = Some(i);
ui.close();
}
}
});
}
if let Some(i) = close {
if let Err(e) = crate::unsaved::close(app, i) {
app.status(e);
@@ -764,6 +809,16 @@ pub fn hint_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
#[cfg(test)]
mod tests {
/// With more tabs than fit, the strip starts late enough to show the active one (#746).
#[test]
fn the_active_tab_stays_in_view() {
let widths = [100.0; 10];
assert_eq!(super::first_tab_shown(&widths, Some(2), 350.0), 0, "it fits from the start");
assert_eq!(super::first_tab_shown(&widths, Some(9), 350.0), 7, "the last three tabs shown");
assert_eq!(super::first_tab_shown(&widths, None, 350.0), 0);
assert_eq!(super::first_tab_shown(&widths, Some(4), 50.0), 4, "a tab wider than the room still shows");
}
use serde_json::json;
use vectorcraft_engine::Session;
+1
View File
@@ -2648,6 +2648,7 @@
Swatch Tiles Mosaicos de muestras
Swatches Muestras
Switch workspace Cambiar espacio de trabajo
Show all open documents Mostrar todos los documentos abiertos
Symbol Instance Instancia de símbolo
Symbol Libraries Bibliotecas de símbolos
Symbol Libraries (on the roadmap) Bibliotecas de símbolos (en la hoja de ruta)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2645,6 +2645,7 @@
Swatch Tiles Carreaux de nuances
Swatches Nuancier
Switch workspace Changer d’espace de travail
Show all open documents Afficher tous les documents ouverts
Symbol Instance Occurrence de symbole
Symbol Libraries Bibliothèques de symboles
Symbol Libraries (on the roadmap) Bibliothèques de symboles (prévu dans la feuille de route)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2648,6 +2648,7 @@
Swatch Tiles Riquadri campioni
Swatches Campioni
Switch workspace Cambia area di lavoro
Show all open documents Mostra tutti i documenti aperti
Symbol Instance Istanza di simbolo
Symbol Libraries Librerie simboli
Symbol Libraries (on the roadmap) Librerie simboli (in programma)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2422,6 +2422,7 @@
Swatch Options… スウォッチオプション…
Swatch Tiles スウォッチタイル
Switch workspace ワークスペースを切り替え
Show all open documents 開いているすべてのドキュメントを表示
Symbol Libraries (on the roadmap) シンボルライブラリ (今後対応予定)
Symbol Screener Tool シンボルスクリーナーツール
Symbol Scruncher Tool シンボルスクランチャーツール
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2644,6 +2644,7 @@
Swatch Tiles Плитки образцов
Swatches Образцы
Switch workspace Переключить рабочую среду
Show all open documents Показать все открытые документы
Symbol Instance Экземпляр символа
Symbol Libraries Библиотеки символов
Symbol Libraries (on the roadmap) Библиотеки символов (в плане развития)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2565,6 +2565,7 @@
Swatch Tiles 色板拼贴
Swatches 色板
Switch workspace 切换工作区
Show all open documents 显示所有打开的文档
Symbol Instance 符号实例
Symbol Libraries 符号库
Symbol Libraries (on the roadmap) 符号库 (已列入路线图)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2651,6 +2651,7 @@
Swatch Tiles 色票拼貼
Swatches 色票
Switch workspace 切換工作區
Show all open documents 顯示所有開啟的文件
Symbol Instance 符號範例
Symbol Libraries 符號資料庫
Symbol Libraries (on the roadmap) 符號資料庫 (已列入路線圖)
Can't render this file because it contains an unexpected character in line 6 and column 47.