Coverage Report

Created: 2026-08-23 16:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/retroglyph/retroglyph/crates/ui/src/widget/tabs.rs
Line
Count
Source
1
//! [`Tabs`]: a horizontal strip of tab labels with a highlighted selected index.
2
use retroglyph_core::color::{Color, Style};
3
use retroglyph_core::grid::{Rect, Size};
4
use retroglyph_core::text::width;
5
6
use super::strip::Strip;
7
use super::{InteractiveWidget, MinSize, Widget};
8
use crate::Surface;
9
use crate::align::Align;
10
use crate::draw::fill_rect;
11
use crate::interact::Density;
12
use crate::interact::Response;
13
use crate::interact::Sense;
14
use crate::text::draw_clipped;
15
use crate::theme::Theme;
16
17
/// A horizontal strip of `titles` with the tab at `selected` highlighted.
18
///
19
/// Unlike [`Table`](super::Table)/[`List`](super::List), `Tabs` is a plain [`Widget`], not a
20
/// [`StatefulWidget`](super::StatefulWidget): there is no scroll offset for a tab strip, only a
21
/// selected index, so it takes `selected: Option<usize>` directly (set via [`Tabs::select`])
22
/// rather than a [`ListState`](crate::state::ListState): the app is free to drive that index however
23
/// it likes (a plain `usize` it owns, a [`FocusRing`](crate::interact::FocusRing), whatever fits), the same
24
/// "app- or interaction-machinery-driven, widget just reads it" division of labor as every other
25
/// widget here.
26
///
27
/// Titles render left to right, `column_spacing` blank columns apart (default `1`, matching
28
/// [`Table::column_spacing`](super::Table::column_spacing)), with an optional single-character
29
/// `divider` (default `None`, i.e. no divider) centered in that spacing: set with
30
/// [`Tabs::divider`]. Drawing stops once a title would start past the area's right edge; there is
31
/// no horizontal scrolling.
32
///
33
/// `style` and `selected_style` default to [`Theme::DARK`] (as if [`Tabs::theme`] had been
34
/// called); set them with [`Tabs::style`]/[`Tabs::selected_style`].
35
///
36
/// As an [`InteractiveWidget`], `type State = usize` (the same index a caller already threads
37
/// into [`Tabs::select`] for the plain [`Widget`] path): a single id covers the whole strip, and
38
/// a click selects the tab whose column range contains [`Response::pointer_pos`], resolved
39
/// against the very same per-tab column layout the drawing routine uses, so the two can't
40
/// diverge.
41
///
42
/// # Examples
43
///
44
/// ```
45
/// use retroglyph_core::grid::{Grid, Rect};
46
/// use retroglyph_ui::widget::{Tabs, Widget};
47
/// use retroglyph_ui::Surface;
48
///
49
/// let titles = ["Overview", "Details", "Settings"];
50
/// let area = Rect::new(0, 0, 30, 1);
51
/// let mut grid = Grid::new(30, 1);
52
/// Tabs::new(&titles)
53
///     .select(Some(0))
54
///     .render(&mut Surface::new(&mut grid, area, 0));
55
/// ```
56
#[derive(Clone, Copy, Debug)]
57
pub struct Tabs<'a> {
58
    titles: &'a [&'a str],
59
    selected: Option<usize>,
60
    style: Style,
61
    selected_style: Style,
62
    column_spacing: u16,
63
    divider: Option<char>,
64
}
65
66
impl<'a> Tabs<'a> {
67
    /// A tab strip over `titles`, with nothing selected, styled from [`Theme::DARK`] (as if
68
    /// [`Tabs::theme`] had been called).
69
    #[must_use]
70
19
    pub fn new(titles: &'a [&'a str]) -> Self {
71
19
        Self {
72
19
            titles,
73
19
            selected: None,
74
19
            style: Style::new(),
75
19
            selected_style: Style::new(),
76
19
            column_spacing: 1,
77
19
            divider: None,
78
19
        }
79
19
        .theme(Theme::DARK)
80
19
    }
