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(¤t_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 }