git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit36fff316e6ca744bee12d7e0490e8137c34c21dd
parent33cbad6f3c
authorLucas Galante <[email protected]>
date2026-06-19 03:34
Integrate editable KeybindsControl widget on input page

 src/main.rs        | 31 ++++++++++++++++++++++++++++++-
 src/pages/input.rs | 39 ++++++++++++++++++---------------------
 2 files changed, 48 insertions(+), 22 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 2a5d5a2..45a4c21 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -823,6 +823,7 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
         self.app.input.cursor_size_spinbox.clear_children(&mut self.ui_context); self.app.input.cursor_size_spinbox.set_parent(None, &mut self.ui_context);
         self.app.input.zoom_in_box.clear_children(&mut self.ui_context); self.app.input.zoom_in_box.set_parent(None, &mut self.ui_context);
         self.app.input.zoom_out_box.clear_children(&mut self.ui_context); self.app.input.zoom_out_box.set_parent(None, &mut self.ui_context);
+        self.app.input.keybinds_control.clear_children(&mut self.ui_context); self.app.input.keybinds_control.set_parent(None, &mut self.ui_context);
 
         for sb in &mut self.app.audio.sink_spinboxes {
             sb.clear_children(&mut self.ui_context);
@@ -1099,13 +1100,14 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
 
 
             Page::Input => {
-                self.page_sec_containers.resize_with(6, cce_ui::widget::Container::new);
+                self.page_sec_containers.resize_with(7, cce_ui::widget::Container::new);
                 link_parent_child(page_root, &mut self.page_sec_containers[0], &mut self.ui_context);
                 link_parent_child(page_root, &mut self.page_sec_containers[1], &mut self.ui_context);
                 link_parent_child(page_root, &mut self.page_sec_containers[2], &mut self.ui_context);
                 link_parent_child(page_root, &mut self.page_sec_containers[3], &mut self.ui_context);
                 link_parent_child(page_root, &mut self.page_sec_containers[4], &mut self.ui_context);
                 link_parent_child(page_root, &mut self.page_sec_containers[5], &mut self.ui_context);
+                link_parent_child(page_root, &mut self.page_sec_containers[6], &mut self.ui_context);
                 
                 link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.dwtp_toggle, &mut self.ui_context);
                 link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.trackpoint_accel_speed_spinbox, &mut self.ui_context);
@@ -1129,6 +1131,7 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
 
                 link_parent_child(&mut self.page_sec_containers[5], &mut self.app.input.zoom_in_box, &mut self.ui_context);
                 link_parent_child(&mut self.page_sec_containers[5], &mut self.app.input.zoom_out_box, &mut self.ui_context);
+                link_parent_child(&mut self.page_sec_containers[6], &mut self.app.input.keybinds_control, &mut self.ui_context);
             }
             Page::Audio => {
                 self.page_sec_containers.resize_with(2, cce_ui::widget::Container::new);
@@ -2118,6 +2121,9 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
             if self.app.input.zoom_out_box.cursor_moved(lx, ly, &mut self.ui_context) {
                 changed = true;
             }
+            if self.app.input.keybinds_control.cursor_moved(lx, ly, &mut self.ui_context) {
+                changed = true;
+            }
         }
         if self.app.current_page == Page::Audio {
             if let Some(idx) = self.audio_sink_dragging {
@@ -2571,6 +2577,7 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
                     if self.app.input.trackpoint_accel_profile_menu.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
                     if self.app.input.zoom_in_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
                     if self.app.input.zoom_out_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+                    if self.app.input.keybinds_control.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
                 }
 
                 Page::Audio => {
@@ -3121,6 +3128,10 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
             if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
                 actions.push(AppAction::Input(pages::input::InputMessage::ApplyCursorSize));
             }
+            let kc = &mut self.app.input.keybinds_control;
+            if state == cce_ui::widget::ElementState::Pressed && !kc.hit_test(lx, ly, &self.ui_context) {
+                kc.unfocus();
+            }
         }
         if state == cce_ui::widget::ElementState::Pressed && self.app.current_page == Page::Services {
             let sb = &mut self.app.services.notifications_duration_spinbox;
@@ -3201,6 +3212,14 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
             if state == cce_ui::widget::ElementState::Pressed && tb.take_change() {
                 actions.push(AppAction::Input(pages::input::InputMessage::ApplyZoomOut));
             }
+            let kc = &mut self.app.input.keybinds_control;
+            if kc.mouse_input(button, state, lx, ly, &mut self.ui_context) {
+                self.needs_rebuild = true;
+            }
+            if kc.take_change() {
+                actions.push(AppAction::Input(pages::input::InputMessage::ReloadKeybinds));
+                self.needs_rebuild = true;
+            }
         }
         if self.app.current_page == Page::Services {
             let toggle = &mut self.app.services.notifications_enable_toggle;
@@ -4004,6 +4023,9 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
                 if self.app.input.zoom_out_box.take_change() {
                     actions.push(AppAction::Input(pages::input::InputMessage::ApplyZoomOut));
                 }
+                if self.app.input.keybinds_control.take_change() {
+                    actions.push(AppAction::Input(pages::input::InputMessage::ReloadKeybinds));
+                }
             }
             Page::Services => {
                 if self.app.services.notifications_duration_spinbox.take_change() {
@@ -5114,6 +5136,13 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
                 self.needs_rebuild = true;
                 return true;
             }
+            if self.app.input.keybinds_control.keyboard_input(event, &mut self.ui_context) {
+                if self.app.input.keybinds_control.take_change() {
+                    self.handle_action(&AppAction::Input(pages::input::InputMessage::ReloadKeybinds));
+                }
+                self.needs_rebuild = true;
+                return true;
+            }
         }
         if self.app.current_page == Page::Accounts {
             let mut consumed = false;
diff --git a/src/pages/input.rs b/src/pages/input.rs
index 762ebc1..6994779 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -3,7 +3,7 @@ use std::io::Write;
 
 use crate::app::PageContent;
 use cce_ui::layout::{PageLayoutBuilder, LayoutStrategy};
-use cce_ui::widget::{Spinbox, Toggle, Trackpad, Dropdown, Finger, Element, TextBox};
+use cce_ui::widget::{Spinbox, Toggle, Trackpad, Dropdown, Finger, Element, TextBox, KeybindsControl};
 
 const CONFIG_PATH: &str = "/home/lsgalante/.config/cce/config.json";
 
@@ -74,6 +74,9 @@ pub struct InputState {
     // Graph settings
     pub zoom_in_box: TextBox,
     pub zoom_out_box: TextBox,
+
+    // Keyboard bindings widget
+    pub keybinds_control: KeybindsControl,
 }
 
 fn scan_cursor_themes() -> Vec<String> {
@@ -157,6 +160,9 @@ impl Default for InputState {
             // Graph defaults
             zoom_in_box: TextBox::new("=".to_string()).with_label("Zoom In"),
             zoom_out_box: TextBox::new("-".to_string()).with_label("Zoom Out"),
+
+            // Keyboard bindings widget
+            keybinds_control: KeybindsControl::new(),
         }
     }
 }