81
82
    /// Select tab `index` (or clear the selection with `None`).
83
    #[must_use]
84
4
    pub const fn select(mut self, index: Option<usize>) -> Self {
85
4
        self.selected = index;
86
4
        self
87
4
    }
88
89
    /// Set the style of unselected tabs.
90
    #[must_use]
91
1
    pub const fn style(mut self, style: Style) -> Self {
92
1
        self.style = style;
93
1
        self
94
1
    }
95
96
    /// Set the style of the selected tab, including its background fill.
97
    #[must_use]
98
1
    pub const fn selected_style(mut self, style: Style) -> Self {
99
1
        self.selected_style = style;
100
1
        self
101
1
    }
102
103
    /// Set the number of blank columns between tabs.
104
    #[must_use]
105
3
    pub const fn column_spacing(mut self, spacing: u16) -> Self {
106
3
        self.column_spacing = spacing;
107
3
        self
108
3
    }
109
110
    /// Set a divider character drawn within the spacing between tabs. `None` (the default) draws
111
    /// no divider: just `column_spacing` blank columns.
112
    #[must_use]
113
1
    pub const fn divider(mut self, divider: Option<char>) -> Self {
114
1
        self.divider = divider;
115
1
        self
116
1
    }
117
118
    /// Applies `theme`'s named roles to this tab strip: `style` becomes `theme.dim` (unselected
119
    /// tabs read as de-emphasized) on `theme.panel_bg`, and `selected_style` becomes `theme.bg`
120
    /// on `theme.accent`: the same bright-on-accent highlight [`super::List::theme`] and
121
    /// [`super::Table::theme`] give their own selected state.
122
    ///
123
    /// `style` sets an explicit background rather than leaving it at [`Style::new()`]'s default:
124
    /// an unset background isn't "transparent" once a real backend draws it (a bare
125
    /// `Color::Default` cell paints as solid black behind the glyph; see
126
    /// `retroglyph-software`'s `DEFAULT_BG`), so this widget assumes it's drawn on
127
    /// `theme.panel_bg`, true when composed with a themed [`super::Panel`]/[`super::Modal`].
128
    /// Drawing this tab strip directly on the raw screen background instead needs a manual
129
    /// `.style(...)` override afterwards.
130
    ///
131
    /// Call before any manual [`Tabs::style`]/[`Tabs::selected_style`] override you want to keep.
132
    #[must_use]
133
20
    pub fn theme(self, theme: Theme) -> Self {
134
20
        self.theme_on(theme, theme.panel_bg)
135
20
    }
136
137
    /// Same as [`Tabs::theme`], but `style` is drawn on `bg` instead of `theme.panel_bg`: for a
138
    /// tab strip drawn directly on a backdrop other than a themed [`super::Panel`]/
139
    /// [`super::Modal`]'s fill. `selected_style` still uses `theme.accent` as its background,
140
    /// unaffected by `bg`. [`Tabs::theme`] is exactly `theme_on(theme, theme.panel_bg)`.
141
    #[must_use]
142
21
    pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
143
21
        self.style = Style::new().fg(theme.dim).bg(bg);
144
21
        self.selected_style = Style::new().fg(theme.bg).bg(theme.accent);
145
21
        self
146
21
    }
147
}
148
149
impl Tabs<'_> {
150
    /// The shared drawing routine both [`Widget::render`] and [`InteractiveWidget::render`] use,
151
    /// parameterized on which tab (if any) is highlighted as selected.
