GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/input/button_strip.rs (27.6K)
1 use crate::colors;
2 use crate::widget::*;
3 use crate::widget::input::get_font_db;
4
5 #[derive(Debug, Clone)]
6 pub struct ButtonStrip {
7 x: f32,
8 y: f32,
9 w: f32,
10 h: f32,
11 pub buttons: Vec<String>,
12 pub selected: Option<usize>,
13 pub vertical: bool,
14 pub just_clicked: Option<usize>,
15 pub hovered_idx: Option<usize>,
16 pub pressed_idx: Option<usize>,
17 pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
18 pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
19 pub last_padding: Option<f32>,
20 pub last_font: Option<String>,
21 pub last_scale: Option<f32>,
22 pub inherit_menubar_font: bool,
23 /// The detached control label, synced from the adapter (`Paint::sync_label`):
24 /// the strip's geometry keeps clear of the label strip above it.
25 label: Option<String>,
26 /// Recessed style: the strip is ONE well carved into the plate below (the
27 /// menu's recess around its run), the segments butting together on its
28 /// floor; the selected segment is a plateau raised back out of it, hover
29 /// and press a wash. Defaults to `control_relief()`; the flat style keeps
30 /// the plain state quads.
31 pub recessed: Option<bool>,
32 /// Keyboard focus (FocusIn / FocusOut): the selected segment's plate wears
33 /// the ring; arrows move the selection, Enter / Space press it.
34 focused: bool,
35 }
36
37 impl ButtonStrip {
38 /// The style in force: the per-widget override (`with_recessed`) when set, else
39 /// the DE's `control_relief`, read live so a runtime switch
40 /// (`layout::set_control_relief`) restyles every control at once.
41 fn recessed(&self) -> bool {
42 self.recessed.unwrap_or_else(crate::layout::control_relief)
43 }
44
45 pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
46 Self {
47 x,
48 y,
49 w,
50 h,
51 buttons: Vec::new(),
52 selected: None,
53 vertical: false,
54 just_clicked: None,
55 hovered_idx: None,
56 pressed_idx: None,
57 tab_text_quads: Vec::new(),
58 tab_quads_cache: std::collections::HashMap::new(),
59 last_padding: None,
60 last_font: None,
61 last_scale: None,
62 inherit_menubar_font: false,
63 label: None,
64 recessed: None,
65 focused: false,
66 }
67 }
68
69 /// Recessed style: see the `recessed` field.
70 pub fn with_recessed(mut self, recessed: bool) -> Self {
71 self.recessed = Some(recessed);
72 self
73 }
74
75 pub fn with_inherit_menubar_font(mut self, inherit: bool) -> Self {
76 self.inherit_menubar_font = inherit;
77 self.generate_rotated_labels();
78 self
79 }
80
81 /// The laid-out content rect: the rect mirrored from the adapter by
82 /// `Layout::rect_assigned` (or the constructor arguments until the first layout)
83 /// less the detached label strip at the top of that block, so the segments,
84 /// the well and the hit-testing all sit below the label.
85 fn rect(&self) -> (f32, f32, f32, f32) {
86 let strip = crate::widget::input::slider::detached_strip(&self.label);
87 (self.x, self.y + strip, self.w, (self.h - strip).max(0.0))
88 }
89
90 fn current_font(&self) -> String {
91 if self.inherit_menubar_font {
92 crate::layout::menubar_font()
93 } else {
94 crate::layout::button_strip_font()
95 }
96 }
97
98 fn current_font_parsed(&self) -> (String, f32) {
99 if self.inherit_menubar_font {
100 crate::layout::menubar_font_parsed()
101 } else {
102 crate::layout::button_strip_font_parsed()
103 }
104 }
105
106 pub fn with_buttons(mut self, buttons: Vec<String>) -> Self {
107 self.buttons = buttons;
108 self.generate_rotated_labels();
109 self
110 }
111
112 pub fn with_selected(mut self, selected: Option<usize>) -> Self {
113 self.selected = selected;
114 self.generate_rotated_labels();
115 self
116 }
117
118 pub fn with_vertical(mut self, vertical: bool) -> Self {
119 self.vertical = vertical;
120 self.generate_rotated_labels();
121 self
122 }
123
124 pub fn selected(&self) -> Option<usize> {
125 self.selected
126 }
127
128 pub fn set_selected(&mut self, selected: Option<usize>) {
129 if self.selected != selected {
130 self.selected = selected;
131 self.generate_rotated_labels();
132 }
133 }
134
135 pub fn take_click(&mut self) -> Option<usize> {
136 self.just_clicked.take()
137 }
138
139 pub fn add_button(&mut self, label: &str) {
140 self.buttons.push(label.to_string());
141 self.generate_rotated_labels();
142 }
143
144 pub fn generate_rotated_labels(&mut self) {
145 let current_padding = crate::layout::button_padding();
146 self.last_padding = Some(current_padding);
147 let current_font = self.current_font();
148 self.last_font = Some(current_font);
149 // Recorded BEFORE the horizontal early-out below. It used to be set
150 // only on the vertical path, so a horizontal strip's `tick` saw
151 // `last_scale == None` on every frame, regenerated, and reported a
152 // change — one ButtonStrip made its whole window redraw at 60 fps
153 // forever (cce-gallery, and through it the compositor, sat at ~15%
154 // CPU with nothing happening).
155 let scale = crate::scale::scale_factor().max(1.0);
156 self.last_scale = Some(scale);
157
158 self.tab_text_quads.clear();
159 if !self.vertical || self.buttons.is_empty() {
160 return;
161 }
162
163 let active_color = colors::menubar_tab_label_color();
164 let active_srgb = colors::to_srgb(active_color);
165 let active_r = (active_srgb[0] * 255.0) as u8;
166 let active_g = (active_srgb[1] * 255.0) as u8;
167 let active_b = (active_srgb[2] * 255.0) as u8;
168 let inactive_r = (active_r as f32 * 0.78) as u8;
169 let inactive_g = (active_g as f32 * 0.78) as u8;
170 let inactive_b = (active_b as f32 * 0.78) as u8;
171
172 let (font_fam, font_size) = self.current_font_parsed();
173
174 for (i, page_name) in self.buttons.iter().enumerate() {
175 let color = if self.selected == Some(i) {
176 [active_r, active_g, active_b]
177 } else {
178 [inactive_r, inactive_g, inactive_b]
179 };
180 let hex_color = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
181
182 let trimmed = page_name.trim();
183 let space_idx = trimmed.find(' ');
184 let has_icon = space_idx.map(|idx| trimmed.split_at(idx).0.trim().chars().count() == 1).unwrap_or(false);
185 let label_text = if has_icon {
186 trimmed.split_at(space_idx.unwrap()).1.trim()
187 } else {
188 trimmed
189 };
190
191 let r = self.item_rect(i);
192 let padding_y = crate::layout::button_padding();
193 let y_offset = if has_icon { padding_y + 12.0 } else { 0.0 };
194 let usable_h = (r.3 - y_offset).max(1.0);
195
196 let vertical_buffer = 30.0;
197 let w_px = (r.2 * scale) as u32;
198 let h_px = ((usable_h + vertical_buffer) * scale) as u32;
199
200 if w_px == 0 || h_px == 0 {
201 self.tab_text_quads.push(Vec::new());
202 continue;
203 }
204
205 let cache_key = format!("{}:{}:{}:{:?}:{}:{}", trimmed, w_px, h_px, color, font_fam, scale);
206 if let Some(cached_quads) = self.tab_quads_cache.get(&cache_key) {
207 self.tab_text_quads.push(cached_quads.clone());
208 continue;
209 }
210
211 let svg_data = format!(
212 r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
213 <text x="{}" y="{}" font-family="{}" font-size="{}" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
214 </svg>"##,
215 w_px, h_px,
216 r.2, usable_h + vertical_buffer,
217 r.2 / 2.0, usable_h / 2.0 + vertical_buffer / 2.0,
218 font_fam,
219 font_size,
220 hex_color,
221 r.2 / 2.0, usable_h / 2.0 + vertical_buffer / 2.0,
222 label_text
223 );
224
225 let opt = resvg::usvg::Options::default();
226 let fontdb = get_font_db();
227
228 let mut page_quads = Vec::new();
229 if let Ok(tree) = resvg::usvg::Tree::from_data(svg_data.as_bytes(), &opt, fontdb) {
230 if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
231 resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
232 let pixels = pixmap.data();
233
234 for row in 0..h_px {
235 for col in 0..w_px {
236 let idx = ((row * w_px + col) * 4) as usize;
237 if idx + 3 < pixels.len() {
238 let a = pixels[idx + 3] as f32 / 255.0;
239 if a > 0.0 {
240 let r = ((pixels[idx] as f32 / 255.0) / a).min(1.0);
241 let g = ((pixels[idx + 1] as f32 / 255.0) / a).min(1.0);
242 let b = ((pixels[idx + 2] as f32 / 255.0) / a).min(1.0);
243 page_quads.push((
244 col as f32 / scale,
245 row as f32 / scale,
246 1.2 / scale,
247 1.2 / scale,
248 [r, g, b, a],
249 ));
250 }
251 }
252 }
253 }
254 }
255 }
256 self.tab_quads_cache.insert(cache_key, page_quads.clone());
257 self.tab_text_quads.push(page_quads);
258 }
259 }
260
261 pub fn item_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
262 if self.buttons.is_empty() || idx >= self.buttons.len() {
263 return (0.0, 0.0, 0.0, 0.0);
264 }
265 let (x, y, w, h) = self.rect();
266
267 let get_button_weight = |i: usize| -> f32 {
268 let label = &self.buttons[i];
269 let trimmed = label.trim();
270 let font_info = self.current_font_parsed();
271 let font_fam = font_info.0;
272 let font_size = font_info.1;
273 let padding = crate::layout::button_padding();
274 if self.vertical {
275 let space_idx = trimmed.find(' ');
276 let has_icon = space_idx.map(|idx| trimmed.split_at(idx).0.trim().chars().count() == 1).unwrap_or(false);
277 let label_text = if has_icon {
278 trimmed.split_at(space_idx.unwrap()).1.trim()
279 } else {
280 trimmed
281 };
282 let text_w = crate::widget::display::measure_text_width(label_text, &font_fam, font_size);
283 if has_icon {
284 (text_w + 12.0 + 3.0 * padding).max(1.0)
285 } else {
286 (text_w + 2.0 * padding).max(1.0)
287 }
288 } else {
289 let text_w = crate::widget::display::measure_text_width(label, &font_fam, font_size);
290 (text_w + 2.0 * padding).max(1.0)
291 }
292 };
293
294 let mut weights = Vec::new();
295 for i in 0..self.buttons.len() {
296 weights.push(get_button_weight(i));
297 }
298
299 let spacing = crate::layout::button_strip_spacing();
300 if self.vertical {
301 let mut current_y = y;
302 let mut btn_h = 0.0;
303 for i in 0..=idx {
304 btn_h = weights[i];
305 if i < idx {
306 current_y += btn_h + spacing;
307 }
308 }
309 (x, current_y, w, btn_h)
310 } else {
311 let mut current_x = x;
312 let mut btn_w = 0.0;
313 for i in 0..=idx {
314 btn_w = weights[i];
315 if i < idx {
316 current_x += btn_w + spacing;
317 }
318 }
319 (current_x, y, btn_w, h)
320 }
321 }
322 }
323
324 impl crate::widget::Layout for ButtonStrip {
325 /// A horizontal strip is one button row tall; a vertical one is content-sized.
326 fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
327 if self.vertical {
328 None
329 } else {
330 Some(crate::scene::layout::Size::new(0.0, crate::layout::button_height()))
331 }
332 }
333
334 // The legacy set_rect override: regenerate the rotated tab labels only when the rect
335 // actually changed.
336 fn rect_assigned(&mut self, rect: crate::scene::layout::Rect) {
337 if self.x != rect.x || self.y != rect.y || self.w != rect.width || self.h != rect.height {
338 self.x = rect.x;
339 self.y = rect.y;
340 self.w = rect.width;
341 self.h = rect.height;
342 self.generate_rotated_labels();
343 }
344 }
345 }
346
347 impl crate::widget::Paint for ButtonStrip {
348 fn color(&self) -> [f32; 4] {
349 [0.0, 0.0, 0.0, 0.0]
350 }
351
352 fn widget_font(&self) -> Option<String> {
353 Some(self.current_font())
354 }
355
356 fn sync_label(&mut self, label: &str) {
357 self.label = Some(label.to_string());
358 }
359
360 fn paint(&self, _rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx) {
361 use crate::scene::layout::Rect;
362 // The strip's well: one recess around the whole run, rounded like the
363 // buttons, before the segments so their fills sit on its floor. The
364 // segment fills are rounded at the same radius (they reach legacy hosts
365 // through `all_rounded_quads`, which the Paginator aggregates).
366 let (sx, sy, sw, sh) = self.rect();
367 let radius = crate::layout::button_corner_radius();
368 let depth = if self.recessed() {
369 let short = if self.vertical { sw } else { sh };
370 let depth = crate::layout::bevel_width().min(short * 0.2);
371 let (well, radii) = crate::layout::carve_inside(Rect { x: sx, y: sy, width: sw, height: sh }, (radius, radius, radius, radius), depth);
372 pc.recess(well, radii, depth);
373 depth
374 } else {
375 0.0
376 };
377 // Per-item state backgrounds, plus the rotated (SVG-rasterized) vertical
378 // tab text clamped to the strip.
379 for i in 0..self.buttons.len() {
380 let r = self.item_rect(i);
381 let mut bg_color = [0.0, 0.0, 0.0, 0.0];
382 if Some(i) == self.selected {
383 bg_color = colors::PANEL_MENU_FOCUSED;
384 } else if Some(i) == self.pressed_idx {
385 bg_color = colors::BUTTON_PRESS;
386 } else if Some(i) == self.hovered_idx {
387 bg_color = colors::PANEL_MENU_HOVER;
388 }
389 // The segment's footprint: in the well, inset by the wall's inner
390 // half-span so it stands on the floor (the selected plateau's rect);
391 // flat, the item rect itself. The state fill and the plateau share it.
392 let inset = if self.recessed() { depth * 0.5 } else { 0.0 };
393 let seg = Rect { x: r.0 + inset, y: r.1 + inset, width: (r.2 - 2.0 * inset).max(0.0), height: (r.3 - 2.0 * inset).max(0.0) };
394 let seg_r = (radius - inset).max(0.0);
395 let focus_ring = self.focused && Some(i) == self.selected;
396 if bg_color != [0.0, 0.0, 0.0, 0.0] {
397 if focus_ring && !self.recessed() {
398 // Flat: no rim to light, so the selected fill wears a hairline
399 // ring in the highlight.
400 let t = crate::widget::ControlPlate::focus_tint();
401 pc.border(seg, (seg_r, seg_r, seg_r, seg_r), bg_color, [t[0], t[1], t[2], 1.0], 1.0);
402 } else {
403 pc.rounded_rect(seg, seg_r, (true, true, true, true), bg_color);
404 }
405 }
406 if self.recessed() && Some(i) == self.selected {
407 // The selected segment: a raised control plate standing on the
408 // well floor, faceless (the floor shows through), at the well's
409 // depth — its rim lit while the strip holds keyboard focus.
410 pc.control_plate(
411 &crate::widget::ControlPlate::control(seg, seg_r, crate::widget::PlateStance::Raised, None)
412 .with_depth(depth)
413 .with_tint(focus_ring.then(crate::widget::ControlPlate::focus_tint)),
414 );
415 }
416
417 if self.vertical {
418 if i < self.tab_text_quads.len() {
419 let min_y = self.y;
420 let max_y = self.y + self.h;
421 let page_name = &self.buttons[i];
422 let trimmed = page_name.trim();
423 let space_idx = trimmed.find(' ');
424 let has_icon = space_idx.map(|idx| trimmed.split_at(idx).0.trim().chars().count() == 1).unwrap_or(false);
425 let padding_y = crate::layout::button_padding();
426 let y_offset = if has_icon { padding_y + 12.0 } else { 0.0 };
427
428 for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
429 let absolute_x = r.0 + qx;
430 let absolute_y = r.1 + y_offset + qy - 15.0;
431
432 let ry1 = absolute_y.max(min_y);
433 let ry2 = (absolute_y + qh).min(max_y);
434 let rh = ry2 - ry1;
435 if rh > 0.0 {
436 pc.quad(Rect { x: absolute_x, y: ry1, width: qw, height: rh }, qc);
437 }
438 }
439 }
440 }
441 }
442
443 // Own labels (horizontal button text / vertical icon glyphs).
444 for tl in self.own_labels() {
445 pc.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
446 }
447 }
448 }
449
450 impl crate::widget::Input for ButtonStrip {
451 // The legacy dispatch reached mouse_input ungated (the hosts call it directly, and a
452 // press on another tab must land while a dropdown popover covers the strip); the item
453 // scan below is the real gate.
454 fn gates_presses(&self) -> bool {
455 false
456 }
457
458 fn wants_tick(&self) -> bool {
459 true
460 }
461
462 // Config watch: regenerate the rotated labels when padding/font/scale change.
463 fn tick(&mut self, _dt: f32, _rect: crate::scene::layout::Rect) -> bool {
464 let mut changed = false;
465 let current_padding = crate::layout::button_padding();
466 let current_font = self.current_font();
467 let current_scale = crate::scale::scale_factor().max(1.0);
468 if self.last_padding != Some(current_padding)
469 || self.last_font.as_ref() != Some(¤t_font)
470 || self.last_scale != Some(current_scale)
471 {
472 self.generate_rotated_labels();
473 changed = true;
474 }
475 changed
476 }
477
478 fn focus_role(&self) -> crate::widget::FocusRole {
479 crate::widget::FocusRole::Plate
480 }
481
482 fn on_event(&mut self, event: &Event, _ectx: &mut crate::widget::EventCtx) -> bool {
483 match event {
484 Event::FocusIn => {
485 self.focused = true;
486 true
487 }
488 Event::FocusOut => {
489 self.focused = false;
490 true
491 }
492 Event::KeyInput(key_event) => {
493 // The segments are plates in a row: the arrows along the strip's
494 // axis move the selection (a keyboard press of the neighbour),
495 // Enter / Space press the selected one again.
496 if !self.focused || key_event.state != ElementState::Pressed || self.buttons.is_empty() {
497 return false;
498 }
499 let n = self.buttons.len();
500 let step: Option<isize> = match key_event.logical_key {
501 Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => Some(-1),
502 Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => Some(1),
503 Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => Some(0),
504 _ => None,
505 };
506 let Some(step) = step else { return false };
507 let target = match (self.selected, step) {
508 (Some(i), 0) => i,
509 (None, _) => if step < 0 { n - 1 } else { 0 },
510 (Some(i), s) => ((i as isize + s).rem_euclid(n as isize)) as usize,
511 };
512 self.selected = Some(target);
513 self.just_clicked = Some(target);
514 self.generate_rotated_labels();
515 true
516 }
517 Event::MouseButton { button, state, x: px, y: py, .. } => {
518 if *button != MouseButton::Left {
519 return false;
520 }
521 let mut changed = false;
522 match state {
523 ElementState::Pressed => {
524 for i in 0..self.buttons.len() {
525 let r = self.item_rect(i);
526 if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
527 self.pressed_idx = Some(i);
528 changed = true;
529 break;
530 }
531 }
532 }
533 ElementState::Released => {
534 if let Some(pressed) = self.pressed_idx {
535 let r = self.item_rect(pressed);
536 if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
537 if self.selected != Some(pressed) {
538 self.selected = Some(pressed);
539 self.just_clicked = Some(pressed);
540 self.generate_rotated_labels();
541 } else {
542 self.selected = None;
543 self.just_clicked = Some(pressed);
544 self.generate_rotated_labels();
545 }
546 }
547 changed = true;
548 }
549 self.pressed_idx = None;
550 }
551 }
552 changed
553 }
554 Event::PointerMove { x: px, y: py, .. } => {
555 let old_hovered = self.hovered_idx;
556 self.hovered_idx = None;
557 for i in 0..self.buttons.len() {
558 let r = self.item_rect(i);
559 if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
560 self.hovered_idx = Some(i);
561 break;
562 }
563 }
564 old_hovered != self.hovered_idx
565 }
566 Event::KeyInput(event) => {
567 if event.state != ElementState::Pressed {
568 return false;
569 }
570 if self.buttons.is_empty() {
571 return false;
572 }
573
574 let current = self.selected.unwrap_or(0);
575 let next;
576
577 match event.logical_key {
578 Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
579 if current > 0 {
580 next = current - 1;
581 } else {
582 next = self.buttons.len() - 1;
583 }
584 }
585 Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
586 if current + 1 < self.buttons.len() {
587 next = current + 1;
588 } else {
589 next = 0;
590 }
591 }
592 _ => return false,
593 }
594
595 if Some(next) != self.selected {
596 self.selected = Some(next);
597 self.just_clicked = Some(next);
598 self.generate_rotated_labels();
599 return true;
600 }
601 false
602 }
603 _ => false,
604 }
605 }
606 }
607
608 impl ButtonStrip {
609 pub(crate) fn own_labels(&self) -> Vec<TextLabel> {
610 let mut labels = Vec::new();
611 let font_info = self.current_font_parsed();
612 let font_fam = font_info.0;
613 let font_size = font_info.1;
614 for (i, btn_label) in self.buttons.iter().enumerate() {
615 let r = self.item_rect(i);
616 let color = if Some(i) == self.selected {
617 [0xf0, 0xf0, 0xf5]
618 } else {
619 [0xa8, 0xa8, 0xb3]
620 };
621 if self.vertical {
622 let trimmed = btn_label.trim();
623 let space_idx = trimmed.find(' ');
624 let has_icon = space_idx.map(|idx| trimmed.split_at(idx).0.trim().chars().count() == 1).unwrap_or(false);
625 if has_icon {
626 let space_idx = space_idx.unwrap();
627 let (icon, _) = trimmed.split_at(space_idx);
628 let icon = icon.trim();
629 if !icon.is_empty() {
630 let icon_font_size = 14.0;
631 let est_icon_w = crate::widget::display::measure_text_width(icon, &font_fam, icon_font_size);
632 let padding_y = crate::layout::button_padding();
633 let icon_y = r.1 + (padding_y - 2.0).max(0.0);
634 labels.push(TextLabel {
635 text: icon.to_string(),
636 x: r.0 + (r.2 - est_icon_w) / 2.0,
637 y: icon_y,
638 font_size: icon_font_size,
639 color,
640 });
641 }
642 }
643 } else {
644 let est_w = crate::widget::display::measure_text_width(btn_label, &font_fam, font_size);
645 labels.push(TextLabel {
646 text: btn_label.clone(),
647 x: r.0 + (r.2 - est_w) / 2.0,
648 y: crate::layout::align_text_y(r.1, r.3, font_size, 0.0),
649 font_size,
650 color,
651 });
652 }
653 }
654 labels
655 }
656 }
657
658 #[cfg(test)]
659 mod focus_tests {
660 use super::*;
661 use crate::widget::{Event, KeyEvent, UiContext, WidgetHost};
662
663 fn press(key: NamedKey) -> Event {
664 Event::KeyInput(KeyEvent { logical_key: Key::Named(key), state: ElementState::Pressed, text: None, repeat: false, ctrl: false, shift: false, alt: false })
665 }
666
667 /// Focused, the arrows move the selection between the segment plates (a
668 /// keyboard press of the neighbour, so hosts see a click), wrapping; Enter
669 /// presses the selected one again.
670 #[test]
671 fn arrows_move_the_selection_and_enter_presses_it() {
672 let mut ctx = UiContext::new();
673 let mut s = Adapted::new(ButtonStrip::new(0.0, 0.0, 200.0, 28.0).with_buttons(vec!["One".into(), "Two".into(), "Three".into()]).with_selected(Some(0)));
674 WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 28.0);
675 assert!(!s.handle_event(&press(NamedKey::ArrowRight), &mut ctx), "unfocused: not this strip's key");
676 s.handle_event(&Event::FocusIn, &mut ctx);
677 assert!(s.handle_event(&press(NamedKey::ArrowRight), &mut ctx));
678 assert_eq!(s.selected(), Some(1));
679 assert_eq!(s.inner_mut().take_click(), Some(1), "hosts see a click");
680 assert!(s.handle_event(&press(NamedKey::ArrowLeft), &mut ctx));
681 assert!(s.handle_event(&press(NamedKey::ArrowLeft), &mut ctx));
682 assert_eq!(s.selected(), Some(2), "wraps");
683 assert!(s.handle_event(&press(NamedKey::Enter), &mut ctx));
684 assert_eq!(s.inner_mut().take_click(), Some(2));
685 }
686 }