git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit25bcf61734e79d20fd7430290919f212dd3bcb11
parentca2a4fa11d
authorLucas Galante <[email protected]>
date2026-06-19 03:34
Implement editable KeybindsControl widget

 src/backend/window_runner.rs         |  20 ++
 src/layout.rs                        |  65 ++++++-
 src/main.rs                          |  49 +++++
 src/widget/input/keybinds_control.rs | 364 +++++++++++++++++++++++++++++++++++
 src/widget/input/mod.rs              |   2 +
 src/widget/mod.rs                    |   3 +-
 6 files changed, 500 insertions(+), 3 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b4e229d..bba42c8 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -621,6 +621,26 @@ fn get_child_widget_for_quad<'a>(
             }
         }
     }
+    if let Some(kc) = w.as_any().downcast_ref::<crate::widget::input::KeybindsControl>() {
+        let (bx, by, bw, bh) = kc.add_button.rect();
+        if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+            return &kc.add_button;
+        }
+        for row in &kc.rows {
+            let (kx, ky, kw, kh) = row.key_input.rect();
+            if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+                return &row.key_input;
+            }
+            let (cx, cy, cw, ch) = row.cmd_input.rect();
+            if qx >= cx - 0.1 && qx + qw <= cx + cw + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + ch + 0.1 {
+                return &row.cmd_input;
+            }
+            let (rx, ry, rw, rh) = row.remove_button.rect();
+            if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+                return &row.remove_button;
+            }
+        }
+    }
     w
 }
 
diff --git a/src/layout.rs b/src/layout.rs
index 4879ce1..6b97d93 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1668,6 +1668,56 @@ fn get_multicontrol_sub_widget_info(
     None
 }
 
+fn get_keybinds_control_sub_widget_info(
+    kc: &crate::widget::input::KeybindsControl,
+    qx: f32, qy: f32, qw: f32, qh: f32,
+) -> Option<((bool, bool, bool, bool), f32, (f32, f32, f32, f32), f32)> {
+    // Check add_button
+    let (bx, by, bw, bh) = kc.add_button.rect();
+    if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+        return Some((
+            kc.add_button.rounded_corners(),
+            kc.add_button.corner_radius(),
+            (bx, by, bw, bh),
+            crate::widget::label_offset(&kc.add_button),
+        ));
+    }
+    // Check rows
+    for row in &kc.rows {
+        // key_input
+        let (kx, ky, kw, kh) = row.key_input.rect();
+        if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+            return Some((
+                row.key_input.rounded_corners(),
+                row.key_input.corner_radius(),
+                (kx, ky, kw, kh),
+                crate::widget::label_offset(&row.key_input),
+            ));
+        }
+        // cmd_input
+        let (cx, cy, cw, ch) = row.cmd_input.rect();
+        if qx >= cx - 0.1 && qx + qw <= cx + cw + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + ch + 0.1 {
+            return Some((
+                row.cmd_input.rounded_corners(),
+                row.cmd_input.corner_radius(),
+                (cx, cy, cw, ch),
+                crate::widget::label_offset(&row.cmd_input),
+            ));
+        }
+        // remove_button
+        let (rx, ry, rw, rh) = row.remove_button.rect();
+        if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+            return Some((
+                row.remove_button.rounded_corners(),
+                row.remove_button.corner_radius(),
+                (rx, ry, rw, rh),
+                crate::widget::label_offset(&row.remove_button),
+            ));
+        }
+    }
+    None
+}
+
 pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
     w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
     let corners = w.rounded_corners();
@@ -1680,7 +1730,7 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
     let top_room = crate::widget::label_offset(w);
     wy += top_room;
     whh -= top_room;
-
+ 
     for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
         let mut corners = corners;
         let mut r = r;
@@ -1688,7 +1738,7 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
         let mut wy = wy;
         let mut www = www;
         let mut whh = whh;
-
+ 
         if let Some(mc) = w.as_any().downcast_ref::<crate::widget::input::MultiControl>() {
             if let Some((sub_corners, sub_radius, (sub_x, sub_y, sub_w, sub_h), sub_label_offset)) = get_multicontrol_sub_widget_info(mc, qx, qy, qw, qh) {
                 corners = sub_corners;
@@ -1700,6 +1750,17 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
             }
         }
 