152
15
    fn draw(&self, surface: &mut Surface<'_>, selected: Option<usize>) {
153
15
        let area = surface.area();
154
15
        if area.width() == 0 || 
area.height() == 014
{
155
1
            return;
156
14
        }
157
158
        // `Strip::columns` reports absolute `x`, matching `area`'s own space (needed so
159
        // `Strip::index_at` can compare it directly against an absolute pointer position); `put`/
160
        // `print`/`fill_rect` below address this surface's own local coordinates instead, so
161
        // `area.left()` is subtracted back out at the point of drawing.
162
14
        let strip = Strip::new(self.titles, self.column_spacing);
163
24
        for (index, abs_x, text_width) in 
strip14
.
columns14
(
area14
) {
164
24
            let x = abs_x - area.left();
165
24
            let title = self.titles[index];
166
24
            let style = if Some(index) == selected {
167
6
                self.selected_style
168
            } else {
169
18
                self.style
170
            };
171
24
            if Some(index) == selected && 
text_width > 06
{
172
6
                fill_rect(
173
6
                    surface,
174
6
                    Rect::new(x, 0, text_width, 1),
175
6
                    ' ',
176
6
                    Style::new().bg(style.background()),
177
6
                );
178
18
            }
179
24
            let _ = draw_clipped(surface, (x, 0), text_width, title, Align::Left, style);
180
181
24
            if let Some(
divider2
) = self.divider
182
2
                && index + 1 < self.titles.len()
183
            {
184
                // Divider column: floor of half the inter-tab gap, so it sits just left of center
185
                // for an even `column_spacing`.
186
1
                let mid = x + text_width + self.column_spacing / 2;
187
1
                if mid < area.width() {
188
1
                    surface.put((mid, 0), divider, Style::new());
189
1
                
}0
190
23
            }
191
        }
192
15
    }
193
194
    /// The index of the tab whose column range contains `pos`, or `None` if `pos` falls in the
195
    /// spacing between tabs, past the last drawn tab, or outside `area` entirely: a click there
196
    /// selects nothing rather than clamping to the nearest tab.
197
2
    fn tab_at(&self, area: Rect, pos: retroglyph_core::grid::Pos) -> Option<usize> {
198
2
        Strip::new(self.titles, self.column_spacing).index_at(area, pos)
199
2
    }
200
}
201
202
impl MinSize for Tabs<'_> {
203
    /// Every title's display width, `column_spacing` columns apart, one row tall, floored at
204
    /// `density`'s [`min_target_size`](Density::min_target_size) the same way [`Button`](super::Button)
205
    /// floors its own label: unlike the drawing routine, this ignores any drawing-time
206
    /// truncation, since `min_size` answers how much room the strip needs, not how it degrades
207
    /// when denied that room.
208
3
    fn min_size(&self, density: Density) -> Size {
209
3
        let mut content_width = 0u16;
210
5
        for (index, &title) in 
self.titles3
.
iter3
().
enumerate3
() {
211
5
            content_width = content_width.saturating_add(width(title));
212
5
            if index + 1 < self.titles.len() {
213
2
                content_width = content_width.saturating_add(self.column_spacing);
214
3
            }
215
        }
216
3
        Size::new(content_width, 1).max(density.min_target_size())
217
3
    }
218
}
219
220
impl Widget for Tabs<'_> {
221
13
    fn render(&self, surface: &mut Surface<'_>) {
222
13
        self.draw(surface, self.selected);
223
13
    }
224
}
225
226
impl<Id> InteractiveWidget<Id> for Tabs<'_> {
227
    type State = usize;
228
229
    /// A single id covers the whole strip: clicking resolves which tab via
230
    /// [`Response::pointer_pos`] and this strip's own column layout, rather than each tab
231
    /// registering its own id.
232
0
    fn sense(&self) -> Sense {
233
0
        Sense::click() | Sense::HOVER
234
0
    }
235
236
2
    fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, response: Response<Id>) {
237
2
        let area = surface.area();
238
239
2
        if response.clicked()
240
2
            && let Some(pos) = response.pointer_pos()
241
2
            && let Some(
index1
) = self.tab_at(area, pos)
242
1
        {
243
1
            *state = index;
244
1
        }
245
246
2
        self.draw(surface, Some(*state));
247
2
    }
