From 03a7320a0cfc50c7008416c403e16fce5e734fdf Mon Sep 17 00:00:00 2001 From: HassanKhan Date: Wed, 11 Nov 2026 15:19:26 +0500 Subject: [PATCH 1/2] =?UTF-8?q?Document=20tabs:=20a=20middle-click=20close?= =?UTF-8?q?s=20the=20tab,=20as=20the=20=C3=97=20does=20(#6506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/chrome/tabs.rs | 6 ++-- crates/ui-egui/src/chrome/tests.rs | 50 ++++++++++++++++++++++++++++++ docs/ui-parity.md | 1 + 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/chrome/tabs.rs b/crates/ui-egui/src/chrome/tabs.rs index 4d5f75821..4bd8754ff 100644 --- a/crates/ui-egui/src/chrome/tabs.rs +++ b/crates/ui-egui/src/chrome/tabs.rs @@ -136,7 +136,7 @@ pub(super) fn first_tab_shown(widths: &[f32], active: Option, room: f32) /// strip shows is the highlighted one. A tab dragged more than [`crate::doc_windows::TEAR_OFF`] /// above or below the strip floats its view in a window under the pointer; one dragged along the /// strip moves its document among the others (`document.move`, #5663), a line marking where it -/// will go. +/// will go. A middle-click on a tab closes it, as its × does (#6506). pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { /// The width of the » button. const MORE: f32 = 30.0; @@ -200,7 +200,9 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { 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.clone(), t.text); hovering_art |= crate::document_drag::hover_tab(app, &resp, win, xr.expand(3.0)); - if xresp.clicked() { + // A middle-click anywhere on the tab closes it as the × does (#6506), so a modified + // document still asks to save first. + if xresp.clicked() || resp.middle_clicked() || xresp.middle_clicked() { close = Some(win); } else if resp.clicked() { activate = Some(win); diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 15c856cd8..71f9b42e2 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -505,6 +505,56 @@ fn a_tabs_close_box_sits_where_the_platform_puts_it() { assert!(super::CLOSE_BEFORE_TITLE == cfg!(target_os = "macos")); } +/// A middle-click on a document tab closes it as the × does (#6506): a saved document closes, and +/// a modified one asks to save first instead of losing its changes. +#[test] +fn a_middle_click_on_a_tab_closes_it_as_the_close_box_does() { + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + for _ in 0..3 { + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + } + // The second document has unsaved changes; the third stays the current one. + app.session.set_active(1); + app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + app.session.set_active(2); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 200.0)); + let frame = |app: &mut crate::VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| super::doc_tabs(app, ui)); + out.textures_delta.clear(); + let mut titles = vec![]; + for c in &out.shapes { + if let egui::Shape::Text(t) = &c.shape + && t.visual_bounding_rect().width() > 30.0 + { + titles.push(t.visual_bounding_rect()); + } + } + titles.sort_by(|a, b| a.left().total_cmp(&b.left())); + titles + }; + let middle_click = |app: &mut crate::VectorcraftApp, at: egui::Pos2| { + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Middle, pressed, modifiers: Default::default() }; + frame(app, vec![egui::Event::PointerMoved(at), press(true)]); + frame(app, vec![press(false)]) + }; + let titles = frame(&mut app, vec![]); + assert_eq!(titles.len(), 3, "{titles:?}"); + // The modified document's tab: the save prompt comes up and the document stays open. + let titles = middle_click(&mut app, titles[1].center()); + assert_eq!(app.session.documents().len(), 3, "a modified document is not closed without asking"); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::unsaved::KIND), "a modified document asks first"); + app.ui.dialog = None; + // The first, saved document's tab: it closes, and the others stay open. + let uid = |app: &crate::VectorcraftApp, i: usize| app.session.documents()[i].uid; + let (second, third) = (uid(&app, 1), uid(&app, 2)); + middle_click(&mut app, titles[0].center()); + assert_eq!(app.session.documents().len(), 2, "the middle-click closed the first document"); + assert_eq!((uid(&app, 0), uid(&app, 1)), (second, third)); + assert!(app.ui.dialog.is_none(), "a saved document closes without asking"); +} + /// One headless frame of the status bar; returns the artboard navigator's buttons, left to /// right: first, previous, next, last. fn navigator(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec) -> Vec { diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a0182867c..c4aba7105 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -363,6 +363,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po |---|---|---| | Selection artwork drag hovers a visible document tab to activate it, then drops editable artwork on its canvas | done | `tests_document_drag`, `edit.dropSelection`, lossless resource and one-step Undo regressions; overflow and cross-window drag remain separate | | A document tab dragged along the tab strip moves its document; a line marks where it goes, the current document and every document's view stay as they were, and a drag off the strip still floats it | done | #5663, [public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html); `document.move`; `tests::tab_reorder`, `chrome::tests` (drop index with floating and view tabs), `engine` `tests::document_move` | +| A middle-click on a document tab closes it as its × does; a modified document asks to save first (VectorCraft addition, as in PhotoCraft) | done | #6506; `chrome::tests::a_middle_click_on_a_tab_closes_it_as_the_close_box_does`; not scored | | Behaviour | Status | Notes | |---|---|---| From be1ce4d2d7d8dd8e8d88539434b75e3aa89e5548 Mon Sep 17 00:00:00 2001 From: HassanKhan Date: Wed, 11 Nov 2026 15:57:09 +0500 Subject: [PATCH 2/2] =?UTF-8?q?Document=20tabs:=20test=20a=20middle-click?= =?UTF-8?q?=20on=20the=20=C3=97;=20docs=20status=20and=20revision=20histor?= =?UTF-8?q?y=20(#6506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only closing a modified document's last view asks to save, so the docs row and the comment say so. --- crates/ui-egui/src/chrome/tabs.rs | 4 ++-- crates/ui-egui/src/chrome/tests.rs | 9 +++++++++ docs/ui-parity.md | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/chrome/tabs.rs b/crates/ui-egui/src/chrome/tabs.rs index 4bd8754ff..506b149fe 100644 --- a/crates/ui-egui/src/chrome/tabs.rs +++ b/crates/ui-egui/src/chrome/tabs.rs @@ -200,8 +200,8 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { 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.clone(), t.text); hovering_art |= crate::document_drag::hover_tab(app, &resp, win, xr.expand(3.0)); - // A middle-click anywhere on the tab closes it as the × does (#6506), so a modified - // document still asks to save first. + // A middle-click anywhere on the tab closes it as the × does (#6506), so closing a + // modified document's last view still asks to save first. if xresp.clicked() || resp.middle_clicked() || xresp.middle_clicked() { close = Some(win); } else if resp.clicked() { diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 71f9b42e2..452fec1cc 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -553,6 +553,15 @@ fn a_middle_click_on_a_tab_closes_it_as_the_close_box_does() { assert_eq!(app.session.documents().len(), 2, "the middle-click closed the first document"); assert_eq!((uid(&app, 0), uid(&app, 1)), (second, third)); assert!(app.ui.dialog.is_none(), "a saved document closes without asking"); + // A middle-click on the × itself closes too: the third, saved document's. + let titles = frame(&mut app, vec![]); + assert_eq!(titles.len(), 2, "{titles:?}"); + let title = titles[1]; + let x = if super::CLOSE_BEFORE_TITLE { title.left() - 16.0 } else { title.right() + 20.0 }; + middle_click(&mut app, egui::pos2(x, title.center().y)); + assert_eq!(app.session.documents().len(), 1, "the middle-click on the × closed the third document"); + assert_eq!(uid(&app, 0), second); + assert!(app.ui.dialog.is_none(), "a saved document closes without asking"); } /// One headless frame of the status bar; returns the artboard navigator's buttons, left to diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c4aba7105..0e94f38e8 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (a middle-click on a document tab closes it, #6506; previously minor: G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -363,7 +363,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po |---|---|---| | Selection artwork drag hovers a visible document tab to activate it, then drops editable artwork on its canvas | done | `tests_document_drag`, `edit.dropSelection`, lossless resource and one-step Undo regressions; overflow and cross-window drag remain separate | | A document tab dragged along the tab strip moves its document; a line marks where it goes, the current document and every document's view stay as they were, and a drag off the strip still floats it | done | #5663, [public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html); `document.move`; `tests::tab_reorder`, `chrome::tests` (drop index with floating and view tabs), `engine` `tests::document_move` | -| A middle-click on a document tab closes it as its × does; a modified document asks to save first (VectorCraft addition, as in PhotoCraft) | done | #6506; `chrome::tests::a_middle_click_on_a_tab_closes_it_as_the_close_box_does`; not scored | +| A middle-click on a document tab or its × closes it as the × does; closing a modified document's last view asks to save first (VectorCraft addition, as in PhotoCraft) | done | #6506; `chrome::tests::a_middle_click_on_a_tab_closes_it_as_the_close_box_does`; not scored | | Behaviour | Status | Notes | |---|---|---| @@ -508,6 +508,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | A middle-click on a document tab (or its ×) closes it as the × does; closing a document's last view with unsaved changes asks to save first (#6506). VectorCraft addition; scores and estimates unchanged. | | 2026-11-10 | minor | Symbol libraries: Inventory Icons joins Basic Shapes, UI Frames and Map Markers as a fourth built-in library (Sword, Shield, Potion, Key, Gem; no default 9-slice). | | 2026-11-11 | minor | Files that record no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files, importer plug-ins) open and revert in Preferences › Units › General instead of points; native, SVG and DXF files and `.ai`/EPS ruler units are kept (#5820). Scores and estimates unchanged. | | 2026-11-10 | minor | Held numeric spinner arrows keep stepping past the click timeout and stop on release (#6307). Scores and estimates unchanged. |