From 96df4928c796b9fa9640a4834419d67954127d1f Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:04:28 +0300 Subject: [PATCH] ui: fit the context menu above the command area; right-button control actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rows are 22 px (as the grip popup), the edit menu drops Select All and Zoom Extents (both stay on the idle menu, matching the commercial solutions' edit menu), and cursor-anchored panels now reserve the overlaid prompt lines (CLIPROMPTLINES) as well as the input row, so a long menu is never hidden behind the command area — it scrolls inside the visible canvas instead. The automation port gains `pointer_right_press` / `pointer_right_release` actions so scripts and MCP clients can exercise the context menu. Co-Authored-By: Claude Opus 5 --- src/app/control/actions.rs | 7 ++++++- src/app/view/mod.rs | 2 +- src/app/view/overlay.rs | 10 +++++----- src/ui/command_line.rs | 18 ++++++++++++++++++ src/ui/popup/context_menu.rs | 12 +++++++++--- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 8c45f84d6..b7b73d36b 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -267,7 +267,8 @@ impl OpenCADStudio { "mtext_cancel" => Message::MTextCancel, "text_input" => Message::TextInlineInput(string(req, "value")?.into()), "text_commit" => Message::TextInlineOk, - "pointer_move" | "pointer_press" | "pointer_release" => { + "pointer_move" | "pointer_press" | "pointer_release" | "pointer_right_press" + | "pointer_right_release" => { let x = req["x"] .as_f64() .filter(|v| v.is_finite()) @@ -289,6 +290,10 @@ impl OpenCADStudio { let event = match name { "pointer_press" => self.update(Message::ViewportLeftPress), "pointer_release" => self.update(Message::ViewportLeftRelease), + // Right button: the context menu / Enter behaviour chosen in + // Options (see `Message::ViewportRightRelease`). + "pointer_right_press" => self.update(Message::ViewportRightPress), + "pointer_right_release" => self.update(Message::ViewportRightRelease), _ => Task::none(), }; return Ok(Task::batch([move_task, event])); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index b5712158f..1ef0dcc27 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1603,7 +1603,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::command_line::history_max_height(self.win_size.1), ) + 72.0 } else { - 34.0 + 34.0 + self.command_line.overlay_lines_height() }; // Quick Properties: stay near the selection cursor, flipping around // it as needed to remain inside the visible drawing area. diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 37b28d79a..7d99b6de4 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -994,6 +994,7 @@ impl Widget for ClampedPin<'_> { &layout::Limits::new(Size::ZERO, safe), ); let content = node.size(); + let bottom_inset = self.bottom_inset; let max_x = (max.width - MARGIN - content.width).max(MARGIN); let right = self.anchor.x + self.gap + self.offset.x; @@ -1006,8 +1007,7 @@ impl Widget for ClampedPin<'_> { max_x }; - let max_y = - (max.height - self.bottom_inset - MARGIN - content.height).max(MARGIN); + let max_y = (max.height - bottom_inset - MARGIN - content.height).max(MARGIN); let below = self.anchor.y + self.gap + self.offset.y; let above = self.anchor.y - self.gap - content.height; let y = if below <= max_y { @@ -1182,7 +1182,7 @@ pub(super) fn viewport_context_menu_overlay( .align_y(iced::Center); let enabled = !children.is_empty(); let mut btn = button(content) - .padding([4, 12]) + .padding([3, 12]) .width(Fill) .height(Length::Fixed(MENU_ROW_H)) .style(move |theme: &Theme, status| context_menu_row_style(theme, status, is_hl)); @@ -1263,9 +1263,9 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element< } let mut btn = button(content) .padding(iced::Padding { - top: 4.0, + top: 3.0, right: 12.0, - bottom: 4.0, + bottom: 3.0, left: 12.0 + indent, }) .width(Fill) diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index fa5cf5865..a703c3800 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -500,6 +500,24 @@ impl CommandLine { e.pinned || (self.fade_ms > 0 && e.created_at.elapsed().as_secs_f32() < self.fade_secs()) } + /// Height of the overlaid prompt lines currently shown above the input + /// row (CLIPROMPTLINES, minus lines that have faded out), in pixels. Lets + /// cursor-anchored panels stay clear of the whole command area, not just + /// the input row. + pub fn overlay_lines_height(&self) -> f32 { + if self.cliprompt_lines == 0 || self.history_open { + return 0.0; + } + let visible = self + .history + .iter() + .filter(|e| self.entry_visible(e)) + .count() + .min(self.cliprompt_lines as usize); + // Text size 12 (line height ≈ 15.6) plus the 1 px padding each side. + visible as f32 * 18.0 + } + /// Visible overlay count respecting CLIPROMPTLINES (0–50). Used in tests. #[cfg(test)] pub fn visible_history_count(&self) -> usize { diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 6d5905389..567aeb693 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -20,7 +20,7 @@ use crate::t; /// Fixed row height used by the renderer, so `ContextMenu::default_row_y` /// (which anchors the default row under the cursor) agrees with the layout. -pub const MENU_ROW_H: f32 = 24.0; +pub const MENU_ROW_H: f32 = 22.0; /// Height of a separator row (1 px line plus its vertical padding). pub const MENU_SEP_H: f32 = 7.0; /// Vertical padding inside the menu panel above the first row. @@ -576,7 +576,11 @@ fn idle_rows( // ── Navigation ───────────────────────────────────────────────────── rows.push(MenuRow::Item(cmd(t!("Pan").into_owned(), "PAN"))); rows.push(MenuRow::Item(cmd(t!("Zoom").into_owned(), "ZOOM"))); - rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + // The edit menu is long already; Zoom Extents stays on the idle menu + // (and on the middle button's double-click), as in AutoCAD. + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + } rows.push(MenuRow::Separator); // ── Selection tools ──────────────────────────────────────────────── @@ -594,7 +598,9 @@ fn idle_rows( MenuAction::DeselectAll, ))); } - rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + } rows.push(MenuRow::Item(MenuItem::new( t!("Quick Select...").into_owned(), MenuAction::QuickSelect,