248
}
249
250
#[cfg(test)]
251
mod tests {
252
    use alloc::vec::Vec;
253
254
    use retroglyph_core::grid::{Grid, HasSize as _, Pos};
255
256
    use super::*;
257
258
    #[test]
259
1
    fn draws_every_title_left_to_right() {
260
1
        let area = Rect::new(0, 0, 20, 1);
261
1
        let titles = ["One", "Two"];
262
1
        let mut grid = Grid::new(20, 1);
263
1
        let tabs = Tabs::new(&titles);
264
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
265
266
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'O');
267
        // "One" (3) + column_spacing (1) = tab 2 starts at column 4.
268
1
        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'T');
269
1
    }
270
271
    #[test]
272
1
    fn highlights_the_selected_tab() {
273
1
        let area = Rect::new(0, 0, 20, 1);
274
1
        let titles = ["One", "Two"];
275
1
        let mut grid = Grid::new(20, 1);
276
1
        let tabs = Tabs::new(&titles).select(Some(1));
277
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
278
279
        // Themed tabs (the `new()` default) distinguish the selected tab by both foreground and
280
        // background color: `theme.bg` on `theme.accent` vs `theme.dim` on `theme.panel_bg`.
281
1
        let selected_style = grid[Pos::new(4, 0)].style();
282
1
        let unselected_style = grid[Pos::new(0, 0)].style();
283
1
        assert_ne!(selected_style.foreground(), unselected_style.foreground());
284
1
        assert_ne!(selected_style.background(), unselected_style.background());
285
1
    }
286
287
    #[test]
288
1
    fn nothing_highlighted_when_unselected() {
289
1
        let area = Rect::new(0, 0, 20, 1);
290
1
        let titles = ["One", "Two"];
291
1
        let mut grid = Grid::new(20, 1);
292
1
        let tabs = Tabs::new(&titles);
293
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
294
295
        // With nothing selected, both tabs use the same unselected `style`, so their backgrounds
296
        // match (unlike a selected tab, which gets `theme.accent` instead).
297
1
        let bg0 = grid[Pos::new(0, 0)].style().background();
298
1
        let bg1 = grid[Pos::new(4, 0)].style().background();
299
1
        assert_eq!(bg0, bg1);
300
1
    }
301
302
    #[test]
303
1
    fn column_spacing_can_be_overridden() {
304
1
        let area = Rect::new(0, 0, 20, 1);
305
1
        let titles = ["A", "B"];
306
1
        let mut grid = Grid::new(20, 1);
307
1
        let tabs = Tabs::new(&titles).column_spacing(3);
308
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
309
310
        // Default spacing (1) would put "B" at column 2; spacing 3 pushes it to column 4.
311
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
312
1
        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'B');
313
1
    }
314
315
    #[test]
316
1
    fn divider_renders_between_tabs_when_set() {
317
1
        let area = Rect::new(0, 0, 20, 1);
318
1
        let titles = ["A", "B"];
319
1
        let mut grid = Grid::new(20, 1);
320
1
        let tabs = Tabs::new(&titles).column_spacing(3).divider(Some('|'));
321
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
322
323
        // "A" at 0, spacing [1,3), midpoint at 1 + 3/2 = 2.
324
1
        assert_eq!(grid[Pos::new(2, 0)].glyph(), '|');
325
1
    }
326
327
    #[test]
328
1
    fn no_divider_by_default() {
329
1
        let area = Rect::new(0, 0, 20, 1);
330
1
        let titles = ["A", "B"];
331
1
        let mut grid = Grid::new(20, 1);
332
1
        let tabs = Tabs::new(&titles);
333
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
334
335
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), ' ');
336
1
    }
337
338
    #[test]
339
1
    fn stops_drawing_past_the_area_width_without_panicking() {
340
1
        let area = Rect::new(0, 0, 4, 1);
341
1
        let titles = ["Alpha", "Bravo", "Charlie"];
342
1
        let mut grid = Grid::new(4, 1);
343
1
        let tabs = Tabs::new(&titles);
344
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0)); // must not panic
345
346
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
347
1
    }
