diff --git a/src/app/update.rs b/src/app/update.rs index 39303586b..f385cd505 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -937,6 +937,15 @@ impl OpenCADStudio { self.focus_cmd_input() } + Message::DynTabNext if self.grip_popup.is_some() => { + if let Some(popup) = self.grip_popup.as_mut() { + if !popup.items.is_empty() { + popup.selected = (popup.selected + 1) % popup.items.len(); + } + } + Task::none() + } + Message::DynTabNext => { let i = self.active_tab; let n = self.tabs[i].dyn_fields.len(); @@ -954,6 +963,17 @@ impl OpenCADStudio { } Message::CommandHistoryPrev => { + // Grip popup wins first — arrow keys walk its items. + if let Some(popup) = self.grip_popup.as_mut() { + if !popup.items.is_empty() { + popup.selected = if popup.selected == 0 { + popup.items.len() - 1 + } else { + popup.selected - 1 + }; + } + return Task::none(); + } // While autocomplete is showing suggestions, ↑ walks up // that list. Otherwise it falls back to recall history. let i = self.active_tab; @@ -967,6 +987,12 @@ impl OpenCADStudio { } Message::CommandHistoryNext => { + if let Some(popup) = self.grip_popup.as_mut() { + if !popup.items.is_empty() { + popup.selected = (popup.selected + 1) % popup.items.len(); + } + return Task::none(); + } let i = self.active_tab; if self.tabs[i].active_cmd.is_none() && self.command_line.autocomplete_next() @@ -1142,6 +1168,15 @@ impl OpenCADStudio { } Message::CommandFinalize => { + // Grip popup open → Enter commits the highlighted item. + if self.grip_popup.is_some() { + let idx = self + .grip_popup + .as_ref() + .map(|p| p.selected) + .unwrap_or(0); + return Task::done(Message::GripMenuPick(idx)); + } // A typed dynamic-input value commits as a point pick // before the plain-Enter (on_enter) path runs. if let Some(task) = self.try_dyn_commit() { @@ -1162,6 +1197,12 @@ impl OpenCADStudio { } Message::CommandEscape => { + // Grip popup intercepts Escape — dismisses the menu + // without doing anything else. + if self.grip_popup.take().is_some() { + self.grip_hover = None; + return Task::none(); + } // Cancel layout rename / context menus first, then fall through. let i_e = self.active_tab; { @@ -2166,6 +2207,14 @@ impl OpenCADStudio { Message::ViewportLeftPress => { let i = self.active_tab; + // Click anywhere outside the popup dismisses it. The + // menu's own buttons live above this mouse_area, so a + // press that reaches here means the cursor is not on + // any menu item. + if self.grip_popup.take().is_some() { + self.grip_hover = None; + return Task::none(); + } let (p, vp_size) = { let sel = self.tabs[i].scene.selection.borrow(); let p = match sel.last_move_pos { diff --git a/src/app/view.rs b/src/app/view.rs index a0b83877f..60a0a1d57 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -684,9 +684,9 @@ impl OpenCADStudio { } } - // Multi-functional grip popup (Phase 2). Floating column of - // menu items at the grip's screen position, with the selected - // item highlighted. Each item dispatches `GripMenuPick`. + // Multi-functional grip popup (Phase 2). One bordered container + // wraps a column of borderless item buttons so the popup reads + // as a single widget instead of stacked tiles. if let Some(popup) = self.grip_popup.as_ref() { if !tab.is_start { let anchor_x = (popup.anchor.x + 12.0).max(0.0); @@ -697,48 +697,61 @@ impl OpenCADStudio { let label = item.label; let btn = button(text(label).size(12).color(Color::WHITE)) .on_press(Message::GripMenuPick(idx)) - .padding([4, 10]) + .padding([3, 10]) + .width(Fill) .style(move |_: &Theme, status| iced::widget::button::Style { background: Some(Background::Color(match (is_sel, status) { (true, _) => Color { r: 0.20, g: 0.45, b: 0.95, - a: 0.95, - }, - (_, iced::widget::button::Status::Hovered) => Color { - r: 0.20, - g: 0.20, - b: 0.20, - a: 0.95, - }, - _ => Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 0.92, - }, - })), - border: Border { - color: Color { - r: 0.35, - g: 0.35, - b: 0.35, a: 1.0, }, - width: 1.0, + (_, iced::widget::button::Status::Hovered) => Color { + r: 0.22, + g: 0.22, + b: 0.22, + a: 1.0, + }, + _ => Color::TRANSPARENT, + })), + border: Border { + color: Color::TRANSPARENT, + width: 0.0, radius: 0.0.into(), }, text_color: Color::WHITE, ..Default::default() }); - col = col.push(iced::widget::opaque(btn)); + col = col.push(btn); } + let menu_panel = container(col) + .padding(2) + .width(iced::Length::Shrink) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color { + r: 0.10, + g: 0.10, + b: 0.10, + a: 0.95, + })), + border: Border { + color: Color { + r: 0.40, + g: 0.40, + b: 0.40, + a: 1.0, + }, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); let popup_layer = iced::widget::column![ Space::new().height(iced::Length::Fixed(anchor_y)), iced::widget::row![ Space::new().width(iced::Length::Fixed(anchor_x)), - col, + iced::widget::opaque(menu_panel), ], ] .width(Fill)