+        if let Some(kc) = w.as_any().downcast_ref::<crate::widget::input::KeybindsControl>() {
+            if let Some((sub_corners, sub_radius, (sub_x, sub_y, sub_w, sub_h), sub_label_offset)) = get_keybinds_control_sub_widget_info(kc, qx, qy, qw, qh) {
+                corners = sub_corners;
+                r = sub_radius;
+                wx = sub_x;
+                wy = sub_y + sub_label_offset;
+                www = sub_w;
+                whh = sub_h - sub_label_offset;
+            }
+        }
+
         if r <= 0.1 || corners == (false, false, false, false) {
             pc.rect_with_radius_corners(qc, qx, qy, qw, qh, 0.0, (false, false, false, false));
             continue;
diff --git a/src/main.rs b/src/main.rs
index 6ff2210..609ff38 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -257,6 +257,55 @@ fn extra_quad_vertices(
         }
     }
 
+    if let Some(kc) = w.as_any().downcast_ref::<cce_ui::widget::input::KeybindsControl>() {
+        // check add_button
+        let (bx, by, b_w, b_h) = kc.add_button.rect();
+        if qx >= bx - 0.1 && qx + qw <= bx + b_w + 0.1 && qy >= by - 0.1 && qy + qh <= by + b_h + 0.1 {
+            corners = kc.add_button.rounded_corners();
+            r = kc.add_button.corner_radius();
+            wx = bx;
+            wy = by;
+            ww = b_w;
+            wh = b_h;
+            top_room = cce_ui::widget::label_offset(&kc.add_button);
+        }
+        for row in &kc.rows {
+            // key_input
+            let (kx, ky, kw, kh) = row.key_input.rect();
+            if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+                corners = row.key_input.rounded_corners();
+                r = row.key_input.corner_radius();
+                wx = kx;
+                wy = ky;
+                ww = kw;
+                wh = kh;
+                top_room = cce_ui::widget::label_offset(&row.key_input);
+            }
+            // cmd_input
+            let (cx, cy, cw, ch) = row.cmd_input.rect();
+            if qx >= cx - 0.1 && qx + qw <= cx + cw + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + ch + 0.1 {
+                corners = row.cmd_input.rounded_corners();
+                r = row.cmd_input.corner_radius();
+                wx = cx;
+                wy = cy;
+                ww = cw;
+                wh = ch;
+                top_room = cce_ui::widget::label_offset(&row.cmd_input);
+            }
+            // remove_button
+            let (rx, ry, rw, rh) = row.remove_button.rect();
+            if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+                corners = row.remove_button.rounded_corners();
+                r = row.remove_button.corner_radius();
+                wx = rx;
+                wy = ry;
+                ww = rw;
+                wh = rh;
+                top_room = cce_ui::widget::label_offset(&row.remove_button);
+            }
+        }
+    }
+
     if corners == (false, false, false, false) {
         return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
     }
