git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit23183a2a776e74ff01a7536f6dc36eaf35f09d46
parent2ff8a4c098
authorLucas Galante <[email protected]>
date2026-07-16 12:48
feat: highlight the focused context's pane border; bind cycling via input.kdl

The focused_pane context (network / viewport / parameters / spreadsheet)
had no visual indicator since the per-pane menubars went hidden in the
floating layout. Draw a highlight border around the focused context's
pane on top of the frame's vertices (circle border on the circular
network pane, respecting network_opacity).

Context cycling moves off the hardcoded Ctrl+Tab check onto the
ShortcutManager table as next_context (Ctrl+Tab) / previous_context
(Ctrl+Shift+Tab), rebindable through input.kdl's cce-designer domain.
The chords still dispatch ahead of the widget key paths so they work
from any pane, and execute_action handles the actions so menus/HTTP
can drive them too. Reverse cycling needs cce-ui's ISO_Left_Tab fix
to actually receive Shift+Tab.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/app.rs      | 29 +++++++++++++-------
 src/main.rs     |  9 +++++++
 src/render.rs   | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/shortcut.rs |  2 ++
 4 files changed, 112 insertions(+), 11 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 0031303..1e37914 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2582,6 +2582,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             register("toggle_spreadsheet", "`", Action::ToggleSpreadsheet);
             register("toggle_circular_pane", "Ctrl+d", Action::ToggleCircularPane);
             register("save_document", "Ctrl+s", Action::Save);
+            register("next_context", "Ctrl+Tab", Action::NextContext);
+            register("previous_context", "Ctrl+Shift+Tab", Action::PrevContext);
         }
 
         let mut state = Self {
@@ -3537,6 +3539,17 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.save_file_chooser();
                 }
             }
+            Action::NextContext | Action::PrevContext => {
+                self.focused_pane = get_next_visible_pane(
+                    self.focused_pane,
+                    self.show_network,
+                    self.show_viewport,
+                    self.show_parameters,
+                    self.show_spreadsheet,
+                    action == Action::PrevContext,
+                );
+                self.sync_pane_focus();
+            }
         }
         if settings_changed {
             self.save_settings();
@@ -4482,17 +4495,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.space_pressed = event.state == ElementState::Pressed;
                 }
 
+                // Context switching dispatches ahead of the widget key paths
+                // (param pane, node palette) so the chord works from any pane.
                 if event.state == ElementState::Pressed {
-                    if event.logical_key == Key::Named(NamedKey::Tab) && self.modifiers.control_key() && !self.modifiers.alt_key() && !self.modifiers.super_key() {
-                        self.focused_pane = get_next_visible_pane(
-                            self.focused_pane,
-                            self.show_network,
-                            self.show_viewport,
-                            self.show_parameters,
-                            self.show_spreadsheet,
-                            self.modifiers.shift_key(),
-                        );
-                        self.sync_pane_focus();
+                    if let Some(action @ (Action::NextContext | Action::PrevContext)) =
+                        self.shortcut_manager.match_action(&self.modifiers, &event.logical_key)
+                    {
+                        self.execute_action(action);
                         return true;
                     }
                 }
diff --git a/src/main.rs b/src/main.rs
index e01d2e8..06fa2ad 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -730,6 +730,15 @@ mod tests {
         let mods_none = ModifiersState::default();
         let key_tick = Key::Character("`".to_string());
         assert_eq!(mgr.match_action(&mods_none, &key_tick), Some(Action::ToggleSpreadsheet));
+
+        // Context cycling chords: exact modifier match separates next from previous
+        mgr.register("Ctrl+Tab", Action::NextContext).unwrap();
+        mgr.register("Ctrl+Shift+Tab", Action::PrevContext).unwrap();
+        let key_tab = Key::Named(NamedKey::Tab);
+        let mods_ctrl_shift = ModifiersState { ctrl: true, alt: false, shift: true, logo: false };
+        assert_eq!(mgr.match_action(&mods_ctrl, &key_tab), Some(Action::NextContext));
+        assert_eq!(mgr.match_action(&mods_ctrl_shift, &key_tab), Some(Action::PrevContext));
+        assert_eq!(mgr.match_action(&mods_none, &key_tab), None);
     }
 }
 
diff --git a/src/render.rs b/src/render.rs
index 073fce9..7a65405 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -8,7 +8,7 @@ use crate::app::{
     make_text_buffer, make_text_buffer_with_font,
     CONTENT_IDX, VIEWPORT_IDX, PARAM_IDX, PARAM_PLATE_IDX,
     BREADCRUMB_IDX, HEADER_IDX, RIGHT_MENUBAR_IDX,
-    SPREADSHEET_MENUBAR_IDX,
+    SPREADSHEET_MENUBAR_IDX, SPREADSHEET_IDX,
     MENUBAR_H,
     LEFT_MENUBAR_IDX, PARAM_MENUBAR_IDX, NETWORK_PANEL_IDX,
     push_circle_vertices, push_circle_border_vertices,
@@ -106,6 +106,87 @@ impl State {
                 );
             }
         }
+
+        self.push_context_border(verts, sw, sh, clip_circle_val);
+    }
+
+    /// Highlight border around the focused context's pane. The per-pane
+    /// menubars are hidden in the floating layout, so this border is the
+    /// only visual indicator of `focused_pane`.
+    fn push_context_border(
+        &self,
+        verts: &mut Vec<Vertex>,
+        sw: f32,
+        sh: f32,
+        clip_circle_val: [f32; 3],
+    ) {
+        if self.is_detached_network {
+            return;
+        }
+
+        let thickness = 2.0;
+        let mut color = colors::highlight_primary_color();
+        color[3] = 0.9;
+
+        let (x, y, w, h) = match self.focused_pane {
+            LEFT_MENUBAR_IDX => {
+                if !self.show_network || self.detached_circular_network {
+                    return;
+                }
+                if self.circular_network_pane {
+                    color[3] *= self.network_opacity;
+                    push_circle_border_vertices(
+                        self.circular_network_layout.x,
+                        self.circular_network_layout.y,
+                        self.circular_network_layout.r,
+                        3.0,
+                        sw,
+                        sh,
+                        color,
+                        64,
+                        clip_circle_val,
+                        verts,
+                    );
+                    return;
+                }
+                color[3] *= self.network_opacity;
+                self.positions[NETWORK_PANEL_IDX]
+            }
+            RIGHT_MENUBAR_IDX => {
+                if !self.show_viewport {
+                    return;
+                }
+                self.positions[VIEWPORT_IDX]
+            }
+            PARAM_MENUBAR_IDX => {
+                if !self.show_parameters {
+                    return;
+                }
+                self.positions[PARAM_IDX]
+            }
+            SPREADSHEET_MENUBAR_IDX => {
+                if !self.show_spreadsheet {
+                    return;
+                }
+                self.positions[SPREADSHEET_IDX]
+            }
+            _ => return,
+        };
+
+        if w <= 0.0 || h <= 0.0 {
+            return;
+        }
+        let r = cce_ui::layout::plate_corner_radius();
+        let radii = cce_ui::widget::CornerRadii::new(r, r, r, r);
+        push_plate_solid_border_vertices(
+            x, y, w, h,
+            radii,
+            thickness,
+            sw, sh,
+            color,
+            [0.0, 0.0, 0.0],
+            verts,
+        );
     }
 
     pub(crate) fn draw_widget_recursive(
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 92970be..4262855 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -13,6 +13,8 @@ pub enum Action {
     ToggleCircularPane,
     DetachCircularWindow,
     Save,
+    NextContext,
+    PrevContext,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]