git.lucas.co / cce-terminal
terminal emulator
git clone https://git.lucas.co/cce-terminal.git

commit7e1bf93640b01d8147c13b32c9b8c9e751cd1a1d
parent1beeb79e60
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 19:56
Open the terminal menu on right-click

A right press on the grid opens the corner control's menu at the pointer,
as a context menu. The rows are shared with the corner menu (menu_rows), so
Copy is offered for a live selection and the dispatch path is unchanged.
While a TUI has mouse reporting on, the press is reported instead; Shift
bypasses reporting, so Shift+right-click still reaches the menu. No edge
clamping: the menu is its own popup, and the positioner keeps it on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/main.rs       | 15 ++++++++++++++-
 src/plate_menu.rs | 42 +++++++++++++++++++++++++++++-------------
 2 files changed, 43 insertions(+), 14 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 2d9acda..4493d93 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -23,7 +23,8 @@
 //! circular trigger on the top-right that cce-designer's panes wear, opening
 //! a menu of the actions a menubar-less terminal has nowhere else to put —
 //! copy/paste, text zoom, scrollback and terminal resets, a new window, and
-//! the tabs. A tab ([`Tab`]) is one shell on one pty with its own `Term`,
+//! the tabs. A right-click on the grid opens the same menu at the pointer
+//! (Shift+right-click while a TUI has mouse reporting on). A tab ([`Tab`]) is one shell on one pty with its own `Term`,
 //! title, view offset and bell; the window shows the active one and the
 //! menu lists them as a radio group (the designer's dock-tab language) with
 //! New Tab / Close Tab. There is no tab bar: the title carries `[i/n]` while
@@ -1362,6 +1363,18 @@ impl Application for TerminalApp {
             *needs_rebuild = true;
             return None;
         }
+        // A right press opens it as a context menu at the pointer — unless
+        // the app owns the pointer, where the press is reported instead
+        // (Shift bypasses reporting, so the menu stays reachable there too).
+        // The selection is left alone, so Copy is offered for it.
+        if button == MouseButton::Right
+            && state == ElementState::Pressed
+            && !(self.mouse_reporting() && !self.selecting)
+        {
+            self.open_context_menu(px, py);
+            *needs_rebuild = true;
+            return None;
+        }
 
         // Application-owned pointer: report and stop — no selection, no
         // middle-paste (Shift bypasses via mouse_reporting).
diff --git a/src/plate_menu.rs b/src/plate_menu.rs
index 959c18b..0215d4e 100644
--- a/src/plate_menu.rs
+++ b/src/plate_menu.rs
@@ -1,7 +1,8 @@
 //! The root plate's corner control: the DE's circular menu trigger (the same
 //! affordance cce-designer's panes and cce-files' preview pane carry, on
 //! `cce_ui::widget::plate_dock`) riding the top-right of the terminal
-//! window, and the menu it opens.
+//! window, and the menu it opens. A right-click on the grid opens the same
+//! menu at the pointer, as a context menu.
 //!
 //! The terminal has one plate — the window itself — so the toolkit's dock
 //! vocabulary (collapse, detach) does not apply; the rows are the actions a
@@ -71,12 +72,34 @@ impl TerminalApp {
         context_menu::is_visible() && !self.plate_menu_actions.is_empty()
     }
 
-    /// Open the corner menu under its control. Rows are contextual: Copy
-    /// only with a selection to copy, Reset Text Size only while zoomed.
-    /// The menu hangs off the control's RIGHT edge, leftwards — anchored on
-    /// the left as the designer's panes do, it would run off the window.
+    /// Open the corner menu under its control. The menu hangs off the
+    /// control's RIGHT edge, leftwards — anchored on the left as the
+    /// designer's panes do, it would run off the window.
     pub(crate) fn open_plate_menu(&mut self) {
         let Some((cx, cy)) = self.plate_corner_center() else { return };
+        let (options, actions) = self.menu_rows();
+        // The menu sizes itself from its labels on `show`, so place it once
+        // to learn the width, then again with its right edge on the control.
+        let top = cy + CORNER_R;
+        context_menu::show(0.0, top, options.clone(), 0, NO_TARGET);
+        let left = (cx + CORNER_R - context_menu::w()).max(0.0);
+        context_menu::show(left, top, options, 0, NO_TARGET);
+        self.plate_menu_actions = actions;
+    }
+
+    /// Open the same menu as a context menu, its top-left at the pointer
+    /// (a right-click on the grid). No clamping here: the menu is its own
+    /// popup surface, and the positioner's constraint adjustment keeps it on
+    /// the output when the click is near the window's right or bottom edge.
+    pub(crate) fn open_context_menu(&mut self, px: f32, py: f32) {
+        let (options, actions) = self.menu_rows();
+        context_menu::show(px, py, options, 0, NO_TARGET);
+        self.plate_menu_actions = actions;
+    }
+
+    /// The rows, contextual: Copy only with a selection to copy, Reset Text
+    /// Size only while zoomed, Close Tab only with another tab to fall to.
+    fn menu_rows(&self) -> (Vec<String>, Vec<PlateMenuAction>) {
         let mut options: Vec<String> = Vec::new();
         let mut actions: Vec<PlateMenuAction> = Vec::new();
         let mut row = |label: &str, action: PlateMenuAction| {
@@ -111,14 +134,7 @@ impl TerminalApp {
         }
         row("-", PlateMenuAction::Separator);
         row("New Window", PlateMenuAction::NewWindow);
-
-        // The menu sizes itself from its labels on `show`, so place it once
-        // to learn the width, then again with its right edge on the control.
-        let top = cy + CORNER_R;
-        context_menu::show(0.0, top, options.clone(), 0, NO_TARGET);
-        let left = (cx + CORNER_R - context_menu::w()).max(0.0);
-        context_menu::show(left, top, options, 0, NO_TARGET);
-        self.plate_menu_actions = actions;
+        (options, actions)
     }
 
     pub(crate) fn close_plate_menu(&mut self) {