@@ -192,6 +198,7 @@ pub enum InputMessage {
 
     ApplyZoomIn,
     ApplyZoomOut,
+    ReloadKeybinds,
 }
 
 pub fn read_input_config() -> InputState {
@@ -274,6 +281,9 @@ pub fn read_input_config() -> InputState {
         // Graph settings
         zoom_in_box: TextBox::new(zoom_in).with_label("Zoom In"),
         zoom_out_box: TextBox::new(zoom_out).with_label("Zoom Out"),
+
+        // Keyboard bindings widget
+        keybinds_control: KeybindsControl::new(),
     }
 }
 
@@ -394,8 +404,6 @@ fn apply_repeat_config(rate: u16, delay: u16) {
     send_ipc_command(&format!("repeat delay {}", delay));
 }
 
-const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 
 pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy, ctx: &mut cce_ui::context::UiContext) -> PageContent {
     let mut final_pc = PageContent::new();
@@ -491,22 +499,8 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
 
     // ── Keyboard Bindings ──
     builder.add_section(&mut final_pc, "Keyboard Bindings", false, |sec| {
-        for kb in &state.keybinds {
-            let binding = if kb.mods.is_empty() {
-                kb.key.clone()
-            } else {
-                format!("{}+{}", kb.mods, kb.key)
-            };
-            let action_label = if kb.command.is_empty() {
-                kb.action.clone()
-            } else {
-                format!("{}: {}", kb.action, kb.command)
-            };
-            sec.text(&binding, 14.0, 0.0, 12.0, TEXT_FG);
-            let label_w = sec_w - 200.0;
-            sec.text(&action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
-            sec.spacing(18.0);
-        }
+        let height = state.keybinds_control.preferred_height().unwrap_or(200.0);
+        sec.widget_full(&mut state.keybinds_control, height, ctx);
     });
 
     final_pc
@@ -607,6 +601,9 @@ pub fn update(state: &mut InputState, msg: InputMessage) {
             let val = state.zoom_out_box.text.clone();
             write_config_value("zoom_out", &format!("\"{}\"", val));
         }
+        InputMessage::ReloadKeybinds => {
+            send_ipc_command("reload");
+        }
         InputMessage::Refreshed(new) => {
             let fingers = state.fingers.clone();
             *state = new;
@@ -644,11 +641,11 @@ mod tests {
 
     #[test]
     fn test_parse_scrolling_params() {
-        let content = "[input]\nnatural_scroll = true\n[inertial]\nscroll_speed = 2.5\n";
+        let content = r#"{"input": {"natural_scroll": true}, "inertial": {"scroll_speed": 2.5}}"#;
         assert_eq!(parse_bool_from_default(content, "natural_scroll", false), true);
         assert_eq!(parse_f32_key(content, "scroll_speed", 1.0), 2.5);
 
-        let empty_content = "";
+        let empty_content = "{}";
         assert_eq!(parse_bool_from_default(empty_content, "natural_scroll", false), false);
         assert_eq!(parse_f32_key(empty_content, "scroll_speed", 1.0), 1.0);
     }