From 6b4f43321f2e74ead24dff9b3bf1f967e4512d5d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 29 Mar 2026 21:57:33 +0200 Subject: [PATCH 1/3] fix: context menu z-order on canvas terminals Context menus were rendered behind overlapping terminal panels because the shared paint layer used Order::Tooltip, which has higher priority than Order::Foreground where context menus are drawn. Lowering the shared layer to Order::Foreground ensures context menus render on top, since egui draws non-Area paint layers before Area layers (like popups) within the same Order. --- src/terminal/panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/panel.rs b/src/terminal/panel.rs index 898061e..48c4d25 100644 --- a/src/terminal/panel.rs +++ b/src/terminal/panel.rs @@ -579,7 +579,7 @@ impl TerminalPanel { // Full-panel opaque fill so higher-z panels fully occlude lower-z panels. let zoom = transform.scaling; let screen_pr = Rect::from_min_max(transform * pr.min, transform * pr.max); - let shared_layer = egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("term_text")); + let shared_layer = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("term_text")); { // Full-panel fill — occludes everything from lower-z panels. let cp = ui From 9f3db40ad8a39cffca9284d58c1ddd01f0262c29 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 29 Mar 2026 23:49:51 +0200 Subject: [PATCH 2/3] fix: context menu renders above terminals and follows canvas pan/zoom - Keep terminal shared layer at Order::Tooltip for proper z-ordering - Render context menu at Order::Debug (above Tooltip) using custom popup - Store click position in canvas space so menu moves with pan/zoom - Close menu on click-elsewhere --- .claude/worktrees/agent-a653bd6e | 1 + .claude/worktrees/agent-ad2e70be | 1 + src/terminal/panel.rs | 141 +++++++++++++++++++------------ 3 files changed, 87 insertions(+), 56 deletions(-) create mode 160000 .claude/worktrees/agent-a653bd6e create mode 160000 .claude/worktrees/agent-ad2e70be diff --git a/.claude/worktrees/agent-a653bd6e b/.claude/worktrees/agent-a653bd6e new file mode 160000 index 0000000..7ce1b21 --- /dev/null +++ b/.claude/worktrees/agent-a653bd6e @@ -0,0 +1 @@ +Subproject commit 7ce1b21019f65444100c7147bff17a3b6a4f02ef diff --git a/.claude/worktrees/agent-ad2e70be b/.claude/worktrees/agent-ad2e70be new file mode 160000 index 0000000..7d8ba86 --- /dev/null +++ b/.claude/worktrees/agent-ad2e70be @@ -0,0 +1 @@ +Subproject commit 7d8ba862989c646d49fcfc9c2502baee6f636458 diff --git a/src/terminal/panel.rs b/src/terminal/panel.rs index 48c4d25..051fddb 100644 --- a/src/terminal/panel.rs +++ b/src/terminal/panel.rs @@ -97,6 +97,7 @@ pub struct TerminalPanel { /// so accumulated movement can escape snap zones naturally. pub resize_virtual_rect: Option, bell_flash_until: f64, + context_menu_pos: Option, /// When set, reset terminal modes (ALT_SCREEN, MOUSE_MODE) after this time. /// Triggered when Ctrl+C is sent while in ALT_SCREEN — the TUI app is likely /// being killed and won't send cleanup escape sequences. @@ -221,6 +222,7 @@ impl TerminalPanel { drag_virtual_pos: None, resize_virtual_rect: None, bell_flash_until: 0.0, + context_menu_pos: None, pending_mode_reset: None, } } @@ -249,6 +251,7 @@ impl TerminalPanel { drag_virtual_pos: None, resize_virtual_rect: None, bell_flash_until: 0.0, + context_menu_pos: None, pending_mode_reset: None, } } @@ -579,7 +582,7 @@ impl TerminalPanel { // Full-panel opaque fill so higher-z panels fully occlude lower-z panels. let zoom = transform.scaling; let screen_pr = Rect::from_min_max(transform * pr.min, transform * pr.max); - let shared_layer = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("term_text")); + let shared_layer = egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("term_text")); { // Full-panel fill — occludes everything from lower-z panels. let cp = ui @@ -1229,64 +1232,90 @@ impl TerminalPanel { } // Context menu with Copy / Paste / Select All - body_resp.context_menu(|ui| { - let has_sel = self.selection.is_some(); - if ui.add_enabled(has_sel, egui::Button::new("Copy")).clicked() { - if let Some(text) = self.selected_text() { - ui.ctx().copy_text(text); - } - ui.close_menu(); - } - if ui.button("Paste").clicked() { - if let Some(pty) = &self.pty { - if let Ok(mut clipboard) = arboard::Clipboard::new() { - if let Ok(text) = clipboard.get_text() { - let mode = self.input_mode(); - if mode.bracketed_paste { - let mut bytes = Vec::new(); - bytes.extend_from_slice(b"\x1b[200~"); - bytes.extend_from_slice(text.as_bytes()); - bytes.extend_from_slice(b"\x1b[201~"); - pty.write(&bytes); - } else { - pty.write(text.as_bytes()); + // Rendered at Order::Debug so it appears above terminal content (Order::Tooltip). + let menu_id = ui.id().with("ctx_menu").with(self.id); + if body_resp.secondary_clicked() { + // Store click position in canvas space so the menu moves with pan/zoom + let screen_pos = ui.input(|i| i.pointer.latest_pos()); + self.context_menu_pos = screen_pos.map(|p| transform.inverse() * p); + ui.memory_mut(|m| m.toggle_popup(menu_id)); + } + if ui.memory(|m| m.is_popup_open(menu_id)) { + // Convert canvas position back to screen space each frame + let menu_pos = self + .context_menu_pos + .map(|p| transform * p) + .unwrap_or(body_resp.rect.center()); + let area_resp = egui::Area::new(menu_id) + .order(egui::Order::Debug) + .fixed_pos(menu_pos) + .interactable(true) + .show(ui.ctx(), |ui| { + egui::Frame::menu(ui.style()).show(ui, |ui| { + let has_sel = self.selection.is_some(); + if ui.add_enabled(has_sel, egui::Button::new("Copy")).clicked() { + if let Some(text) = self.selected_text() { + ui.ctx().copy_text(text); } + ui.memory_mut(|m| m.close_popup()); } - } - } - ui.close_menu(); - } - if ui.button("Select All").clicked() { - let last_col = (self.last_cols as usize).saturating_sub(1); - let last_row = (self.last_rows as usize).saturating_sub(1); - self.selection = Some((0, 0, last_col, last_row)); - self.selection_display_offset = - scrollbar_state.map(|s| s.display_offset).unwrap_or(0); - ui.close_menu(); - } - ui.separator(); - if ui.button("Clear Scrollback").clicked() { - if let Some(pty) = &self.pty { - pty.write(b"\x1b[3J"); - } - ui.close_menu(); - } - if ui.button("Reset Terminal").clicked() { - if let Some(pty) = &self.pty { - pty.write(b"\x1bc"); - } - ui.close_menu(); - } - ui.separator(); - if ui.button("Rename").clicked() { - ix.action = Some(PanelAction::Rename); - ui.close_menu(); - } - if ui.button("Close").clicked() { - ix.action = Some(PanelAction::Close); - ui.close_menu(); + if ui.button("Paste").clicked() { + if let Some(pty) = &self.pty { + if let Ok(mut clipboard) = arboard::Clipboard::new() { + if let Ok(text) = clipboard.get_text() { + let mode = self.input_mode(); + if mode.bracketed_paste { + let mut bytes = Vec::new(); + bytes.extend_from_slice(b"\x1b[200~"); + bytes.extend_from_slice(text.as_bytes()); + bytes.extend_from_slice(b"\x1b[201~"); + pty.write(&bytes); + } else { + pty.write(text.as_bytes()); + } + } + } + } + ui.memory_mut(|m| m.close_popup()); + } + if ui.button("Select All").clicked() { + let last_col = (self.last_cols as usize).saturating_sub(1); + let last_row = (self.last_rows as usize).saturating_sub(1); + self.selection = Some((0, 0, last_col, last_row)); + self.selection_display_offset = + scrollbar_state.map(|s| s.display_offset).unwrap_or(0); + ui.memory_mut(|m| m.close_popup()); + } + ui.separator(); + if ui.button("Clear Scrollback").clicked() { + if let Some(pty) = &self.pty { + pty.write(b"\x1b[3J"); + } + ui.memory_mut(|m| m.close_popup()); + } + if ui.button("Reset Terminal").clicked() { + if let Some(pty) = &self.pty { + pty.write(b"\x1bc"); + } + ui.memory_mut(|m| m.close_popup()); + } + ui.separator(); + if ui.button("Rename").clicked() { + ix.action = Some(PanelAction::Rename); + ui.memory_mut(|m| m.close_popup()); + } + if ui.button("Close").clicked() { + ix.action = Some(PanelAction::Close); + ui.memory_mut(|m| m.close_popup()); + } + }); + }); + // Close menu when clicking outside + if area_resp.response.clicked_elsewhere() { + ui.memory_mut(|m| m.close_popup()); + self.context_menu_pos = None; } - }); + } ix } From b944a82928431fd6b2ceee7e60291ec81e5588df Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 29 Mar 2026 23:49:59 +0200 Subject: [PATCH 3/3] chore: remove accidentally committed worktree refs --- .claude/worktrees/agent-a653bd6e | 1 - .claude/worktrees/agent-ad2e70be | 1 - 2 files changed, 2 deletions(-) delete mode 160000 .claude/worktrees/agent-a653bd6e delete mode 160000 .claude/worktrees/agent-ad2e70be diff --git a/.claude/worktrees/agent-a653bd6e b/.claude/worktrees/agent-a653bd6e deleted file mode 160000 index 7ce1b21..0000000 --- a/.claude/worktrees/agent-a653bd6e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7ce1b21019f65444100c7147bff17a3b6a4f02ef diff --git a/.claude/worktrees/agent-ad2e70be b/.claude/worktrees/agent-ad2e70be deleted file mode 160000 index 7d8ba86..0000000 --- a/.claude/worktrees/agent-ad2e70be +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7d8ba862989c646d49fcfc9c2502baee6f636458