1use super::InteractionPreview;
18use super::focus::{RingPlacement, RingedButton};
19use super::{CheckboxState, MaterialCheckbox, MaterialRadio, MaterialSwitch};
20use crate::{
21 color::ColorRole,
22 icons::Icon,
23 theme::color,
24 tokens::{
25 shape::{OuterCorners, list_corners},
26 state,
27 typography::{TypeRole, rem},
28 },
29};
30use gpui::{prelude::*, *};
31use std::rc::Rc;
32
33pub const LIST_SEGMENT_RADIUS: Pixels = px(16.);
35pub const LIST_SEGMENT_INNER_RADIUS: Pixels = px(4.);
37pub const LIST_SEGMENT_GAP: Pixels = px(2.);
39
40#[allow(clippy::large_enum_variant)]
44pub enum ListLeading {
45 None,
46 Monogram(SharedString),
48 Icon(Icon),
50 Image(Option<AnyElement>),
52 Video(Option<AnyElement>),
54 Checkbox(bool),
55 Radio(bool),
56 Switch(bool),
57}
58
59#[allow(clippy::large_enum_variant)]
63pub enum ListTrailing {
64 None,
65 Icon(Icon),
67 Checkbox(bool),
68 Radio(bool),
69 Switch(bool),
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub enum ListDensity {
77 #[default]
78 Standard,
79 Compact,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum ListLines {
86 One,
87 Two,
88 Three,
89}
90
91impl ListLines {
92 pub fn of(overline: bool, supporting: bool, supporting_lines: usize) -> Self {
95 let lines = 1 + usize::from(overline) + if supporting { supporting_lines } else { 0 };
96 match lines {
97 1 => Self::One,
98 2 => Self::Two,
99 _ => Self::Three,
100 }
101 }
102}
103
104fn row_metrics(lines: ListLines, density: ListDensity) -> (f32, f32) {
106 match (density, lines) {
107 (ListDensity::Standard, ListLines::One) => (56., 8.),
108 (ListDensity::Standard, ListLines::Two) => (72., 8.),
109 (ListDensity::Standard, ListLines::Three) => (88., 12.),
110 (ListDensity::Compact, ListLines::One) => (40., 0.),
111 (ListDensity::Compact, ListLines::Two) => (56., 4.),
112 (ListDensity::Compact, ListLines::Three) => (72., 4.),
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq)]
118struct ListPaint {
119 container: Rgba,
120 headline: Rgba,
121 support: Rgba,
122 layer: Rgba,
124}
125
126fn list_paint(selected: bool, disabled: bool) -> ListPaint {
129 if disabled {
130 let ink = state::disabled(color(ColorRole::ON_SURFACE));
131 return ListPaint {
132 container: if selected {
133 color(ColorRole::ON_SURFACE).opacity(state::DISABLED_CONTAINER)
134 } else {
135 color(ColorRole::SURFACE)
136 },
137 headline: ink,
138 support: ink,
139 layer: color(ColorRole::ON_SURFACE),
140 };
141 }
142 if selected {
143 ListPaint {
144 container: color(ColorRole::SECONDARY_CONTAINER),
145 headline: color(ColorRole::ON_SECONDARY_CONTAINER),
146 support: color(ColorRole::ON_SECONDARY_CONTAINER),
147 layer: color(ColorRole::ON_SECONDARY_CONTAINER),
148 }
149 } else {
150 ListPaint {
151 container: color(ColorRole::SURFACE),
152 headline: color(ColorRole::ON_SURFACE),
153 support: color(ColorRole::ON_SURFACE_VARIANT),
154 layer: color(ColorRole::ON_SURFACE),
155 }
156 }
157}
158
159type Click = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
160
161#[derive(gpui::IntoElement)]
164pub struct ListItem {
165 id: ElementId,
166 headline: SharedString,
167 overline: Option<SharedString>,
168 supporting: Option<SharedString>,
169 supporting_lines: usize,
170 trailing_text: Option<SharedString>,
171 leading: ListLeading,
172 trailing: ListTrailing,
173 divider: bool,
174 divider_inset: bool,
175 selected: bool,
176 disabled: bool,
177 density: ListDensity,
178 corners: Option<OuterCorners>,
179 container: ColorRole,
180 preview: Option<InteractionPreview>,
181 on_click: Option<Click>,
182}
183
184impl ListItem {
185 pub fn new(id: impl Into<ElementId>, headline: impl Into<SharedString>) -> Self {
186 Self {
187 id: id.into(),
188 headline: headline.into(),
189 overline: None,
190 supporting: None,
191 supporting_lines: 1,
192 trailing_text: None,
193 leading: ListLeading::None,
194 trailing: ListTrailing::None,
195 divider: false,
196 divider_inset: false,
197 selected: false,
198 disabled: false,
199 density: ListDensity::Standard,
200 corners: None,
201 container: ColorRole::SURFACE,
202 preview: None,
203 on_click: None,
204 }
205 }
206 pub fn overline(mut self, text: impl Into<SharedString>) -> Self {
208 self.overline = Some(text.into());
209 self
210 }
211 pub fn supporting(mut self, text: impl Into<SharedString>) -> Self {
213 self.supporting = Some(text.into());
214 self
215 }
216 pub fn three_line(mut self, three_line: bool) -> Self {
218 self.supporting_lines = if three_line { 2 } else { 1 };
219 self
220 }
221 pub fn trailing_text(mut self, text: impl Into<SharedString>) -> Self {
223 self.trailing_text = Some(text.into());
224 self
225 }
226 pub fn leading(mut self, leading: ListLeading) -> Self {
227 self.leading = leading;
228 self
229 }
230 pub fn trailing(mut self, trailing: ListTrailing) -> Self {
231 self.trailing = trailing;
232 self
233 }
234 pub fn divider(mut self, divider: bool) -> Self {
236 self.divider = divider;
237 self
238 }
239 pub fn divider_inset(mut self, inset: bool) -> Self {
242 self.divider_inset = inset;
243 self
244 }
245 pub fn selected(mut self, selected: bool) -> Self {
246 self.selected = selected;
247 self
248 }
249 pub fn disabled(mut self, disabled: bool) -> Self {
250 self.disabled = disabled;
251 self
252 }
253 pub fn container(mut self, role: ColorRole) -> Self {
257 self.container = role;
258 self
259 }
260 pub fn density(mut self, density: ListDensity) -> Self {
261 self.density = density;
262 self
263 }
264 pub fn segment(mut self, corners: OuterCorners) -> Self {
267 self.corners = Some(corners);
268 self
269 }
270 pub fn preview_state(mut self, state: InteractionPreview) -> Self {
274 self.preview = Some(state);
275 self
276 }
277 pub fn on_click(mut self, f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
280 self.on_click = Some(Rc::new(f));
281 self
282 }
283
284 pub fn lines(&self) -> ListLines {
286 ListLines::of(
287 self.overline.is_some(),
288 self.supporting.is_some(),
289 self.supporting_lines,
290 )
291 }
292}
293
294fn segment_radii(outer: OuterCorners) -> Corners<Pixels> {
296 let radius = |outside: bool| {
297 if outside {
298 LIST_SEGMENT_RADIUS
299 } else {
300 LIST_SEGMENT_INNER_RADIUS
301 }
302 };
303 Corners {
304 top_left: radius(outer[0]),
305 top_right: radius(outer[1]),
306 bottom_right: radius(outer[2]),
307 bottom_left: radius(outer[3]),
308 }
309}
310
311fn typed<E: Styled>(element: E, role: TypeRole) -> E {
312 let scale = role.scale();
313 element
314 .font_family(scale.font_family)
315 .text_size(scale.font_size)
316 .line_height(scale.line_height)
317 .font_weight(FontWeight(scale.font_weight as f32))
318}
319
320fn control_slot(width: f32, control: impl IntoElement) -> Div {
322 div()
323 .flex()
324 .flex_shrink_0()
325 .items_center()
326 .justify_center()
327 .w(rem(width))
328 .h(rem(24.))
329 .child(control)
330}
331
332fn control_preview(disabled: bool) -> InteractionPreview {
335 if disabled {
336 InteractionPreview::Disabled
337 } else {
338 InteractionPreview::Enabled
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344enum Control {
345 Checkbox,
346 Radio,
347 Switch,
348}
349
350fn control(
351 id: &'static str,
352 kind: Control,
353 label: &SharedString,
354 on: bool,
355 disabled: bool,
356) -> AnyElement {
357 let preview = control_preview(disabled);
358 match kind {
359 Control::Checkbox => control_slot(
360 24.,
361 MaterialCheckbox::new(id, label.clone())
362 .state(if on {
363 CheckboxState::Checked
364 } else {
365 CheckboxState::Unchecked
366 })
367 .preview_state(preview),
368 )
369 .into_any_element(),
370 Control::Radio => control_slot(
371 24.,
372 MaterialRadio::new(id, label.clone())
373 .selected(on)
374 .preview_state(preview),
375 )
376 .into_any_element(),
377 Control::Switch => div()
378 .flex_shrink_0()
379 .child(
380 MaterialSwitch::new(id, label.clone())
381 .checked(on)
382 .preview_state(preview),
383 )
384 .into_any_element(),
385 }
386}
387
388fn thumbnail(width: f32, height: f32, content: Option<AnyElement>) -> Div {
390 div()
391 .flex_shrink_0()
392 .w(rem(width))
393 .h(rem(height))
394 .overflow_hidden()
395 .bg(color(ColorRole::SURFACE_CONTAINER_HIGH))
396 .children(content)
397}
398
399impl ListItem {
400 fn leading_element(&mut self, ink: Rgba, disabled: bool) -> Option<AnyElement> {
401 let label = self.headline.clone();
402 let leading = std::mem::replace(&mut self.leading, ListLeading::None);
403 Some(match leading {
404 ListLeading::None => return None,
405 ListLeading::Monogram(text) => typed(
406 div()
407 .flex()
408 .flex_shrink_0()
409 .items_center()
410 .justify_center()
411 .size(rem(40.))
412 .rounded_full()
413 .bg(if disabled {
414 color(ColorRole::ON_SURFACE).opacity(state::DISABLED_CONTAINER)
415 } else {
416 color(ColorRole::PRIMARY_CONTAINER)
417 })
418 .text_color(if disabled {
419 ink
420 } else {
421 color(ColorRole::ON_PRIMARY_CONTAINER)
422 }),
423 TypeRole::TitleMedium,
424 )
425 .child(text)
426 .into_any_element(),
427 ListLeading::Icon(icon) => div()
428 .flex_shrink_0()
429 .size(rem(24.))
430 .child(icon.size(rem(24.)).color(ink))
431 .into_any_element(),
432 ListLeading::Image(content) => thumbnail(56., 56., content).into_any_element(),
433 ListLeading::Video(content) => thumbnail(114., 64., content).into_any_element(),
434 ListLeading::Checkbox(checked) => {
435 control("checkbox", Control::Checkbox, &label, checked, disabled)
436 }
437 ListLeading::Radio(selected) => {
438 control("radio", Control::Radio, &label, selected, disabled)
439 }
440 ListLeading::Switch(checked) => {
441 control("switch", Control::Switch, &label, checked, disabled)
442 }
443 })
444 }
445
446 fn trailing_element(&mut self, ink: Rgba, disabled: bool) -> Option<AnyElement> {
447 let label = self.headline.clone();
448 let trailing = std::mem::replace(&mut self.trailing, ListTrailing::None);
449 Some(match trailing {
450 ListTrailing::None => return None,
451 ListTrailing::Icon(icon) => div()
452 .flex_shrink_0()
453 .size(rem(24.))
454 .child(icon.size(rem(24.)).color(ink))
455 .into_any_element(),
456 ListTrailing::Checkbox(checked) => control(
457 "trailing-checkbox",
458 Control::Checkbox,
459 &label,
460 checked,
461 disabled,
462 ),
463 ListTrailing::Radio(selected) => {
464 control("trailing-radio", Control::Radio, &label, selected, disabled)
465 }
466 ListTrailing::Switch(checked) => control(
467 "trailing-switch",
468 Control::Switch,
469 &label,
470 checked,
471 disabled,
472 ),
473 })
474 }
475}
476
477impl ListItem {
478 fn toggle_state(&self) -> (Option<bool>, bool) {
481 let toggled = match (&self.leading, &self.trailing) {
482 (ListLeading::Checkbox(on) | ListLeading::Switch(on), _)
483 | (_, ListTrailing::Checkbox(on) | ListTrailing::Switch(on)) => Some(*on),
484 _ => None,
485 };
486 let radio = matches!(
487 (&self.leading, &self.trailing),
488 (ListLeading::Radio(true), _) | (_, ListTrailing::Radio(true))
489 );
490 (toggled, radio)
491 }
492}
493
494impl RenderOnce for ListItem {
495 fn render(mut self, _: &mut Window, _: &mut App) -> impl IntoElement {
496 let preview = self.preview;
497 let disabled = self.disabled || preview.is_some_and(InteractionPreview::is_disabled);
498 let lines = self.lines();
499 let (min_height, pad_y) = row_metrics(lines, self.density);
500 let mut paint = list_paint(self.selected, disabled);
501 if !self.selected {
502 paint.container = color(self.container);
503 }
504 let (toggled, radio) = self.toggle_state();
505 let selected = self.selected || radio;
506 let description = self.supporting.clone();
507 let leading = self.leading_element(paint.support, disabled);
508 let trailing = self.trailing_element(paint.support, disabled);
509 let top_aligned = lines == ListLines::Three;
510 let supporting_lines = self.supporting_lines;
511
512 let text = div()
513 .flex()
514 .flex_col()
515 .flex_1()
516 .min_w_0()
517 .when_some(self.overline.clone(), |column, overline| {
518 column.child(
519 typed(div().id("overline"), TypeRole::LabelMedium)
520 .text_color(paint.support)
521 .truncate()
522 .child(overline),
523 )
524 })
525 .child(
526 typed(div().id("headline"), TypeRole::BodyLarge)
527 .text_color(paint.headline)
528 .truncate()
529 .child(self.headline.clone()),
530 )
531 .when_some(self.supporting.clone(), |column, supporting| {
532 column.child(
533 typed(div().id("supporting"), TypeRole::BodyMedium)
534 .text_color(paint.support)
535 .whitespace_normal()
536 .line_clamp(supporting_lines)
537 .text_ellipsis()
538 .child(supporting),
539 )
540 });
541
542 let trailing_text = self.trailing_text.clone().map(|value| {
543 typed(div().flex_shrink_0(), TypeRole::LabelSmall)
544 .text_color(paint.support)
545 .child(value)
546 });
547
548 let row = div()
549 .flex()
550 .flex_1()
551 .w_full()
552 .min_w_0()
553 .gap(rem(16.))
554 .px(rem(16.))
555 .py(rem(pad_y))
556 .min_h(rem(min_height))
557 .map(|row| {
558 if top_aligned {
559 row.items_start()
560 } else {
561 row.items_center()
562 }
563 })
564 .children(leading)
565 .child(text)
566 .when(trailing_text.is_some() || trailing.is_some(), |row| {
567 row.child(
568 div()
569 .flex()
570 .flex_shrink_0()
571 .items_center()
572 .gap(rem(10.))
573 .children(trailing_text)
574 .children(trailing),
575 )
576 });
577
578 let radii = self.corners.map(segment_radii).unwrap_or_default();
579 let divider = self.divider;
580 let inset = self.divider_inset;
581 let container = paint.container;
582 let decorate = move |element: Div| {
583 element
584 .w_full()
585 .min_w_0()
586 .flex()
587 .flex_col()
588 .bg(container)
589 .rounded_tl(radii.top_left)
590 .rounded_tr(radii.top_right)
591 .rounded_br(radii.bottom_right)
592 .rounded_bl(radii.bottom_left)
593 .when(divider && !inset, |item| {
594 item.border_b_1()
595 .border_color(color(ColorRole::OUTLINE_VARIANT))
596 })
597 };
598 let inset_line = (divider && inset).then(inset_divider);
599
600 let Some(on_click) = self.on_click.take() else {
601 let layer = preview.map(|p| p.layer_opacity()).unwrap_or(0.);
604 return decorate(div())
605 .id(self.id)
606 .when(layer > 0. && !disabled, |item| {
607 item.bg(state::layer(container, paint.layer, layer))
608 })
609 .relative()
610 .child(row)
611 .children(inset_line)
612 .into_any_element();
613 };
614
615 let (hover, press) = (
616 state::hovered(container, paint.layer),
617 state::pressed(container, paint.layer),
618 );
619 let mut button = RingedButton::new(self.id, RingPlacement::Inner(radii))
620 .map(|button| {
621 button
622 .w_full()
623 .min_w_0()
624 .flex()
625 .flex_col()
626 .items_stretch()
627 .text_left()
628 .whitespace_normal()
629 .bg(container)
630 .rounded_tl(radii.top_left)
631 .rounded_tr(radii.top_right)
632 .rounded_br(radii.bottom_right)
633 .rounded_bl(radii.bottom_left)
634 .relative()
635 .when(divider && !inset, |item| {
636 item.border_b_1()
637 .border_color(color(ColorRole::OUTLINE_VARIANT))
638 })
639 })
640 .accessibility_label(self.headline)
641 .map(|button| {
644 button
645 .aria_selected(selected)
646 .when_some(toggled, |button, on| {
647 button.aria_toggled(if on {
648 gpui::accesskit::Toggled::True
649 } else {
650 gpui::accesskit::Toggled::False
651 })
652 })
653 .when_some(description, |button, text| button.aria_description(text))
654 })
655 .state_layers(hover, press)
656 .disabled(disabled)
657 .on_click(move |event, window, cx| on_click(event, window, cx))
658 .child(row)
659 .children(inset_line);
660 if let Some(state) = preview {
661 button = button.preview_state(state);
662 }
663 button.into_any_element()
664 }
665}
666
667fn inset_divider() -> Div {
670 div()
671 .absolute()
672 .left(rem(16.))
673 .right_0()
674 .bottom_0()
675 .h(px(1.))
676 .bg(color(ColorRole::OUTLINE_VARIANT))
677}
678
679pub fn list_subheader(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Stateful<Div> {
682 typed(div().id(id), TypeRole::TitleSmall)
683 .role(Role::Heading)
684 .flex()
685 .items_center()
686 .min_h(rem(48.))
687 .px(rem(16.))
688 .text_color(color(ColorRole::ON_SURFACE_VARIANT))
689 .child(text.into())
690}
691
692pub fn segmented_list(items: impl IntoIterator<Item = ListItem>) -> Div {
695 let items: Vec<ListItem> = items.into_iter().collect();
696 let count = items.len();
697 div()
698 .flex()
699 .flex_col()
700 .w_full()
701 .gap(LIST_SEGMENT_GAP)
702 .children(
703 items
704 .into_iter()
705 .enumerate()
706 .map(move |(index, item)| item.segment(list_corners(index, count))),
707 )
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::components::focus::test_rings;
714 use core::prelude::v1::test;
715 use gpui::{Context, TestAppContext, VisualTestContext};
716 use std::cell::Cell;
717
718 #[test]
719 fn lines_follow_the_overline_and_supporting_text() {
720 assert_eq!(ListLines::of(false, false, 1), ListLines::One);
721 assert_eq!(ListLines::of(false, true, 1), ListLines::Two);
722 assert_eq!(ListLines::of(true, false, 1), ListLines::Two);
723 assert_eq!(ListLines::of(false, true, 2), ListLines::Three);
724 assert_eq!(ListLines::of(true, true, 1), ListLines::Three);
725 }
726
727 #[test]
728 fn heights_match_m3_and_the_kit_density() {
729 assert_eq!(row_metrics(ListLines::One, ListDensity::Standard).0, 56.);
730 assert_eq!(row_metrics(ListLines::Two, ListDensity::Standard).0, 72.);
731 assert_eq!(row_metrics(ListLines::Three, ListDensity::Standard).0, 88.);
732 assert_eq!(row_metrics(ListLines::One, ListDensity::Compact).0, 40.);
733 assert_eq!(row_metrics(ListLines::Two, ListDensity::Compact).0, 56.);
734 assert_eq!(row_metrics(ListLines::Three, ListDensity::Compact).0, 72.);
735 }
736
737 #[test]
738 fn selected_and_disabled_rows_use_their_roles() {
739 let enabled = list_paint(false, false);
740 assert_eq!(enabled.container, color(ColorRole::SURFACE));
741 assert_eq!(enabled.headline, color(ColorRole::ON_SURFACE));
742 assert_eq!(enabled.support, color(ColorRole::ON_SURFACE_VARIANT));
743 let selected = list_paint(true, false);
744 assert_eq!(selected.container, color(ColorRole::SECONDARY_CONTAINER));
745 assert_eq!(selected.headline, color(ColorRole::ON_SECONDARY_CONTAINER));
746 let disabled = list_paint(false, true);
747 assert_eq!(
748 disabled.headline,
749 state::disabled(color(ColorRole::ON_SURFACE))
750 );
751 }
752
753 #[test]
754 fn segments_round_only_their_outer_corners() {
755 let radii = segment_radii(list_corners(0, 3));
756 assert_eq!(radii.top_left, LIST_SEGMENT_RADIUS);
757 assert_eq!(radii.bottom_left, LIST_SEGMENT_INNER_RADIUS);
758 let last = segment_radii(list_corners(2, 3));
759 assert_eq!(last.bottom_right, LIST_SEGMENT_RADIUS);
760 assert_eq!(last.top_right, LIST_SEGMENT_INNER_RADIUS);
761 }
762
763 struct Harness {
764 clicks: Rc<Cell<usize>>,
765 disabled: bool,
766 checked: bool,
767 }
768 impl Render for Harness {
769 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
770 window.set_rem_size(px(16.));
771 let clicks = self.clicks.clone();
772 div()
773 .w(px(360.))
774 .child(
775 ListItem::new("item", "List item")
776 .supporting("Supporting")
777 .leading(ListLeading::Monogram("A".into()))
778 .trailing(ListTrailing::Switch(self.checked))
779 .disabled(self.disabled)
780 .on_click(cx.listener(move |this, _, _, cx| {
781 clicks.set(clicks.get() + 1);
782 this.checked = !this.checked;
783 cx.notify();
784 })),
785 )
786 .child(
787 div()
788 .debug_selector(|| "one".into())
789 .child(ListItem::new("one", "One")),
790 )
791 .child(
792 div().debug_selector(|| "three".into()).child(
793 ListItem::new("three", "Three")
794 .supporting("Short")
795 .three_line(true),
796 ),
797 )
798 }
799 }
800
801 fn draw(cx: &mut VisualTestContext) {
802 cx.update(|window, cx| {
803 window.refresh();
804 window.draw(cx).clear(cx);
805 });
806 }
807
808 #[gpui::test]
809 fn the_row_toggles_its_switch_and_ignores_input_while_disabled(cx: &mut TestAppContext) {
810 cx.update(gpui_base::init);
811 let clicks = Rc::new(Cell::new(0));
812 let (view, cx) = cx.add_window_view({
813 let clicks = clicks.clone();
814 move |_, _| Harness {
815 clicks,
816 disabled: false,
817 checked: false,
818 }
819 });
820 draw(cx);
821 let three = cx.debug_bounds("three").unwrap();
822 let one = cx.debug_bounds("one").unwrap();
823 assert!(
826 one.size.height >= px(56.),
827 "one-line items are at least 56dp"
828 );
829 assert!(
830 three.size.height >= px(88.),
831 "three-line items are at least 88dp"
832 );
833 let row = point(px(100.), one.top() - px(20.));
834 cx.simulate_click(row, Modifiers::default());
835 draw(cx);
836 assert_eq!(clicks.get(), 1);
837 assert!(view.read_with(cx, |v, _| v.checked));
838 cx.update(|window, cx| window.focus_next(cx));
840 let keystroke =
841 cx.update(|_, cx| gpui::Keystroke::parse_with_os("a", cx.platform_info().os).unwrap());
842 cx.simulate_event(gpui::KeyDownEvent {
843 keystroke,
844 is_held: false,
845 prefer_character_input: false,
846 });
847 draw(cx);
848 assert!(test_rings::painted("item").is_some());
849 view.update(cx, |v, cx| {
850 v.disabled = true;
851 cx.notify();
852 });
853 draw(cx);
854 cx.simulate_click(row, Modifiers::default());
855 draw(cx);
856 assert_eq!(clicks.get(), 1, "a disabled row is inert");
857 }
858
859 struct Dividers;
860 impl Render for Dividers {
861 fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
862 window.set_rem_size(px(14.));
863 div()
864 .w(px(360.))
865 .child(list_subheader("subheader", "Today").debug_selector(|| "subheader".into()))
866 .child(
867 ListItem::new("inset", "Inset")
868 .divider(true)
869 .divider_inset(true),
870 )
871 }
872 }
873
874 #[gpui::test]
875 fn inset_dividers_start_16dp_in_under_a_48dp_subheader(cx: &mut TestAppContext) {
876 let (_, cx) = cx.add_window_view(|_, _| Dividers);
877 cx.run_until_parked();
878 cx.update(|window, cx| {
879 window.refresh();
880 window.draw(cx).clear(cx);
881 });
882 let subheader = cx.debug_bounds("subheader").unwrap();
883 assert_eq!(subheader.size.height, px(48.));
884 cx.update(|window, _| {
885 let scale = window.scale_factor();
886 let line = window
887 .painted_quads()
888 .into_iter()
889 .find(|quad| quad.bounds.size.height.0 == scale)
890 .expect("the inset divider paints a 1dp line");
891 assert_eq!(line.bounds.origin.x.0, 16. * scale);
892 assert_eq!(line.bounds.size.width.0, (360. - 16.) * scale);
893 assert_eq!(line.bounds.origin.y.0, (48. + 56. - 1.) * scale);
895 assert_eq!(line.border_widths.bottom.0, 0., "a fill, not the border");
896 });
897 }
898}