348
349
    #[test]
350
1
    fn style_can_be_overridden() {
351
        use retroglyph_core::color::Color;
352
353
1
        let area = Rect::new(0, 0, 20, 1);
354
1
        let titles = ["One"];
355
1
        let custom = Style::new().fg(Color::RED);
356
1
        let mut grid = Grid::new(20, 1);
357
1
        let tabs = Tabs::new(&titles).style(custom);
358
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
359
360
1
        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::RED);
361
1
    }
362
363
    #[test]
364
1
    fn selected_style_can_be_overridden() {
365
        use retroglyph_core::color::Color;
366
367
1
        let area = Rect::new(0, 0, 20, 1);
368
1
        let titles = ["One"];
369
1
        let custom = Style::new().fg(Color::GREEN).bg(Color::BLUE);
370
1
        let mut grid = Grid::new(20, 1);
371
1
        let tabs = Tabs::new(&titles).selected_style(custom).select(Some(0));
372
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
373
374
1
        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::GREEN);
375
1
        assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::BLUE);
376
1
    }
377
378
    #[test]
379
1
    fn zero_width_is_a_no_op() {
380
1
        let area = Rect::new(0, 0, 0, 1);
381
1
        let titles = ["One"];
382
1
        let mut grid = Grid::new(1, 1);
383
1
        let tabs = Tabs::new(&titles);
384
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
385
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
386
1
    }
387
388
    #[test]
389
1
    fn theme_maps_named_roles_onto_style_and_selected_style() {
390
1
        let area = Rect::new(0, 0, 20, 1);
391
1
        let titles = ["One", "Two"];
392
1
        let mut grid = Grid::new(20, 1);
393
1
        let tabs = Tabs::new(&titles).theme(Theme::DARK).select(Some(1));
394
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
395
396
1
        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.dim);
397
1
        assert_eq!(
398
1
            grid[Pos::new(0, 0)].style().background(),
399
            Theme::DARK.panel_bg
400
        );
401
1
        assert_eq!(grid[Pos::new(4, 0)].style().foreground(), Theme::DARK.bg);
402
1
        assert_eq!(
403
1
            grid[Pos::new(4, 0)].style().background(),
404
            Theme::DARK.accent
405
        );
406
1
    }
407
408
    #[test]
409
1
    fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
410
        use retroglyph_core::color::Color;
411
412
1
        let area = Rect::new(0, 0, 20, 1);
413
1
        let titles = ["One"];
414
1
        let mut grid = Grid::new(20, 1);
415
1
        let tabs = Tabs::new(&titles)
416
1
            .theme_on(Theme::DARK, Color::Default)
417
1
            .select(Some(0));
418
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
419
420
        // `selected_style` uses `theme.accent` as its background regardless of `bg`: only the
421
        // unselected `style` picks up the custom backdrop.
422
1
        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.bg);
423
1
        assert_eq!(
424
1
            grid[Pos::new(0, 0)].style().background(),
425
            Theme::DARK.accent
426
        );
427
1
    }
428
429
    #[test]
430
1
    fn click_selects_the_tab_under_the_pointer() {
431
1
        let area = Rect::new(0, 0, 20, 1);
432
1
        let titles = ["One", "Two"];
433
1
        let tabs = Tabs::new(&titles);
434
1
        let mut state = 0usize;
435
436
1
        let response: Response<()> = Response {
437
1
            hovered: true,
438
1
            clicked: true,
439
1
            pointer_pos: Some(Pos::new(4, 0)), // over "Two"
440
1
            ..Response::default()
441
1
        };
442
1
        let mut grid = Grid::new(20, 1);
443
1
        InteractiveWidget::render(
444
1
            &tabs,
445
1
            &mut Surface::new(&mut grid, area, 0),
446
1
            &mut state,
447
1
            response,
448
        );
449
1
        assert_eq!(state, 1);
450
1
    }
