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()
|
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 => {
|
Message::DynTabNext => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let n = self.tabs[i].dyn_fields.len();
|
let n = self.tabs[i].dyn_fields.len();
|
||||||
@@ -954,6 +963,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandHistoryPrev => {
|
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
|
// While autocomplete is showing suggestions, ↑ walks up
|
||||||
// that list. Otherwise it falls back to recall history.
|
// that list. Otherwise it falls back to recall history.
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
@@ -967,6 +987,12 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandHistoryNext => {
|
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;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].active_cmd.is_none()
|
if self.tabs[i].active_cmd.is_none()
|
||||||
&& self.command_line.autocomplete_next()
|
&& self.command_line.autocomplete_next()
|
||||||
@@ -1142,6 +1168,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandFinalize => {
|
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
|
// A typed dynamic-input value commits as a point pick
|
||||||
// before the plain-Enter (on_enter) path runs.
|
// before the plain-Enter (on_enter) path runs.
|
||||||
if let Some(task) = self.try_dyn_commit() {
|
if let Some(task) = self.try_dyn_commit() {
|
||||||
@@ -1162,6 +1197,12 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandEscape => {
|
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.
|
// Cancel layout rename / context menus first, then fall through.
|
||||||
let i_e = self.active_tab;
|
let i_e = self.active_tab;
|
||||||
{
|
{
|
||||||
@@ -2166,6 +2207,14 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
Message::ViewportLeftPress => {
|
Message::ViewportLeftPress => {
|
||||||
let i = self.active_tab;
|
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 (p, vp_size) = {
|
||||||
let sel = self.tabs[i].scene.selection.borrow();
|
let sel = self.tabs[i].scene.selection.borrow();
|
||||||
let p = match sel.last_move_pos {
|
let p = match sel.last_move_pos {
|
||||||
|
|||||||
+40
-27
@@ -684,9 +684,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-functional grip popup (Phase 2). Floating column of
|
// Multi-functional grip popup (Phase 2). One bordered container
|
||||||
// menu items at the grip's screen position, with the selected
|
// wraps a column of borderless item buttons so the popup reads
|
||||||
// item highlighted. Each item dispatches `GripMenuPick`.
|
// as a single widget instead of stacked tiles.
|
||||||
if let Some(popup) = self.grip_popup.as_ref() {
|
if let Some(popup) = self.grip_popup.as_ref() {
|
||||||
if !tab.is_start {
|
if !tab.is_start {
|
||||||
let anchor_x = (popup.anchor.x + 12.0).max(0.0);
|
let anchor_x = (popup.anchor.x + 12.0).max(0.0);
|
||||||
@@ -697,48 +697,61 @@ impl OpenCADStudio {
|
|||||||
let label = item.label;
|
let label = item.label;
|
||||||
let btn = button(text(label).size(12).color(Color::WHITE))
|
let btn = button(text(label).size(12).color(Color::WHITE))
|
||||||
.on_press(Message::GripMenuPick(idx))
|
.on_press(Message::GripMenuPick(idx))
|
||||||
.padding([4, 10])
|
.padding([3, 10])
|
||||||
|
.width(Fill)
|
||||||
.style(move |_: &Theme, status| iced::widget::button::Style {
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
||||||
background: Some(Background::Color(match (is_sel, status) {
|
background: Some(Background::Color(match (is_sel, status) {
|
||||||
(true, _) => Color {
|
(true, _) => Color {
|
||||||
r: 0.20,
|
r: 0.20,
|
||||||
g: 0.45,
|
g: 0.45,
|
||||||
b: 0.95,
|
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,
|
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(),
|
radius: 0.0.into(),
|
||||||
},
|
},
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
..Default::default()
|
..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![
|
let popup_layer = iced::widget::column![
|
||||||
Space::new().height(iced::Length::Fixed(anchor_y)),
|
Space::new().height(iced::Length::Fixed(anchor_y)),
|
||||||
iced::widget::row![
|
iced::widget::row![
|
||||||
Space::new().width(iced::Length::Fixed(anchor_x)),
|
Space::new().width(iced::Length::Fixed(anchor_x)),
|
||||||
col,
|
iced::widget::opaque(menu_panel),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
|
|||||||
Reference in new issue
Block a user