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

src/widget/input/button_strip.rs (26.1K)

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