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

src/widget/input/keybind_recorder.rs (10.9K)

  1 use crate::colors;
  2 use crate::scene::layout::{Rect, Size};
  3 use crate::scene::paint::PaintCtx;
  4 use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
  5 use crate::widget::*;
  6 
  7 /// Keybinding capture field (narrow-trait model, Phase 6as leaf sweep): click to arm,
  8 /// then the next chord/key commits into `value`. The detached control label rides the
  9 /// adapter's base-label machinery; the model paints only the field itself.
 10 #[derive(Debug, Clone)]
 11 pub struct KeybindRecorder {
 12     pub value: String,
 13     pub recording: bool,
 14     pub just_changed: bool,
 15     pressed: bool,
 16     hovered: bool,
 17     /// Recessed style, the TextBox's: the field is a well carved into the
 18     /// plate below with no fill of its own, its rim lit in the highlight
 19     /// accent while recording (the TextBox's editing treatment). Defaults to
 20     /// `control_relief()`; the flat style is the shared well frame.
 21     recessed: Option<bool>,
 22     /// Keyboard focus (FocusIn / FocusOut): Enter / Space arm recording.
 23     focused: bool,
 24 }
 25 
 26 impl KeybindRecorder {
 27     /// The style in force: the per-widget override (`with_recessed`) when set, else
 28     /// the DE's `control_relief`, read live so a runtime switch
 29     /// (`layout::set_control_relief`) restyles every control at once.
 30     fn recessed(&self) -> bool {
 31         self.recessed.unwrap_or_else(crate::layout::control_relief)
 32     }
 33 
 34     pub fn new(value: String) -> Adapted<KeybindRecorder> {
 35         Adapted::new(KeybindRecorder {
 36             value,
 37             recording: false,
 38             just_changed: false,
 39             pressed: false,
 40             hovered: false,
 41             recessed: None,
 42             focused: false,
 43         })
 44     }
 45 
 46     pub fn take_change(&mut self) -> bool {
 47         let changed = self.just_changed;
 48         self.just_changed = false;
 49         changed
 50     }
 51 
 52     fn commit(&mut self, parts: Vec<&str>) {
 53         self.value = parts.join("+");
 54         self.just_changed = true;
 55         self.recording = false;
 56     }
 57 }
 58 
 59 impl Adapted<KeybindRecorder> {
 60     /// Recessed style: see the `recessed` field.
 61     pub fn with_recessed(mut self, recessed: bool) -> Self {
 62         self.recessed = Some(recessed);
 63         self
 64     }
 65 }
 66 
 67 impl Layout for KeybindRecorder {
 68     fn intrinsic_size(&self) -> Option<Size> {
 69         Some(Size::new(0.0, crate::layout::textbox_height()))
 70     }
 71 }
 72 
 73 impl Paint for KeybindRecorder {
 74     fn color(&self) -> [f32; 4] {
 75         // A well: the plate is its floor, in both styles.
 76         [0.0, 0.0, 0.0, 0.0]
 77     }
 78 
 79     /// The field text in the TextBox's font: both are wells you type into.
 80     fn widget_font(&self) -> Option<String> {
 81         Some(crate::layout::control_label_font_detached())
 82     }
 83 
 84     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 85         if self.recessed() {
 86             let radius = crate::layout::textbox_corner_radius();
 87             let depth = crate::layout::bevel_width().min(rect.height * 0.2);
 88             let (well, radii) = crate::layout::carve_inside(rect, (radius, radius, radius, radius), depth);
 89             if self.recording {
 90                 let hc = crate::color::highlight_primary_color();
 91                 ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
 92             } else {
 93                 ctx.recess(well, radii, depth);
 94             }
 95             self.paint_text(rect, ctx);
 96             return;
 97         }
 98         // The flat style: the one well frame (`colors::well_frame_color`), lit
 99         // while recording, over the plate — no floor of its own, like the
100         // TextBox it stands beside. Rounded like the text wells.
101         let radius = crate::layout::textbox_corner_radius();
102         let frame = colors::well_frame_color(self.hovered, self.recording || self.pressed);
103         ctx.border(rect, (radius, radius, radius, radius), [0.0; 4], frame, 1.0);
104 
105         self.paint_text(rect, ctx);
106     }
107 }
108 
109 impl KeybindRecorder {
110     fn paint_text(&self, rect: Rect, ctx: &mut PaintCtx) {
111         let (display_text, color) = if self.recording {
112             ("[ Press Keys... ]".to_string(), [135, 135, 153])
113         } else if self.value.is_empty() {
114             ("None".to_string(), [127, 127, 127])
115         } else {
116             (self.value.clone(), [221, 221, 226])
117         };
118         let (_, font_size) = crate::layout::control_label_font_detached_parsed();
119         let text_y = crate::layout::align_text_y(rect.y, rect.height, font_size, 0.0);
120         // A recorded chord is as long as the keys pressed into it.
121         ctx.text_with(
122             display_text,
123             rect.x + 8.0,
124             text_y,
125             font_size,
126             color,
127             None,
128             Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]),
129         );
130     }
131 }
132 
133 impl Input for KeybindRecorder {
134     fn focus_role(&self) -> crate::widget::FocusRole {
135         crate::widget::FocusRole::Well
136     }
137     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
138         match event {
139             Event::MouseButton { button, state, x, y, .. } => {
140                 if *button != MouseButton::Left {
141                     return false;
142                 }
143                 match state {
144                     // Presses are adapter hit-gated; releases arrive regardless (the
145                     // legacy commit/cancel contract).
146                     ElementState::Pressed => {
147                         self.pressed = true;
148                         true
149                     }
150                     ElementState::Released => {
151                         let r = ectx.rect;
152                         let inside = *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height;
153                         if self.pressed && inside {
154                             self.pressed = false;
155                             self.recording = true;
156                             ectx.request_focus();
157                             return true;
158                         }
159                         let was = self.pressed;
160                         self.pressed = false;
161                         was
162                     }
163                 }
164             }
165             Event::KeyInput(key_event) if !self.recording => {
166                 // A focused well not yet recording: Enter / Space arm it (the
167                 // click's job, by key). Anything else is not this field's.
168                 if !self.focused || key_event.state != ElementState::Pressed {
169                     return false;
170                 }
171                 match key_event.logical_key {
172                     Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
173                         self.recording = true;
174                         true
175                     }
176                     _ => false,
177                 }
178             }
179             Event::KeyInput(key_event) => {
180                 let Some(ui) = ectx.ui.as_deref_mut() else {
181                     return false;
182                 };
183                 let mut parts = Vec::new();
184                 if ui.logo_pressed {
185                     parts.push("super");
186                 }
187                 if ui.ctrl_pressed {
188                     parts.push("ctrl");
189                 }
190                 if ui.alt_pressed {
191                     parts.push("alt");
192                 }
193                 if ui.shift_pressed {
194                     parts.push("shift");
195                 }
196 
197                 match key_event.state {
198                     ElementState::Pressed => match &key_event.logical_key {
199                         Key::Named(NamedKey::Escape) => {
200                             self.recording = false;
201                             true
202                         }
203                         Key::Named(NamedKey::Control)
204                         | Key::Named(NamedKey::Shift)
205                         | Key::Named(NamedKey::Alt)
206                         | Key::Named(NamedKey::Super) => {
207                             // Modifier pressed: track the active-modifier chord so far.
208                             if !parts.is_empty() {
209                                 self.value = parts.join("+");
210                             }
211                             true
212                         }
213                         Key::Named(key) => {
214                             let key_str = match key {
215                                 NamedKey::Backspace => "backspace",
216                                 NamedKey::Tab => "tab",
217                                 NamedKey::Enter => "enter",
218                                 NamedKey::Space => "space",
219                                 NamedKey::ArrowDown => "down",
220                                 NamedKey::ArrowLeft => "left",
221                                 NamedKey::ArrowRight => "right",
222                                 NamedKey::ArrowUp => "up",
223                                 NamedKey::End => "end",
224                                 NamedKey::Home => "home",
225                                 NamedKey::PageDown => "pagedown",
226                                 NamedKey::PageUp => "pageup",
227                                 NamedKey::Delete => "delete",
228                                 _ => "",
229                             };
230                             if !key_str.is_empty() {
231                                 parts.push(key_str);
232                                 self.commit(parts);
233                             }
234                             true
235                         }
236                         Key::Character(ch) => {
237                             let ch = ch.clone();
238                             parts.push(ch.as_str());
239                             self.commit(parts);
240                             true
241                         }
242                     },
243                     ElementState::Released => match &key_event.logical_key {
244                         Key::Named(NamedKey::Control)
245                         | Key::Named(NamedKey::Shift)
246                         | Key::Named(NamedKey::Alt)
247                         | Key::Named(NamedKey::Super) => {
248                             // Modifier released with nothing else held: commit the
249                             // recorded modifier-only binding.
250                             if !self.value.is_empty()
251                                 && !ui.ctrl_pressed
252                                 && !ui.shift_pressed
253                                 && !ui.alt_pressed
254                                 && !ui.logo_pressed
255                             {
256                                 self.just_changed = true;
257                                 self.recording = false;
258                             }
259                             true
260                         }
261                         _ => true,
262                     },
263                 }
264             }
265             Event::MouseEnter => {
266                 self.hovered = true;
267                 false
268             }
269             Event::MouseLeave => {
270                 self.hovered = false;
271                 false
272             }
273             Event::FocusIn => {
274                 self.focused = true;
275                 false
276             }
277             Event::FocusOut => {
278                 self.focused = false;
279                 self.recording = false;
280                 false
281             }
282             _ => false,
283         }
284     }
285 }