From 6475096ff1849c048ee87bae91540294de386815 Mon Sep 17 00:00:00 2001 From: MajedAlaejem Date: Fri, 23 Oct 2026 18:42:01 +0300 Subject: [PATCH] feat: add context menu support for editor tabs and empty space in chrome UI --- crates/ui-egui/src/chrome.rs | 86 +++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index b1916998c4..b6f4ec4a91 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -475,6 +475,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { (title.clone(), ui.painter().layout_no_wrap(title, theme::semibold(title_size), color)) }) .collect(); + let doc_paths: Vec> = app.session.documents().iter().map(|d| d.path.clone()).collect(); let widths: Vec = tabs.iter().map(|(_, g)| g.size().x + 50.0).collect(); let overflow = widths.iter().sum::() > strip.width(); let room = strip.width() - if overflow { MORE } else { 0.0 }; @@ -482,6 +483,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { let mut x = strip.left(); let mut activate = None; let mut close = None; + let mut clicked_cmd: Option<(&'static str, serde_json::Value)> = None; let mut hovering_art = false; for (i, ((_, galley), w)) in tabs.iter().zip(&widths).enumerate().skip(first) { if x + w > strip.left() + room && Some(i) != active { @@ -503,6 +505,9 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { 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.clone(), t.text); + resp.context_menu(|ui| { + render_tab_menu(ui, true, i, doc_paths.get(i).and_then(|p| p.as_ref()), &mut close, &mut clicked_cmd); + }); hovering_art |= crate::document_drag::hover_tab(app, &resp, i, xr.expand(3.0)); if xresp.clicked() { close = Some(i); @@ -511,6 +516,14 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { } x += w; } + let empty_right = if overflow { strip.right() - MORE } else { strip.right() }; + if empty_right > x { + let empty_r = egui::Rect::from_min_max(egui::pos2(x, strip.top()), egui::pos2(empty_right, strip.bottom() - 1.0)); + let empty_resp = ui.interact(empty_r, ui.id().with("tabs-empty-space"), Sense::click()); + empty_resp.context_menu(|ui| { + render_tab_menu(ui, false, active.unwrap_or(0), None, &mut close, &mut clicked_cmd); + }); + } crate::document_drag::finish_tabs(ui.ctx(), hovering_art); // The » button: every open document, the active one checked. if overflow { @@ -528,7 +541,9 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { } }); } - if let Some(i) = close { + if let Some((cmd, params)) = clicked_cmd { + crate::menus::invoke(app, cmd, params); + } else if let Some(i) = close { if let Err(e) = crate::unsaved::close(app, i) { app.status(e); } @@ -548,6 +563,75 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { } } +/// Helper to render the tab or strip context menu with macOS native styling. +fn render_tab_menu( + ui: &mut egui::Ui, + is_tab: bool, + tab_idx: usize, + doc_path: Option<&String>, + close: &mut Option, + clicked_cmd: &mut Option<(&'static str, serde_json::Value)>, +) { + ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 1.0); + ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); + + let menu_row = |ui: &mut egui::Ui, text: &str, enabled: bool| -> bool { + let text = format!(" {text}"); + let rich = egui::RichText::new(text).size(13.0); + let btn = egui::Button::new(rich) + .min_size(egui::vec2(185.0, 22.0)) + .frame_when_inactive(false); + let resp = ui.add_enabled(enabled, btn); + resp.clicked() + }; + + let menu_sep = |ui: &mut egui::Ui| { + let (rect, _) = ui.allocate_exact_size(egui::vec2(185.0, 7.0), egui::Sense::hover()); + let t = Tokens::get(ui.ctx()); + ui.painter().line_segment( + [egui::pos2(rect.left() + 8.0, rect.center().y), egui::pos2(rect.right() - 8.0, rect.center().y)], + egui::Stroke::new(1.0, t.divider.gamma_multiply(0.5)), + ); + }; + + menu_row(ui, tl!("Consolidate All to Here"), false); + + if menu_row(ui, tl!("Close"), true) { + *close = Some(tab_idx); + ui.close(); + } + if menu_row(ui, tl!("Close All"), true) { + *clicked_cmd = Some(("file.closeAll", serde_json::json!({}))); + ui.close(); + } + if is_tab { + menu_row(ui, tl!("Move to New Window"), false); + } + + menu_sep(ui); + + if menu_row(ui, tl!("New Document…"), true) { + *clicked_cmd = Some(("file.newDialog", serde_json::json!({}))); + ui.close(); + } + if menu_row(ui, tl!("Open Document…"), true) { + *clicked_cmd = Some(("file.open", serde_json::json!({}))); + ui.close(); + } + + if is_tab { + menu_sep(ui); + let reveal_label = if cfg!(target_os = "macos") { tl!("Reveal in Finder") } else { tl!("Show in Folder") }; + let has_path = doc_path.is_some(); + if menu_row(ui, reveal_label, has_path) { + if let Some(path) = doc_path { + *clicked_cmd = Some(("file.reveal", serde_json::json!({ "path": path }))); + } + ui.close(); + } + } +} + /// The isolation mode breadcrumb bar. It is drawn over the top of `area` (the canvas), after the /// canvas, so entering isolation mode does not change the canvas's size (#3126). pub fn isolation_bar(app: &mut VectorcraftApp, ui: &mut Ui, area: egui::Rect) {