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 }