451
452
    #[test]
453
1
    fn click_past_the_last_tab_selects_nothing() {
454
1
        let area = Rect::new(0, 0, 20, 1);
455
1
        let titles = ["One", "Two"];
456
1
        let tabs = Tabs::new(&titles);
457
1
        let mut state = 0usize;
458
459
1
        let response: Response<()> = Response {
460
1
            hovered: true,
461
1
            clicked: true,
462
1
            pointer_pos: Some(Pos::new(10, 0)), // past "Two", still inside the wide area
463
1
            ..Response::default()
464
1
        };
465
1
        let mut grid = Grid::new(20, 1);
466
1
        InteractiveWidget::render(
467
1
            &tabs,
468
1
            &mut Surface::new(&mut grid, area, 0),
469
1
            &mut state,
470
1
            response,
471
        );
472
1
        assert_eq!(state, 0); // unchanged: not clamped to the last tab
473
1
    }
474
475
    #[test]
476
1
    fn tabs_wide_title_loses_half_its_characters() {
477
1
        let area = Rect::new(0, 0, 20, 1);
478
1
        let titles = ["設定", "次"];
479
1
        let mut grid = Grid::new(20, 1);
480
1
        let tabs = Tabs::new(&titles);
481
1
        Widget::render(&tabs, &mut Surface::new(&mut grid, area, 0));
482
483
        // "設定" is 4 columns wide; both characters must draw in full.
484
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), '設');
485
1
        assert_eq!(grid[Pos::new(2, 0)].glyph(), '定');
486
        // "次" (2 cols) starts after "設定" (4) + column_spacing (1) = column 5.
487
1
        assert_eq!(grid[Pos::new(5, 0)].glyph(), '次');
488
1
    }
489
490
    #[test]
491
1
    fn tabs_wide_title_does_not_overlap_the_next_tab() {
492
1
        let area = Rect::new(0, 0, 20, 1);
493
1
        let titles = ["設定", "次"];
494
1
        let tabs = Tabs::new(&titles);
495
1
        let columns: Vec<_> = Strip::new(&titles, tabs.column_spacing)
496
1
            .columns(area)
497
1
            .collect();
498
499
        // "設定" occupies 4 columns starting at 0; "次" must start no earlier than column 5
500
        // (4 + column_spacing 1), never inside "設定"'s own span.
501
1
        assert_eq!(columns[0], (0, 0, 4));
502
1
        assert_eq!(columns[1].1, 5);
503
1
    }
504
505
    #[test]
506
1
    fn min_size_sums_title_widths_and_spacing() {
507
        // "One" (3) + spacing (1) + "Two" (3) = 7 cols, past either density's 6-cell floor, and
508
        // a mouse density's 1-row floor doesn't grow the height, so the natural width wins.
509
1
        let titles = ["One", "Two"];
510
1
        let size = Tabs::new(&titles).min_size(Density::Mouse);
511
1
        assert_eq!(size.width(), 7);
512
1
        assert_eq!(size.height(), 1);
513
1
    }
514
515
    #[test]
516
1
    fn min_size_floors_a_narrow_strip_at_the_density_minimum() {
517
        // A single 1-col title is nowhere near either density's 6-cell width floor.
518
1
        let titles = ["A"];
519
1
        let size = Tabs::new(&titles).min_size(Density::Touch);
520
1
        assert_eq!(size, Density::Touch.min_target_size());
521
1
    }
522
523
    #[test]
524
1
    fn min_size_respects_custom_column_spacing() {
525
        // "A" (1) + spacing (3) + "B" (1) = 5 cols, still under the 6-cell floor, so the floor
526
        // wins; this only proves the spacing was actually added into the sum before flooring.
527
1
        let titles = ["A", "B"];
528
1
        let size = Tabs::new(&titles)
529
1
            .column_spacing(3)
530
1
            .min_size(Density::Mouse);
531
1
        assert_eq!(size.width(), 6);
532
1
    }
533
}