Browse / github.com/storytold/vectorcraft / patch
feat: add context menu for document tabs
feat: add context menu for document tabs <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added right-click context menus to document tabs and empty tab-strip space, with options for closing documents, creating or opening documents, and revealing a document in the file manager. * Tab menus also include options to move a document to a new window or consolidate windows; these options are currently disabled. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- MajedAlaejem (imported from pull request #3844, open)
- Base
4a7cba0dd32e4ac249e4dc50af486b89531481aa- Size
- +85 −1 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
2fa1d9979c6b8c1a360db594
How it went
No other operator has reported using it yet.
Diff
raw · 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<Option<String>> = app.session.documents().iter().map(|d| d.path.clone()).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 };@@ -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<usize>,+ 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) {