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 (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(&current_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 }