node-based graph editor
git clone https://git.lucas.co/cce-graph.git
fix: the open menu dropdowns take the keyboard
Escape closes an open File/Edit/View menu, arrows move the hover,
Enter selects. The widget has handled those keys itself since cce-ui's
routed events, but handle_key_input never forwarded a key to any
dropdown - mouse events were routed, keys were not (the same bug
cce-files had). The selection-to-AppMessage drains are extracted from
the mouse path into shared helpers so a selection means the same thing
however it was made, keeping the sentinel selected=999 protocol.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/main.rs | 125 ++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 87 insertions(+), 38 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index e6b0576..34d327b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -429,6 +429,63 @@ fn matches_keybind(event: &KeyEvent, keybind: &str) -> bool {
}
impl GraphApp {
+ /// Drain a consumed file-menu interaction into its message — shared by the
+ /// mouse path and the key path so a selection means the same thing however
+ /// it was made. Keeps the sentinel `selected = 999` protocol.
+ fn drain_file_menu(&mut self) -> Option<AppMessage> {
+ let mut msg = None;
+ if self.dropdown_file.take_change() {
+ let selected_idx = self.dropdown_file.selected;
+ if selected_idx < self.dropdown_file.options.len() {
+ let option_text = &self.dropdown_file.options[selected_idx];
+ match option_text.as_str() {
+ "New" => msg = Some(AppMessage::New),
+ "Open" | "Open..." => msg = Some(AppMessage::Open),
+ "Save" => msg = Some(AppMessage::Save),
+ "Save As" => msg = Some(AppMessage::SaveAs),
+ "Exit" => msg = Some(AppMessage::Exit),
+ "-" => {}
+ _ => {
+ let path = std::path::PathBuf::from(option_text);
+ msg = Some(AppMessage::OpenRecent(path));
+ }
+ }
+ }
+ self.dropdown_file.selected = 999;
+ }
+ msg
+ }
+
+ fn drain_edit_menu(&mut self) -> Option<AppMessage> {
+ let mut msg = None;
+ if self.dropdown_edit.take_change() {
+ match self.dropdown_edit.selected {
+ 0 => msg = Some(AppMessage::AddNode),
+ 1 => msg = Some(AppMessage::AddImage),
+ _ => {}
+ }
+ self.dropdown_edit.selected = 999;
+ }
+ msg
+ }
+
+ fn drain_view_menu(&mut self) -> Option<AppMessage> {
+ let mut msg = None;
+ if self.dropdown_view.take_change() {
+ match self.dropdown_view.selected {
+ 0 => msg = Some(AppMessage::ToggleGrid),
+ 1 => msg = Some(AppMessage::ToggleUniformBackground),
+ 2 => msg = Some(AppMessage::SetOpacity95),
+ 3 => msg = Some(AppMessage::SetOpacity75),
+ 4 => msg = Some(AppMessage::SetOpacity50),
+ 5 => msg = Some(AppMessage::ToggleControlPanel),
+ _ => {}
+ }
+ self.dropdown_view.selected = 999;
+ }
+ msg
+ }
+
fn delete_selected_node(&mut self) {
if let Some(idx) = self.graph.selected_node() {
let mut nodes = self.graph.get_nodes();
@@ -1478,54 +1535,22 @@ impl Application for GraphApp {
let dd_file = self.dropdown_file.id();
if self.ui_context.propagate_event(&ev, dd_file) {
changed = true;
- if self.dropdown_file.take_change() {
- let selected_idx = self.dropdown_file.selected;
- if selected_idx < self.dropdown_file.options.len() {
- let option_text = &self.dropdown_file.options[selected_idx];
- match option_text.as_str() {
- "New" => msg_out = Some(AppMessage::New),
- "Open" | "Open..." => msg_out = Some(AppMessage::Open),
- "Save" => msg_out = Some(AppMessage::Save),
- "Save As" => msg_out = Some(AppMessage::SaveAs),
- "Exit" => msg_out = Some(AppMessage::Exit),
- "-" => {}
- _ => {
- let path = std::path::PathBuf::from(option_text);
- msg_out = Some(AppMessage::OpenRecent(path));
- }
- }
- }
- self.dropdown_file.selected = 999;
+ if let Some(m) = self.drain_file_menu() {
+ msg_out = Some(m);
}
}
let dd_edit = self.dropdown_edit.id();
if self.ui_context.propagate_event(&ev, dd_edit) {
changed = true;
- if self.dropdown_edit.take_change() {
- let idx = self.dropdown_edit.selected;
- match idx {
- 0 => msg_out = Some(AppMessage::AddNode),
- 1 => msg_out = Some(AppMessage::AddImage),
- _ => {}
- }
- self.dropdown_edit.selected = 999;
+ if let Some(m) = self.drain_edit_menu() {
+ msg_out = Some(m);
}
}
let dd_view = self.dropdown_view.id();
if self.ui_context.propagate_event(&ev, dd_view) {
changed = true;
- if self.dropdown_view.take_change() {
- let idx = self.dropdown_view.selected;
- match idx {
- 0 => msg_out = Some(AppMessage::ToggleGrid),
- 1 => msg_out = Some(AppMessage::ToggleUniformBackground),
- 2 => msg_out = Some(AppMessage::SetOpacity95),
- 3 => msg_out = Some(AppMessage::SetOpacity75),
- 4 => msg_out = Some(AppMessage::SetOpacity50),
- 5 => msg_out = Some(AppMessage::ToggleControlPanel),
- _ => {}
- }
- self.dropdown_view.selected = 999;
+ if let Some(m) = self.drain_view_menu() {
+ msg_out = Some(m);
}
}
@@ -1638,6 +1663,30 @@ impl Application for GraphApp {
return None;
}
+ // An open menu dropdown takes the keyboard — Escape closes it, arrows
+ // move the hover, Enter selects — routed to the widget exactly like
+ // its mouse events above, drained through the same helpers. The
+ // widget has handled these keys itself since cce-ui's routed events;
+ // this app just never forwarded a key to it (the cce-files bug).
+ if self.dropdown_file.open || self.dropdown_edit.open || self.dropdown_view.open {
+ let kev = Event::KeyInput(event.clone());
+ let root = if self.dropdown_file.open {
+ self.dropdown_file.id()
+ } else if self.dropdown_edit.open {
+ self.dropdown_edit.id()
+ } else {
+ self.dropdown_view.id()
+ };
+ if self.ui_context.propagate_event(&kev, root) {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ return self
+ .drain_file_menu()
+ .or_else(|| self.drain_edit_menu())
+ .or_else(|| self.drain_view_menu());
+ }
+ }
+
if event.state == ElementState::Pressed {
// input.kdl `cce-graph.delete_node`, falling back to the legacy
// config.kdl graph `delete` prop.