GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(keybind): KeybindRecorder onto the narrow traits (Phase 6as leaf sweep)
Model keeps the value/recording/pressed state machine; the field renders
from the laid-out rect (detached label rides the adapter); the recording
key machine reads modifier state through EventCtx.ui (the transitional
shared fields); click-to-arm requests focus via the adapter pointer;
FocusOut cancels recording. Constructor returns Adapted<KeybindRecorder>.
Verified live in data-editor: keybind row select shows the value, click
arms to "[ Press Keys... ]". Key capture itself needs a physical keyboard
(user spot-check).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/widget/input/keybind_recorder.rs | 364 ++++++++++++++++-------------------
1 file changed, 164 insertions(+), 200 deletions(-)
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index d12e98c..9140129 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -1,34 +1,30 @@
use crate::colors;
+use crate::scene::layout::{Rect, Size};
+use crate::scene::paint::PaintCtx;
+use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
use crate::widget::*;
+/// Keybinding capture field (narrow-trait model, Phase 6as leaf sweep): click to arm,
+/// then the next chord/key commits into `value`. The detached control label rides the
+/// adapter's base-label machinery; the model paints only the field itself.
#[derive(Debug, Clone)]
pub struct KeybindRecorder {
- pub base: Widget,
pub value: String,
pub recording: bool,
pub just_changed: bool,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
- pub pressed: bool,
+ pressed: bool,
+ hovered: bool,
}
impl KeybindRecorder {
- pub fn new(value: String) -> Self {
- Self {
- base: Widget::new(),
+ pub fn new(value: String) -> Adapted<KeybindRecorder> {
+ Adapted::new(KeybindRecorder {
value,
recording: false,
just_changed: false,
- parent: None,
- children: Vec::new(),
pressed: false,
- }
- }
-
- pub fn with_config(mut self, file: &str, key: &str) -> Self {
- self.base.config_file = Some(file.to_string());
- self.base.config_key = Some(key.to_string());
- self
+ hovered: false,
+ })
}
pub fn take_change(&mut self) -> bool {
@@ -36,199 +32,41 @@ impl KeybindRecorder {
self.just_changed = false;
changed
}
-}
-
-impl Element for KeybindRecorder {
- crate::impl_widget_base!(KeybindRecorder);
- // Leaf legacy widget: own fonted labels via paint_self (the default no longer
- // drains the text getters).
- fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
- crate::scene::painter::paint_legacy_leaf(
- self, ui, ctx,
- crate::scene::painter::fonted_leaf_labels(self, ui, self.own_labels()),
- );
+ fn commit(&mut self, parts: Vec<&str>) {
+ self.value = parts.join("+");
+ self.just_changed = true;
+ self.recording = false;
}
+}
- fn preferred_height(&self) -> Option<f32> {
- Some(crate::layout::textbox_height())
+impl Layout for KeybindRecorder {
+ fn intrinsic_size(&self) -> Option<Size> {
+ Some(Size::new(0.0, crate::layout::textbox_height()))
}
+}
+impl Paint for KeybindRecorder {
fn color(&self) -> [f32; 4] {
[0.08, 0.08, 0.12, 1.0]
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py, ctx) {
- self.pressed = true;
- return true;
- }
- }
- ElementState::Released => {
- if self.pressed && self.hit_test(px, py, ctx) {
- self.pressed = false;
- self.recording = true;
- ctx.set_focused(self);
- self.mark_dirty(ctx);
- return true;
- }
- let was = self.pressed;
- self.pressed = false;
- return was;
- }
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- if !self.recording {
- return false;
- }
-
- match event.state {
- ElementState::Pressed => {
- match &event.logical_key {
- Key::Named(NamedKey::Escape) => {
- // Cancel recording
- self.recording = false;
- self.mark_dirty(ctx);
- return true;
- }
- Key::Named(NamedKey::Control) |
- Key::Named(NamedKey::Shift) |
- Key::Named(NamedKey::Alt) |
- Key::Named(NamedKey::Super) => {
- // Modifier key pressed. Build current active modifiers list.
- let mut parts = Vec::new();
- if ctx.logo_pressed { parts.push("super"); }
- if ctx.ctrl_pressed { parts.push("ctrl"); }
- if ctx.alt_pressed { parts.push("alt"); }
- if ctx.shift_pressed { parts.push("shift"); }
-
- if !parts.is_empty() {
- self.value = parts.join("+");
- }
- self.mark_dirty(ctx);
- return true;
- }
- Key::Named(key) => {
- // Named key pressed (e.g. Enter, Space, Backspace, Arrow keys)
- let mut parts = Vec::new();
- if ctx.logo_pressed { parts.push("super"); }
- if ctx.ctrl_pressed { parts.push("ctrl"); }
- if ctx.alt_pressed { parts.push("alt"); }
- if ctx.shift_pressed { parts.push("shift"); }
-
- let key_str = match key {
- NamedKey::Backspace => "backspace",
- NamedKey::Tab => "tab",
- NamedKey::Enter => "enter",
- NamedKey::Space => "space",
- NamedKey::ArrowDown => "down",
- NamedKey::ArrowLeft => "left",
- NamedKey::ArrowRight => "right",
- NamedKey::ArrowUp => "up",
- NamedKey::End => "end",
- NamedKey::Home => "home",
- NamedKey::PageDown => "pagedown",
- NamedKey::PageUp => "pageup",
- NamedKey::Delete => "delete",
- _ => "",
- };
-
- if !key_str.is_empty() {
- parts.push(key_str);
- self.value = parts.join("+");
- self.just_changed = true;
- self.recording = false;
- self.mark_dirty(ctx);
- }
- return true;
- }
- Key::Character(ch) => {
- // Character key pressed (e.g. "a", "q", "1", etc.)
- let mut parts = Vec::new();
- if ctx.logo_pressed { parts.push("super"); }
- if ctx.ctrl_pressed { parts.push("ctrl"); }
- if ctx.alt_pressed { parts.push("alt"); }
- if ctx.shift_pressed { parts.push("shift"); }
-
- parts.push(ch.as_str());
- self.value = parts.join("+");
- self.just_changed = true;
- self.recording = false;
- self.mark_dirty(ctx);
- return true;
- }
- }
- }
- ElementState::Released => {
- match &event.logical_key {
- Key::Named(NamedKey::Control) |
- Key::Named(NamedKey::Shift) |
- Key::Named(NamedKey::Alt) |
- Key::Named(NamedKey::Super) => {
- // When a modifier key is released, if we have a non-empty value and no keys are pressed anymore,
- // we commit the recorded modifier keybinding!
- if !self.value.is_empty() && !ctx.ctrl_pressed && !ctx.shift_pressed && !ctx.alt_pressed && !ctx.logo_pressed {
- self.just_changed = true;
- self.recording = false;
- self.mark_dirty(ctx);
- }
- return true;
- }
- _ => {
- return true;
- }
- }
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let top = self.base.label_offset();
- let visual_h = self.base.h - top;
- let bg_color = [0.08, 0.08, 0.12, 1.0];
-
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
let border_color = if self.recording {
colors::HIGHLIGHT_PRIMARY
} else if self.pressed {
[0.30, 0.50, 0.32, 1.0]
- } else if self.base.hovered {
+ } else if self.hovered {
[0.25, 0.25, 0.35, 1.0]
} else {
[0.18, 0.18, 0.24, 1.0]
};
+ ctx.quad(rect, border_color);
+ ctx.quad(
+ Rect { x: rect.x + 1.0, y: rect.y + 1.0, width: rect.width - 2.0, height: rect.height - 2.0 },
+ [0.08, 0.08, 0.12, 1.0],
+ );
- quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, border_color));
- quads.push((self.base.x + 1.0, self.base.y + top + 1.0, self.base.w - 2.0, visual_h - 2.0, bg_color));
- quads
- }
-
-
-
- fn unfocus(&mut self) {
- self.recording = false;
- }
-}
-
-impl Control for KeybindRecorder {}
-
-impl KeybindRecorder {
- pub(crate) fn own_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- let top = self.base.label_offset();
-
- if let Some(lbl) = self.control_label() {
- labels.push(lbl);
- }
-
- let text_y = crate::layout::align_text_y(self.base.y, self.base.h, 12.0, top);
-
let (display_text, color) = if self.recording {
("[ Press Keys... ]".to_string(), [135, 135, 153])
} else if self.value.is_empty() {
@@ -236,16 +74,142 @@ impl KeybindRecorder {
} else {
(self.value.clone(), [221, 221, 226])
};
+ let text_y = crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0);
+ ctx.text(display_text, rect.x + 8.0, text_y, 12.0, color);
+ }
+}
- labels.push(TextLabel {
- text: display_text,
- x: self.base.x + 8.0,
- y: text_y,
- font_size: 12.0,
- color,
- });
+impl Input for KeybindRecorder {
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ match event {
+ Event::MouseButton { button, state, x, y, .. } => {
+ if *button != MouseButton::Left {
+ return false;
+ }
+ match state {
+ // Presses are adapter hit-gated; releases arrive regardless (the
+ // legacy commit/cancel contract).
+ ElementState::Pressed => {
+ self.pressed = true;
+ true
+ }
+ ElementState::Released => {
+ let r = ectx.rect;
+ let inside = *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height;
+ if self.pressed && inside {
+ self.pressed = false;
+ self.recording = true;
+ ectx.request_focus();
+ return true;
+ }
+ let was = self.pressed;
+ self.pressed = false;
+ was
+ }
+ }
+ }
+ Event::KeyInput(key_event) => {
+ if !self.recording {
+ return false;
+ }
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ let mut parts = Vec::new();
+ if ui.logo_pressed {
+ parts.push("super");
+ }
+ if ui.ctrl_pressed {
+ parts.push("ctrl");
+ }
+ if ui.alt_pressed {
+ parts.push("alt");
+ }
+ if ui.shift_pressed {
+ parts.push("shift");
+ }
- labels
+ match key_event.state {
+ ElementState::Pressed => match &key_event.logical_key {
+ Key::Named(NamedKey::Escape) => {
+ self.recording = false;
+ true
+ }
+ Key::Named(NamedKey::Control)
+ | Key::Named(NamedKey::Shift)
+ | Key::Named(NamedKey::Alt)
+ | Key::Named(NamedKey::Super) => {
+ // Modifier pressed: track the active-modifier chord so far.
+ if !parts.is_empty() {
+ self.value = parts.join("+");
+ }
+ true
+ }
+ Key::Named(key) => {
+ let key_str = match key {
+ NamedKey::Backspace => "backspace",
+ NamedKey::Tab => "tab",
+ NamedKey::Enter => "enter",
+ NamedKey::Space => "space",
+ NamedKey::ArrowDown => "down",
+ NamedKey::ArrowLeft => "left",
+ NamedKey::ArrowRight => "right",
+ NamedKey::ArrowUp => "up",
+ NamedKey::End => "end",
+ NamedKey::Home => "home",
+ NamedKey::PageDown => "pagedown",
+ NamedKey::PageUp => "pageup",
+ NamedKey::Delete => "delete",
+ _ => "",
+ };
+ if !key_str.is_empty() {
+ parts.push(key_str);
+ self.commit(parts);
+ }
+ true
+ }
+ Key::Character(ch) => {
+ let ch = ch.clone();
+ parts.push(ch.as_str());
+ self.commit(parts);
+ true
+ }
+ },
+ ElementState::Released => match &key_event.logical_key {
+ Key::Named(NamedKey::Control)
+ | Key::Named(NamedKey::Shift)
+ | Key::Named(NamedKey::Alt)
+ | Key::Named(NamedKey::Super) => {
+ // Modifier released with nothing else held: commit the
+ // recorded modifier-only binding.
+ if !self.value.is_empty()
+ && !ui.ctrl_pressed
+ && !ui.shift_pressed
+ && !ui.alt_pressed
+ && !ui.logo_pressed
+ {
+ self.just_changed = true;
+ self.recording = false;
+ }
+ true
+ }
+ _ => true,
+ },
+ }
+ }
+ Event::MouseEnter => {
+ self.hovered = true;
+ false
+ }
+ Event::MouseLeave => {
+ self.hovered = false;
+ false
+ }
+ Event::FocusOut => {
+ self.recording = false;
+ false
+ }
+ _ => false,
+ }
}
-
}