Tab overflow list scrolls to every open document (#4017)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
heojunfo
2026-10-26 11:22:54 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 581c79dcb1
commit 452c034329
3 changed files with 99 additions and 6 deletions
+10 -6
View File
@@ -512,7 +512,9 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
x += w;
}
crate::document_drag::finish_tabs(ui.ctx(), hovering_art);
// The » button: every open document, the active one checked.
// The » button: every open document, the active one checked. The list scrolls once it
// reaches the bottom of the window, so every document of a large session stays reachable
// (#4017).
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.scope_id().with("tabs-more"), Sense::click());
@@ -520,12 +522,14 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
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();
widgets::menu_scroll(ui, |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 {
+2
View File
@@ -173,6 +173,8 @@ mod tests_sysclip_emf;
#[cfg(test)]
mod tests_sysclip_probe;
#[cfg(test)]
mod tests_tab_overflow;
#[cfg(test)]
mod tests_taskbar;
#[cfg(test)]
mod tests_transparencygrid;
+87
View File
@@ -0,0 +1,87 @@
//! The » list of the document tab strip with more documents than fit in the window (#4017).
use egui::{Event, PointerButton, Pos2};
use serde_json::json;
use vectorcraft_engine::Session;
use crate::{VectorcraftApp, chrome};
const SIZE: egui::Vec2 = egui::vec2(1000.0, 700.0);
/// Run a tab-strip frame and return the visible texts with their screen rects.
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, events: Vec<Event>) -> Vec<(String, egui::Rect)> {
let mut out = ctx.run_ui(
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, SIZE)), time: Some(time), events, ..Default::default() },
|ui| {
egui::CentralPanel::default().show(ui, |ui| chrome::doc_tabs(app, ui));
},
);
out.textures_delta.clear();
let screen = egui::Rect::from_min_size(Pos2::ZERO, SIZE);
// Texts nested in shape lists too, kept only where their clip rect shows them whole.
fn walk(s: &egui::Shape, clip: egui::Rect, v: &mut Vec<(String, egui::Rect)>) {
match s {
egui::Shape::Text(t) => {
let r = egui::Rect::from_min_size(t.pos, t.galley.size());
if clip.contains_rect(r) {
v.push((t.galley.text().to_owned(), r));
}
}
egui::Shape::Vec(list) => list.iter().for_each(|s| walk(s, clip, v)),
_ => {}
}
}
let mut texts = vec![];
for c in &out.shapes {
walk(&c.shape, c.clip_rect.intersect(screen), &mut texts);
}
texts
}
fn click(at: Pos2, pressed: bool) -> Event {
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }
}
/// Every document of a large session can be picked from the » list: it scrolls instead of running
/// off the bottom of the window.
#[test]
fn the_tab_overflow_list_scrolls_to_its_last_document() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
for i in 0..60 {
app.run("file.new", json!({"width": 100, "height": 100, "name": format!("Doc {i:02}")})).unwrap();
}
app.session.set_active(0);
app.sync_views();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let texts = frame(&mut app, &ctx, 0.0, vec![]);
let tab = texts.iter().find(|(t, _)| t.starts_with("Doc 00")).map(|(_, r)| *r).unwrap();
// The » button sits at the right end of the strip, level with the tabs.
let more = egui::pos2(SIZE.x - 8.0 - 15.0, tab.center().y);
frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(more)]);
frame(&mut app, &ctx, 0.15, vec![click(more, true)]);
frame(&mut app, &ctx, 0.2, vec![click(more, false)]);
frame(&mut app, &ctx, 0.25, vec![]);
let texts = frame(&mut app, &ctx, 0.3, vec![]);
// The list's rows put a check column before each title; the tabs don't.
let row = |texts: &[(String, egui::Rect)], name: &str| texts.iter().find(|(t, _)| !t.starts_with("Doc") && t.contains(name)).map(|(_, r)| *r);
let first = row(&texts, "Doc 00").unwrap_or_else(|| panic!("the » list is open: {texts:?}"));
assert!(row(&texts, "Doc 59").is_none(), "the last document starts below the bottom of the window");
let wheel = Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -20000.0),
phase: egui::TouchPhase::Move,
modifiers: Default::default(),
source: egui::MouseWheelSource::Unknown,
};
frame(&mut app, &ctx, 0.35, vec![Event::PointerMoved(first.center()), wheel]);
let mut texts = vec![];
for i in 4..40 {
texts = frame(&mut app, &ctx, f64::from(i) * 0.1, vec![]);
}
let last = row(&texts, "Doc 59").unwrap_or_else(|| panic!("the wheel scrolls the list to its last document: {texts:?}")).center();
frame(&mut app, &ctx, 4.0, vec![Event::PointerMoved(last)]);
frame(&mut app, &ctx, 4.1, vec![click(last, true)]);
frame(&mut app, &ctx, 4.2, vec![click(last, false)]);
assert_eq!(app.session.active_index(), Some(59), "the last document is picked from the list");
}