diff --git a/src/widget/input/keybinds_control.rs b/src/widget/input/keybinds_control.rs
new file mode 100644
index 0000000..d353cfc
--- /dev/null
+++ b/src/widget/input/keybinds_control.rs
@@ -0,0 +1,364 @@
+use crate::colors;
+use crate::widget::*;
+use crate::widget::focus;
+use crate::widget::input::text_box::TextBox;
+use crate::widget::input::button::Button;
+use crate::widget::TextLabel;
+
+#[derive(Clone, Debug)]
+pub struct KeybindRow {
+    pub key_input: TextBox,
+    pub cmd_input: TextBox,
+    pub remove_button: Button,
+}
+
+impl KeybindRow {
+    pub fn new(binding: String, command: String) -> Self {
+        let key_input = TextBox::new(binding);
+        let cmd_input = TextBox::new(command);
+        let remove_button = Button::new(0.0, 0.0, 0.0, 0.0).with_label("Remove");
+        Self {
+            key_input,
+            cmd_input,
+            remove_button,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct KeybindsControl {
+    pub base: Widget,
+    pub rows: Vec<KeybindRow>,
+    pub add_button: Button,
+    pub just_changed: bool,
+}
+
+impl KeybindsControl {
+    pub fn new() -> Self {
+        let mut kc = Self {
+            base: Widget::new(),
+            rows: Vec::new(),
+            add_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Add Keybind +"),
+            just_changed: false,
+        };
+        kc.load_from_config();
+        kc
+    }
+
+    pub fn load_from_config(&mut self) {
+        let content = std::fs::read_to_string("/home/lsgalante/.config/cce/config.json").unwrap_or_default();
+        let val: serde_json::Value = serde_json::from_str(&content).unwrap_or_default();
+        let mut loaded = Vec::new();
+        if let Some(arr) = val.get("keybind").and_then(|k| k.as_array()) {
+            for v in arr {
+                let mods = v.get("mods").and_then(|m| m.as_str()).unwrap_or("").to_string();
+                let key = v.get("key").and_then(|k| k.as_str()).unwrap_or("").to_string();
+                let action = v.get("action").and_then(|a| a.as_str()).unwrap_or("").to_string();
+                let command = v.get("command").and_then(|c| c.as_str()).unwrap_or("").to_string();
+                
+                let binding = if mods.is_empty() {
+                    key
+                } else {
+                    format!("{}+{}", mods, key)
+                };
+                let cmd_val = if action == "spawn" {
+                    command
+                } else {
+                    action
+                };
+                loaded.push((binding, cmd_val));
+            }
+        }
+        self.rows = loaded.into_iter().map(|(binding, cmd_val)| {
+            KeybindRow::new(binding, cmd_val)
+        }).collect();
+    }
+
+    pub fn save_to_config(&self) {
+        let path = "/home/lsgalante/.config/cce/config.json";
+        let content = std::fs::read_to_string(path).unwrap_or_default();
+        let mut val: serde_json::Value = serde_json::from_str(&content).unwrap_or_default();
+        
+        let mut keybinds = Vec::new();
+        for row in &self.rows {
+            let binding = row.key_input.get_value_string().unwrap_or_default();
+            let cmd_val = row.cmd_input.get_value_string().unwrap_or_default();
+            
+            let (mods, key) = if let Some(last_plus) = binding.rfind('+') {
+                (binding[..last_plus].to_string(), binding[last_plus + 1..].to_string())
+            } else {
+                ("".to_string(), binding)
+            };
+
+            let built_in_actions = [
+                "expose", "close", "focus-next", "focus-prev", "mode-next",
+                "mode-next-shared", "fullscreen", "exit", "reload",
+                "view-1", "view-2", "view-3", "view-4",
+                "set-tag-1", "set-tag-2", "set-tag-3", "set-tag-4"
+            ];
+            
+            let mut obj = serde_json::Map::new();
+            obj.insert("mods".to_string(), serde_json::Value::String(mods));
+            obj.insert("key".to_string(), serde_json::Value::String(key));
+            
+            if built_in_actions.contains(&cmd_val.as_str()) {
+                obj.insert("action".to_string(), serde_json::Value::String(cmd_val));
+            } else {
+                obj.insert("action".to_string(), serde_json::Value::String("spawn".to_string()));
+                obj.insert("command".to_string(), serde_json::Value::String(cmd_val));
+            }
+            
+            keybinds.push(serde_json::Value::Object(obj));
+        }
+        
+        if let Some(obj) = val.as_object_mut() {
+            obj.insert("keybind".to_string(), serde_json::Value::Array(keybinds));
+        }
+        
+        if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
+            let _ = std::fs::write(path, updated_str);
+        }
+    }
+}
+
+fn link_child(parent_ptr: *mut (dyn Element + 'static), parent_id: WidgetId, child: &mut dyn Element, ctx: &mut UiContext) {
+    let c_ptr = child.as_ptr();
+    if let Some(c_base) = child.base() {
+        let c_id = c_base.id();
+        ctx.register_widget(parent_id, parent_ptr);
+        ctx.register_widget(c_id, c_ptr);
+        ctx.layout_tree.parents.insert(c_id, parent_id);
+        let children = ctx.layout_tree.children.entry(parent_id).or_default();
+        if !children.contains(&c_id) {
+            children.push(c_id);
+        }
+    }
+    child.set_parent(Some(parent_ptr), ctx);
+}
+
+impl Element for KeybindsControl {
+    crate::impl_widget_base!(KeybindsControl);
+
+    fn preferred_height(&self) -> Option<f32> {
+        let pad_y = 8.0;
+        let gap_between_rows = 8.0;
+        let row_h = crate::layout::textbox_height();
+        let add_btn_h = 30.0;
+
+        let mut total_h = pad_y * 2.0;
+        if !self.rows.is_empty() {
+            total_h += self.rows.len() as f32 * row_h + (self.rows.len() - 1) as f32 * gap_between_rows;
+            total_h += gap_between_rows;
+        }
+        total_h += add_btn_h;
+        Some(total_h)
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
+        let size = self.measure(constraints, ctx);
+        self.set_rect(origin.x, origin.y, size.width, size.height);
+
+        let pad_x = 8.0;
+        let pad_y = 8.0;
+        let gap_between_rows = 8.0;
+
+        let row_h = crate::layout::textbox_height();
+
+        let usable_w = (size.width - 2.0 * pad_x).max(1.0);
+        let gap_x = 6.0;
+        let row_usable_w = usable_w - 2.0 * gap_x;
+
+        let key_w = row_usable_w * 0.36;
+        let cmd_w = row_usable_w * 0.49;
+        let remove_w = row_usable_w * 0.15;
+
+        let mut curr_y = origin.y + pad_y;
+        let self_ptr = self.as_ptr();
+        let self_id = self.base.id();
+
+        for row in &mut self.rows {
+            let key_x = origin.x + pad_x;
+            row.key_input.layout(Point { x: key_x, y: curr_y }, LayoutConstraints::new(key_w, key_w, row_h, row_h), ctx);
+
+            let cmd_x = key_x + key_w + gap_x;
+            row.cmd_input.layout(Point { x: cmd_x, y: curr_y }, LayoutConstraints::new(cmd_w, cmd_w, row_h, row_h), ctx);
+
+            let remove_x = cmd_x + cmd_w + gap_x;
+            row.remove_button.layout(Point { x: remove_x, y: curr_y }, LayoutConstraints::new(remove_w, remove_w, row_h, row_h), ctx);
+
+            link_child(self_ptr, self_id, &mut row.key_input, ctx);
+            link_child(self_ptr, self_id, &mut row.cmd_input, ctx);
+            link_child(self_ptr, self_id, &mut row.remove_button, ctx);
+
+            curr_y += row_h + gap_between_rows;
+        }
+
+        let add_btn_w = 120.0f32.min(usable_w);
+        let add_btn_h = 30.0;
+        let add_x = origin.x + pad_x;
+        self.add_button.layout(Point { x: add_x, y: curr_y }, LayoutConstraints::new(add_btn_w, add_btn_w, add_btn_h, add_btn_h), ctx);
+        link_child(self_ptr, self_id, &mut self.add_button, ctx);
+    }
+
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = self.extra_quads();
+        if let Some(hq) = self.highlight_quad(ctx) {
+            if hq.4 != colors::HIGHLIGHT_SECONDARY {
+                quads.push(hq);
+            }
+        }
+
+        for row in &self.rows {
+            quads.extend(row.key_input.all_quads(ctx));
+            quads.extend(row.cmd_input.all_quads(ctx));
+            quads.extend(row.remove_button.all_quads(ctx));
+        }
+
+        quads.extend(self.add_button.all_quads(ctx));
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        Vec::new()
+    }
+
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        let mut labels = Vec::new();
+        for row in &self.rows {
+            labels.extend(row.key_input.text_labels_with_bounds(ctx));
+            labels.extend(row.cmd_input.text_labels_with_bounds(ctx));
+            labels.extend(row.remove_button.text_labels_with_bounds(ctx));
+        }
+        labels.extend(self.add_button.text_labels_with_bounds(ctx));
+        labels
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let mut labels = Vec::new();
+        let font = self.widget_font();
+        for l in self.text_labels() {
+            labels.push((l, font.clone(), None));
+        }
+
+        for row in &self.rows {
+            labels.extend(row.key_input.text_labels_with_font_and_bounds(ctx));
+            labels.extend(row.cmd_input.text_labels_with_font_and_bounds(ctx));
+            labels.extend(row.remove_button.text_labels_with_font_and_bounds(ctx));
+        }
+
+        labels.extend(self.add_button.text_labels_with_font_and_bounds(ctx));
+        labels
+    }
+
+    fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
+        let mut handled = false;
+
+        if self.add_button.handle_event(event, ctx) {
+            handled = true;
+        }
+
+        let mut row_to_remove = None;
+        for (i, row) in self.rows.iter_mut().enumerate() {
+            if row.key_input.handle_event(event, ctx) {
+                handled = true;
+            }
+            if row.cmd_input.handle_event(event, ctx) {
+                handled = true;
+            }
+            if row.remove_button.handle_event(event, ctx) {
+                handled = true;
+            }
+            if row.remove_button.take_click() {
+                row_to_remove = Some(i);
+                handled = true;
+            }
+        }
+
+        if let Some(idx) = row_to_remove {
+            self.rows.remove(idx);
+            self.save_to_config();
+            self.just_changed = true;
+            handled = true;
+        }
+
+        if self.add_button.take_click() {
+            let new_row = KeybindRow::new("".to_string(), "".to_string());
+            self.rows.push(new_row);
+            self.save_to_config();
+            self.just_changed = true;
+            handled = true;
+        }
+
+        let mut key_changed = false;
+        let mut cmd_changed = false;
+
+        for row in &mut self.rows {
+            if row.key_input.take_change() {
+                key_changed = true;
+            }
+            if row.cmd_input.take_change() {
+                cmd_changed = true;
+            }
+        }
+
+        if key_changed || cmd_changed {
+            self.save_to_config();
+            self.just_changed = true;
+        }
+
+        if !handled {
+            match event {
+                Event::PointerMove { x, y } => {
+                    let is_hit = self.hit_test(*x, *y, ctx);
+                    let was = self.hovered();
+                    self.set_hovered(is_hit);
+                    if was != is_hit {
+                        handled = true;
+                    }
+                }
+                _ => {}
+            }
+        }
+
+        handled
+    }
+
+    fn take_change(&mut self) -> bool {
+        let ret = self.just_changed;
+        self.just_changed = false;
+        ret
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        self.handle_event(&Event::MouseButton { button, state, x: px, y: py }, ctx)
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+        self.handle_event(&Event::KeyInput(event.clone()), ctx)
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        self.handle_event(&Event::PointerMove { x: px, y: py }, ctx)
+    }
+
+    fn unfocus(&mut self) {
+        for row in &mut self.rows {
+            row.key_input.unfocus();
+            row.cmd_input.unfocus();
+        }
+    }
+
+    fn hit_test(&self, px: f32, py: f32, _ctx: &UiContext) -> bool {
+        let (x, y, w, h) = self.rect();
+        if w <= 0.0 || h <= 0.0 {
+            return false;
+        }
+        px >= x && px <= x + w && py >= y && py <= y + h
+    }
+}
+
+impl Control for KeybindsControl {}
diff --git a/src/widget/input/mod.rs b/src/widget/input/mod.rs
index b0d2bd8..2b7d13c 100644
--- a/src/widget/input/mod.rs
+++ b/src/widget/input/mod.rs
@@ -10,6 +10,7 @@ pub mod trackpad;
 pub mod font_selector;
 pub mod button_strip;
 pub mod multi_control;
+pub mod keybinds_control;
 
 pub use canvas::Canvas;
 pub use button::{Button, ButtonKind, PageButton};
@@ -23,6 +24,7 @@ pub use trackpad::{Trackpad, Finger};
 pub use font_selector::FontSelector;
 pub use button_strip::ButtonStrip;
 pub use multi_control::{MultiControl, InstancedControl, InstancedWidget, MultiControlRow};
+pub use keybinds_control::{KeybindsControl, KeybindRow};
 
 pub const BREADCRUMB_PADDING: f32 = 8.0;
 pub const SEGMENT_GAP: f32 = 4.0;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index be933ae..4c576d7 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -544,7 +544,8 @@ pub use self::core::{Widget, focus, hover_animation, popovers, clipboard, contex
 pub use self::input::{
     Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, Slider, RangeSlider,
     ColorSelector, Finger, Trackpad, Canvas, get_font_db, ActiveThumb, FontSelector,
-    ButtonStrip, MultiControl, InstancedControl, InstancedWidget, MultiControlRow
+    ButtonStrip, MultiControl, InstancedControl, InstancedWidget, MultiControlRow,
+    KeybindsControl, KeybindRow
 };
 pub use self::container::{
     Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,