forked from 1b-agent/opencadstudio-upstream
feat(grip): keyboard nav + click-outside dismiss + single-widget popup
- Popup items now render inside one bordered container with borderless buttons so the menu reads as a single widget instead of stacked tiles. - Arrow Up / Down (and Tab) walk `grip_popup.selected` while the popup is open; Enter commits the highlighted item by re-emitting `GripMenuPick`; Escape dismisses without acting; the underlying handlers (`CommandHistoryPrev/Next`, `CommandFinalize`, `CommandEscape`, `DynTabNext`) keep their normal behaviour when no popup is open. - A press inside the viewport that reaches the mouse_area (i.e. not on a popup item) closes the popup and consumes the click — the user's next press then starts a normal selection / drag.
This commit is contained in:
1 parent
a4c65649e1
commit
391aa3a76f
2 files changed
+89
-27
No files matched your search
@@ -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 {
|
||||
|
||||
+40
-27
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user