Skip to main content

gpui_kit_material/components/
date_parts.rs

1//! Building blocks of the M3 date pickers: the calendar day cell, the year
2//! chip, the month/year menu button, the docked month/year list item and the
3//! calendar math they share.
4//!
5//! Reference: the Material 3 Design Kit "Date & time pickers" page
6//! (`.Building Blocks/Local M3 calendar cell`, `.Building Blocks/Year`,
7//! `.Building Blocks/Menu button`) and
8//! <https://m3.material.io/components/date-pickers/specs>. Inspected tokens:
9//! a 48dp cell around a 40dp circle with body-large text; selected days on
10//! primary / on-primary, today outlined (1dp) in primary, range days on a
11//! 40dp secondary-container band with on-secondary-container text, adjacent
12//! month days at 38% on-surface; a 72×36 year pill (selected: primary with
13//! title-medium on-primary, otherwise body-large on-surface-variant); a 40dp
14//! label-large menu button with an 18dp drop arrow. State layers use the
15//! shared [`state`] opacities and the 3dp secondary focus ring.
16//!
17//! The date arithmetic mirrors GPUI Base's calendar (`gpui_base::Calendar`,
18//! whose month grid helper is private) with a fixed six-week grid so a
19//! picker keeps its height while the month changes.
20use super::InteractionPreview;
21use super::button::ButtonFocusRing;
22use super::text_field::type_style;
23use crate::{
24    color::ColorRole,
25    icons::{Icon, Symbol},
26    motion::{self, MotionToken},
27    theme::{blend, color},
28    tokens::{
29        state,
30        typography::{TypeRole, rem},
31    },
32};
33use chrono::{Datelike, Days, Months, NaiveDate, Weekday};
34use gpui::{prelude::*, *};
35use std::rc::Rc;
36
37/// The day cell's square and its circular container, in dp.
38pub const DAY_CELL: f32 = 48.;
39const DAY_CONTAINER: f32 = 40.;
40/// The year chip: a 72×36 pill in an 88×52 cell.
41pub const YEAR_CELL_HEIGHT: f32 = 52.;
42const YEAR_PILL: (f32, f32) = (72., 36.);
43/// The docked menu list item height.
44pub const LIST_ITEM_HEIGHT: f32 = 48.;
45/// Weeks in every month grid: six keeps the picker height stable.
46pub const GRID_WEEKS: usize = 6;
47
48pub(crate) type Activate = Rc<dyn Fn(&mut Window, &mut App)>;
49
50/// The weeks shown for `year`/`month` starting on `first_day`: always
51/// `GRID_WEEKS` (six) rows of seven dates, padded with adjacent-month days.
52pub fn month_weeks(year: i32, month: u32, first_day: Weekday) -> Vec<[NaiveDate; 7]> {
53    let first = first_of_month(year, month);
54    let offset =
55        (first.weekday().num_days_from_sunday() + 7 - first_day.num_days_from_sunday()) % 7;
56    let start = first - Days::new(offset as u64);
57    (0..GRID_WEEKS)
58        .map(|week| std::array::from_fn(|day| start + Days::new((week * 7 + day) as u64)))
59        .collect()
60}
61
62/// The first day of `month` (1-based) in `year`; months outside 1..=12
63/// carry into adjacent years.
64pub fn first_of_month(year: i32, month: u32) -> NaiveDate {
65    let total = year as i64 * 12 + month as i64 - 1;
66    NaiveDate::from_ymd_opt(
67        total.div_euclid(12) as i32,
68        total.rem_euclid(12) as u32 + 1,
69        1,
70    )
71    .unwrap_or(NaiveDate::MIN)
72}
73
74/// `date` moved by `months`, clamped to the end of shorter months.
75pub fn add_months(date: NaiveDate, months: i32) -> NaiveDate {
76    let moved = if months >= 0 {
77        date.checked_add_months(Months::new(months as u32))
78    } else {
79        date.checked_sub_months(Months::new(months.unsigned_abs()))
80    };
81    moved.unwrap_or(date)
82}
83
84/// The weekdays of a grid row starting at `first_day`.
85pub fn week_days(first_day: Weekday) -> [Weekday; 7] {
86    let mut day = first_day;
87    std::array::from_fn(|_| {
88        let current = day;
89        day = day.succ();
90        current
91    })
92}
93
94/// Where a day sits in a selected range, for its band.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub enum DayRange {
97    /// Not in a range.
98    #[default]
99    None,
100    /// The first day of a range: the band runs to its trailing edge.
101    Start,
102    /// A day between the ends: the band spans the cell.
103    Middle,
104    /// The last day: the band runs from its leading edge.
105    End,
106}
107
108/// The paint of one day cell.
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub(crate) struct DayPaint {
111    pub fill: Rgba,
112    pub ink: Rgba,
113    pub layer: Rgba,
114    pub outline: Option<Rgba>,
115}
116
117/// The Figma kit's per-type paint. `selected` is the eased selection
118/// amount, so the fill and ink crossfade on the effects spring.
119pub(crate) fn day_paint(selected: f32, today: bool, range: bool, adjacent: bool) -> DayPaint {
120    let t = selected.clamp(0., 1.);
121    let rest_ink = if range {
122        color(ColorRole::ON_SECONDARY_CONTAINER)
123    } else if today {
124        color(ColorRole::PRIMARY)
125    } else if adjacent {
126        state::disabled(color(ColorRole::ON_SURFACE))
127    } else {
128        color(ColorRole::ON_SURFACE)
129    };
130    let rest_layer = if today && !range {
131        color(ColorRole::PRIMARY)
132    } else {
133        color(ColorRole::ON_SURFACE)
134    };
135    let primary = color(ColorRole::PRIMARY);
136    DayPaint {
137        fill: Rgba {
138            a: primary.a * t,
139            ..primary
140        },
141        ink: blend(rest_ink, color(ColorRole::ON_PRIMARY), t),
142        layer: blend(rest_layer, color(ColorRole::ON_PRIMARY), t),
143        outline: (today && t < 1.).then_some(Rgba {
144            a: primary.a * (1. - t),
145            ..primary
146        }),
147    }
148}
149
150/// The M3 disabled paint of a day: a selected day keeps an on-surface 12%
151/// container, its label and today's outline turn on-surface 38%. Fading
152/// the enabled paint instead leaves a dark on-primary label on a dim
153/// primary container, unreadable in dark themes.
154pub(crate) fn disabled_day_paint(paint: DayPaint, selected: f32) -> DayPaint {
155    let on_surface = color(ColorRole::ON_SURFACE);
156    let content = state::disabled(on_surface);
157    DayPaint {
158        fill: on_surface.opacity(state::DISABLED_CONTAINER * selected.clamp(0., 1.)),
159        ink: content,
160        layer: paint.layer,
161        outline: paint.outline.map(|_| content),
162    }
163}
164
165/// The M3 disabled colors of a year: (container, label).
166pub(crate) fn disabled_year_paint(selected: bool) -> (Rgba, Rgba) {
167    let on_surface = color(ColorRole::ON_SURFACE);
168    (
169        on_surface.opacity(if selected {
170            state::DISABLED_CONTAINER
171        } else {
172            0.
173        }),
174        state::disabled(on_surface),
175    )
176}
177
178/// One day of a calendar grid (`.Building Blocks/Local M3 calendar cell`).
179#[derive(gpui::IntoElement)]
180pub struct CalendarDay {
181    id: ElementId,
182    label: SharedString,
183    accessible: Option<SharedString>,
184    selected: bool,
185    today: bool,
186    adjacent: bool,
187    range: DayRange,
188    disabled: bool,
189    cursor: bool,
190    animate: bool,
191    preview: Option<InteractionPreview>,
192    on_click: Option<Activate>,
193}
194
195impl CalendarDay {
196    /// A day showing `label` (its number, formatted by the caller).
197    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
198        Self {
199            id: id.into(),
200            label: label.into(),
201            accessible: None,
202            selected: false,
203            today: false,
204            adjacent: false,
205            range: DayRange::None,
206            disabled: false,
207            cursor: false,
208            animate: false,
209            preview: None,
210            on_click: None,
211        }
212    }
213    /// The full date announced by assistive technology.
214    pub fn accessible_label(mut self, label: impl Into<SharedString>) -> Self {
215        self.accessible = Some(label.into());
216        self
217    }
218    /// A selected day, or a range end: the filled primary circle.
219    pub fn selected(mut self, selected: bool) -> Self {
220        self.selected = selected;
221        self
222    }
223    /// Today's date: outlined in primary.
224    pub fn today(mut self, today: bool) -> Self {
225        self.today = today;
226        self
227    }
228    /// A day of the previous or next month shown to fill the grid.
229    pub fn adjacent(mut self, adjacent: bool) -> Self {
230        self.adjacent = adjacent;
231        self
232    }
233    /// The day's place in a selected range.
234    pub fn range(mut self, range: DayRange) -> Self {
235        self.range = range;
236        self
237    }
238    pub fn disabled(mut self, disabled: bool) -> Self {
239        self.disabled = disabled;
240        self
241    }
242    /// The grid's keyboard cursor: the focus ring shows here while the grid
243    /// has keyboard focus (`grid_focused`).
244    pub(crate) fn cursor(mut self, cursor: bool) -> Self {
245        self.cursor = cursor;
246        self
247    }
248    /// Crossfade selection changes on the fast effects spring.
249    pub(crate) fn animate(mut self, animate: bool) -> Self {
250        self.animate = animate;
251        self
252    }
253    /// Render a static specimen in one forced interaction state.
254    pub fn preview_state(mut self, state: InteractionPreview) -> Self {
255        self.preview = Some(state);
256        self
257    }
258    pub fn on_click(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
259        self.on_click = Some(Rc::new(f));
260        self
261    }
262}
263
264impl RenderOnce for CalendarDay {
265    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
266        let preview = self.preview;
267        let disabled = self.disabled || preview.is_some_and(InteractionPreview::is_disabled);
268        let live = !disabled && preview.is_none() && self.on_click.is_some();
269        let target = if self.selected { 1. } else { 0. };
270        let selected = if self.animate && preview.is_none() {
271            motion::token_value(
272                (self.id.clone(), "selected"),
273                target,
274                MotionToken::FastEffects,
275                window,
276                cx,
277            )
278        } else {
279            target
280        };
281        let range = self.range != DayRange::None;
282        let paint = day_paint(selected, self.today, range && !self.selected, self.adjacent);
283        let paint = if disabled {
284            disabled_day_paint(paint, selected)
285        } else {
286            paint
287        };
288        let forced = |state: InteractionPreview| !disabled && preview == Some(state);
289        let hover = state::hovered(paint.fill, paint.layer);
290        let press = state::pressed(paint.fill, paint.layer);
291        let focused_layer = state::layer(paint.fill, paint.layer, state::FOCUS);
292        let adjacent_hover = self.adjacent && !self.selected;
293        let hover_ink = color(ColorRole::ON_SURFACE_VARIANT);
294        let band = color(ColorRole::SECONDARY_CONTAINER);
295        let half = DAY_CELL / 2.;
296        let keyboard = self.cursor && live;
297        let container = div()
298            .id("day")
299            .size(rem(DAY_CONTAINER))
300            .flex()
301            .items_center()
302            .justify_center()
303            .rounded_full()
304            .bg(paint.fill)
305            .when_some(paint.outline, |c, outline| {
306                c.border(rem(1.)).border_color(outline)
307            })
308            .text_color(paint.ink)
309            .map(|c| type_style(c, TypeRole::BodyLarge))
310            .when(forced(InteractionPreview::Hovered), |c| {
311                c.bg(hover)
312                    .when(adjacent_hover, |c| c.text_color(hover_ink))
313            })
314            .when(forced(InteractionPreview::Focused), |c| {
315                c.bg(focused_layer)
316                    .when(adjacent_hover, |c| c.text_color(hover_ink))
317            })
318            .when(forced(InteractionPreview::Pressed), |c| c.bg(press))
319            .when(live, |c| {
320                c.cursor_pointer()
321                    .hover(move |s| {
322                        let s = s.bg(hover);
323                        if adjacent_hover {
324                            s.text_color(hover_ink)
325                        } else {
326                            s
327                        }
328                    })
329                    .active(move |s| s.bg(press))
330            })
331            .child(self.label.clone());
332        let ring = ButtonFocusRing::resolved(container, keyboard, rem(DAY_CONTAINER / 2.))
333            .forced(forced(InteractionPreview::Focused));
334        let on_click = self.on_click.filter(|_| live);
335        div()
336            .id(self.id)
337            .relative()
338            .flex_shrink_0()
339            .size(rem(DAY_CELL))
340            .flex()
341            .items_center()
342            .justify_center()
343            .role(Role::GridCell)
344            .aria_label(self.accessible.unwrap_or(self.label))
345            .aria_selected(self.selected || range)
346            .when(self.cursor, |cell| cell.aria_active_descendant())
347            .when(range, |cell| {
348                let (left, width) = match self.range {
349                    DayRange::Start => (half, half),
350                    DayRange::End => (0., half),
351                    _ => (0., DAY_CELL),
352                };
353                cell.child(
354                    div()
355                        .absolute()
356                        .top(rem((DAY_CELL - DAY_CONTAINER) / 2.))
357                        .left(rem(left))
358                        .w(rem(width))
359                        .h(rem(DAY_CONTAINER))
360                        .bg(band)
361                        .when(disabled, |band| band.opacity(state::DISABLED_CONTENT)),
362                )
363            })
364            .child(ring)
365            .when_some(on_click, |cell, f| {
366                cell.on_click(move |_, window, cx| f(window, cx))
367            })
368    }
369}
370
371/// A year in the modal picker's year grid (`.Building Blocks/Year`).
372#[derive(gpui::IntoElement)]
373pub struct YearChip {
374    id: ElementId,
375    label: SharedString,
376    selected: bool,
377    cursor: bool,
378    disabled: bool,
379    preview: Option<InteractionPreview>,
380    on_click: Option<Activate>,
381}
382
383impl YearChip {
384    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
385        Self {
386            id: id.into(),
387            label: label.into(),
388            selected: false,
389            cursor: false,
390            disabled: false,
391            preview: None,
392            on_click: None,
393        }
394    }
395    pub fn selected(mut self, selected: bool) -> Self {
396        self.selected = selected;
397        self
398    }
399    pub(crate) fn cursor(mut self, cursor: bool) -> Self {
400        self.cursor = cursor;
401        self
402    }
403    pub fn disabled(mut self, disabled: bool) -> Self {
404        self.disabled = disabled;
405        self
406    }
407    pub fn preview_state(mut self, state: InteractionPreview) -> Self {
408        self.preview = Some(state);
409        self
410    }
411    pub fn on_click(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
412        self.on_click = Some(Rc::new(f));
413        self
414    }
415}
416
417impl RenderOnce for YearChip {
418    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
419        let preview = self.preview;
420        let disabled = self.disabled || preview.is_some_and(InteractionPreview::is_disabled);
421        let live = !disabled && preview.is_none() && self.on_click.is_some();
422        let forced = |state: InteractionPreview| !disabled && preview == Some(state);
423        let (fill, ink, layer, role) = if self.selected {
424            (
425                color(ColorRole::PRIMARY),
426                color(ColorRole::ON_PRIMARY),
427                color(ColorRole::ON_PRIMARY),
428                TypeRole::TitleMedium,
429            )
430        } else {
431            let clear = Rgba {
432                a: 0.,
433                ..color(ColorRole::ON_SURFACE)
434            };
435            (
436                clear,
437                color(ColorRole::ON_SURFACE_VARIANT),
438                color(ColorRole::ON_SURFACE),
439                TypeRole::BodyLarge,
440            )
441        };
442        let (fill, ink) = if disabled {
443            disabled_year_paint(self.selected)
444        } else {
445            (fill, ink)
446        };
447        let hover = state::hovered(fill, layer);
448        let press = state::pressed(fill, layer);
449        let focus = state::layer(fill, layer, state::FOCUS);
450        let pill = div()
451            .id("year")
452            .w(rem(YEAR_PILL.0))
453            .h(rem(YEAR_PILL.1))
454            .flex()
455            .items_center()
456            .justify_center()
457            .rounded_full()
458            .bg(fill)
459            .text_color(ink)
460            .map(|c| type_style(c, role))
461            .when(forced(InteractionPreview::Hovered), |c| c.bg(hover))
462            .when(forced(InteractionPreview::Focused), |c| c.bg(focus))
463            .when(forced(InteractionPreview::Pressed), |c| c.bg(press))
464            .when(live, |c| {
465                c.cursor_pointer()
466                    .hover(move |s| s.bg(hover))
467                    .active(move |s| s.bg(press))
468            })
469            .child(self.label.clone());
470        let ring = ButtonFocusRing::resolved(pill, self.cursor && live, rem(YEAR_PILL.1 / 2.))
471            .forced(forced(InteractionPreview::Focused));
472        let on_click = self.on_click.filter(|_| live);
473        div()
474            .id(self.id)
475            .flex_1()
476            .h(rem(YEAR_CELL_HEIGHT))
477            .flex()
478            .items_center()
479            .justify_center()
480            .role(Role::GridCell)
481            .aria_label(self.label)
482            .aria_selected(self.selected)
483            .when(self.cursor, |cell| cell.aria_active_descendant())
484            .child(ring)
485            .when_some(on_click, |cell, f| {
486                cell.on_click(move |_, window, cx| f(window, cx))
487            })
488    }
489}
490
491/// The month/year dropdown trigger (`.Building Blocks/Menu button`): a
492/// label-large label and an 18dp arrow that points up while its list is
493/// open, on a 40dp pill. The arrow turns on the fast spatial spring.
494#[derive(gpui::IntoElement)]
495pub struct PickerMenuButton {
496    id: ElementId,
497    label: SharedString,
498    accessible: Option<SharedString>,
499    open: bool,
500    disabled: bool,
501    arrow: bool,
502    preview: Option<InteractionPreview>,
503    on_click: Option<Activate>,
504}
505
506impl PickerMenuButton {
507    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
508        Self {
509            id: id.into(),
510            label: label.into(),
511            accessible: None,
512            open: false,
513            disabled: false,
514            arrow: true,
515            preview: None,
516            on_click: None,
517        }
518    }
519    /// What the button does, for assistive technology (defaults to the label).
520    pub fn accessible_label(mut self, label: impl Into<SharedString>) -> Self {
521        self.accessible = Some(label.into());
522        self
523    }
524    /// Whether the list it controls is open: the arrow points up.
525    pub fn open(mut self, open: bool) -> Self {
526        self.open = open;
527        self
528    }
529    pub fn disabled(mut self, disabled: bool) -> Self {
530        self.disabled = disabled;
531        self
532    }
533    /// Show the drop arrow (the docked picker hides it on the inactive menu).
534    pub fn arrow(mut self, arrow: bool) -> Self {
535        self.arrow = arrow;
536        self
537    }
538    pub fn preview_state(mut self, state: InteractionPreview) -> Self {
539        self.preview = Some(state);
540        self
541    }
542    pub fn on_click(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
543        self.on_click = Some(Rc::new(f));
544        self
545    }
546}
547
548fn drop_arrow() -> Icon {
549    Icon::from(Symbol::named("arrow_drop_down").expect("arrow_drop_down is a Material Symbol"))
550}
551
552impl RenderOnce for PickerMenuButton {
553    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
554        let preview = self.preview;
555        let disabled = self.disabled || preview.is_some_and(InteractionPreview::is_disabled);
556        let turn = if preview.is_some() {
557            if self.open { 1. } else { 0. }
558        } else {
559            motion::token_value(
560                (self.id.clone(), "arrow"),
561                if self.open { 1. } else { 0. },
562                MotionToken::FastSpatial,
563                window,
564                cx,
565            )
566        };
567        let ink = color(ColorRole::ON_SURFACE_VARIANT);
568        let clear = Rgba { a: 0., ..ink };
569        let mut button = gpui_base::Button::new(self.id.clone())
570            .accessibility_label(self.accessible.unwrap_or_else(|| self.label.clone()))
571            .disabled(disabled)
572            .h(rem(40.))
573            .pl(rem(8.))
574            .pr(rem(4.))
575            .flex()
576            .items_center()
577            .gap(rem(8.))
578            .rounded_full()
579            .text_color(ink)
580            .map(|b| type_style(b, TypeRole::LabelLarge))
581            .when(disabled, |b| b.opacity(state::DISABLED_CONTENT))
582            .child(self.label)
583            .when(self.arrow, |b| {
584                b.child(
585                    drop_arrow()
586                        .size(rem(18.))
587                        .color(ink)
588                        .rotate(std::f32::consts::PI * turn),
589                )
590            });
591        if let Some(f) = self.on_click.filter(|_| !disabled) {
592            button = button.on_click(move |_, window, cx| f(window, cx));
593        }
594        let hover = state::hovered(clear, ink);
595        let press = state::pressed(clear, ink);
596        let focus = window
597            .use_keyed_state((self.id, "focus"), cx, |_, cx| cx.focus_handle())
598            .read(cx)
599            .clone();
600        let enabled = !disabled;
601        button = match preview {
602            Some(InteractionPreview::Hovered) => button.bg(hover),
603            Some(InteractionPreview::Focused) => button.bg(state::layer(clear, ink, state::FOCUS)),
604            Some(InteractionPreview::Pressed) => button.bg(press),
605            Some(_) => button,
606            None if enabled => button
607                .cursor_pointer()
608                .hover(move |s| s.bg(hover))
609                .active(move |s| s.bg(press)),
610            None => button,
611        };
612        let button = button.track_focus(&focus).tab_stop(preview.is_none());
613        ButtonFocusRing::new(button, focus, enabled, rem(20.))
614            .forced(enabled && preview == Some(InteractionPreview::Focused))
615    }
616}
617
618/// A docked month or year menu row: a 48dp list item with a leading check
619/// on the selection, which rests on a hovered-strength layer.
620#[derive(gpui::IntoElement)]
621pub struct PickerListItem {
622    id: ElementId,
623    label: SharedString,
624    selected: bool,
625    cursor: bool,
626    preview: Option<InteractionPreview>,
627    on_click: Option<Activate>,
628}
629
630impl PickerListItem {
631    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
632        Self {
633            id: id.into(),
634            label: label.into(),
635            selected: false,
636            cursor: false,
637            preview: None,
638            on_click: None,
639        }
640    }
641    pub fn selected(mut self, selected: bool) -> Self {
642        self.selected = selected;
643        self
644    }
645    pub(crate) fn cursor(mut self, cursor: bool) -> Self {
646        self.cursor = cursor;
647        self
648    }
649    pub fn preview_state(mut self, state: InteractionPreview) -> Self {
650        self.preview = Some(state);
651        self
652    }
653    pub fn on_click(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
654        self.on_click = Some(Rc::new(f));
655        self
656    }
657}
658
659impl RenderOnce for PickerListItem {
660    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
661        let preview = self.preview;
662        let disabled = preview.is_some_and(InteractionPreview::is_disabled);
663        let live = !disabled && preview.is_none() && self.on_click.is_some();
664        let ink = color(ColorRole::ON_SURFACE);
665        let clear = Rgba { a: 0., ..ink };
666        // The kit rests the selected row on the hover-strength layer.
667        let rest = if self.selected {
668            state::hovered(clear, ink)
669        } else {
670            clear
671        };
672        let hover = state::layer(rest, ink, state::HOVER);
673        let press = state::layer(rest, ink, state::PRESS);
674        let row = div()
675            .id("item")
676            .h(rem(LIST_ITEM_HEIGHT))
677            .w_full()
678            .flex()
679            .items_center()
680            .gap(rem(16.))
681            .px(rem(16.))
682            .bg(rest)
683            .text_color(ink)
684            .map(|r| type_style(r, TypeRole::BodyLarge))
685            .when(preview == Some(InteractionPreview::Hovered), |r| {
686                r.bg(hover)
687            })
688            .when(preview == Some(InteractionPreview::Pressed), |r| {
689                r.bg(press)
690            })
691            .when(live, |r| {
692                r.cursor_pointer()
693                    .hover(move |s| s.bg(hover))
694                    .active(move |s| s.bg(press))
695            })
696            .child(
697                div()
698                    .size(rem(24.))
699                    .flex_shrink_0()
700                    .when(self.selected, |slot| {
701                        slot.child(
702                            Icon::from(crate::icons::IconName::Check)
703                                .size(rem(24.))
704                                .color(color(ColorRole::ON_SURFACE_VARIANT)),
705                        )
706                    }),
707            )
708            .child(self.label.clone());
709        let ring = ButtonFocusRing::resolved(row, self.cursor && live, rem(0.))
710            .inward()
711            .forced(preview == Some(InteractionPreview::Focused));
712        let on_click = self.on_click.filter(|_| live);
713        div()
714            .id(self.id)
715            .w_full()
716            .role(Role::ListBoxOption)
717            .aria_label(self.label)
718            .aria_selected(self.selected)
719            .when(self.cursor, |item| item.aria_active_descendant())
720            .child(ring)
721            .when_some(on_click, |item, f| {
722                item.on_click(move |_, window, cx| f(window, cx))
723            })
724    }
725}