1use crate::color::ColorRole::{self, *};
4use crate::icons::IconName;
5use crate::theme::color;
6use crate::tokens::{elevation, layout::*, shape::*, state, typography::*};
7
8use super::InteractionPreview;
9use gpui::{prelude::*, *};
10use std::rc::Rc;
11
12use super::text::accessible_text;
13
14pub enum NoticeKind {
16 Info,
17 Danger,
18}
19
20pub fn notice_banner(
23 id: impl Into<ElementId>,
24 kind: NoticeKind,
25 text: impl Into<SharedString>,
26) -> Div {
27 let (bg, tone, icon, role) = match kind {
28 NoticeKind::Info => (
29 ColorRole::SECONDARY_CONTAINER,
30 ColorRole::PRIMARY,
31 IconName::Info,
32 Role::Status,
33 ),
34 NoticeKind::Danger => (
35 ColorRole::ERROR_CONTAINER,
36 ColorRole::ERROR,
37 IconName::Error,
38 Role::Alert,
39 ),
40 };
41 div()
42 .flex()
43 .flex_wrap()
44 .w_full()
45 .min_w_0()
46 .items_center()
47 .gap(rem(12.))
48 .p(rem(12.))
49 .rounded(RADIUS_SMALL)
50 .bg(color(bg))
51 .child(icon.icon().size(ICON_SLOT).color(color(tone)))
52 .child(
53 accessible_text(id, text)
54 .role(role)
55 .flex_1()
56 .flex_basis(rem(200.))
57 .min_w_0()
58 .whitespace_normal(),
59 )
60}
61
62pub enum StatusKind {
65 Success,
66 Warning,
67 Danger,
68 Progress,
69 Neutral,
70 Tonal,
72}
73
74#[deprecated(note = "status labels are not M3 badges: use `StatusKind` with `status_chip`")]
76pub type BadgeKind = StatusKind;
77
78pub fn status_chip(kind: StatusKind) -> Div {
82 let (text, bg, icon) = match kind {
83 StatusKind::Success => (SUCCESS, SUCCESS_BG, Some(IconName::CheckCircle)),
84 StatusKind::Warning => (WARNING, WARNING_BG, Some(IconName::Warning)),
85 StatusKind::Danger => (ERROR, ERROR_CONTAINER, Some(IconName::Error)),
86 StatusKind::Progress => (PRIMARY, SECONDARY_CONTAINER, None),
88 StatusKind::Neutral => (ON_SURFACE_VARIANT, SURFACE_CONTAINER_HIGHEST, None),
92 StatusKind::Tonal => (ON_SECONDARY_CONTAINER, SECONDARY_CONTAINER, None),
93 };
94 let mut view = div()
95 .flex()
96 .items_center()
97 .flex_shrink_0()
98 .gap(rem(4.))
99 .min_h(rem(24.))
100 .px(rem(10.))
101 .py(rem(3.))
102 .rounded(RADIUS_SMALL)
103 .bg(color(bg))
104 .text_color(color(text))
105 .text_size(TEXT_AUX)
106 .font_weight(FontWeight::MEDIUM)
107 .whitespace_nowrap();
108 if let Some(icon) = icon {
109 view = view.child(icon.icon().size(STATUS_ICON));
110 }
111 view
112}
113
114const STATUS_ICON: Rems = rem(16.);
116
117#[deprecated(
119 note = "a status chip, not an M3 badge: use `status_chip` (M3 badges are `small_badge` and `large_badge`)"
120)]
121pub fn badge(kind: StatusKind) -> Div {
122 status_chip(kind)
123}
124
125pub fn outlined_status_chip(
128 id: impl Into<ElementId>,
129 label: impl Into<SharedString>,
130) -> Stateful<Div> {
131 accessible_text(id, label)
132 .flex_shrink_0()
133 .px(rem(10.))
134 .py(rem(4.))
135 .rounded(RADIUS_SMALL)
136 .border_1()
137 .border_color(color(OUTLINE_VARIANT))
138 .text_size(TEXT_AUX)
139 .line_height(rem(16.))
140 .font_weight(FontWeight::MEDIUM)
141 .text_color(color(ON_SURFACE_VARIANT))
142 .whitespace_nowrap()
143}
144
145#[deprecated(note = "a status chip, not an M3 badge: use `outlined_status_chip`")]
148pub fn outlined_badge(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Stateful<Div> {
149 outlined_status_chip(id, label)
150}
151
152pub const SMALL_BADGE_SIZE: Pixels = px(6.);
155pub const LARGE_BADGE_SIZE: Rems = rem(16.);
157pub const LARGE_BADGE_PADDING: Rems = rem(4.);
159pub const BADGE_CONTAINER: ColorRole = ERROR;
161pub const BADGE_LABEL: ColorRole = ON_ERROR;
162
163pub fn small_badge() -> Div {
166 div()
167 .flex_shrink_0()
168 .size(SMALL_BADGE_SIZE)
169 .rounded(RADIUS_FULL)
170 .bg(color(BADGE_CONTAINER))
171}
172
173pub fn large_badge(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Stateful<Div> {
178 let scale = TypeRole::LabelSmall.scale();
179 accessible_text(id, label)
180 .flex()
181 .flex_shrink_0()
182 .items_center()
183 .justify_center()
184 .h(LARGE_BADGE_SIZE)
185 .min_w(LARGE_BADGE_SIZE)
186 .px(LARGE_BADGE_PADDING)
187 .rounded(RADIUS_FULL)
188 .bg(color(BADGE_CONTAINER))
189 .text_color(color(BADGE_LABEL))
190 .text_size(scale.font_size)
191 .line_height(scale.line_height)
192 .font_weight(FontWeight(scale.font_weight as f32))
193 .whitespace_nowrap()
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum CardVariant {
199 Elevated,
201 Filled,
203 Outlined,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209struct CardStyle {
210 container: ColorRole,
211 elevation: u8,
212 outline: Option<ColorRole>,
213}
214
215fn card_style(variant: CardVariant) -> CardStyle {
216 match variant {
217 CardVariant::Elevated => CardStyle {
218 container: SURFACE_CONTAINER_LOW,
219 elevation: 1,
220 outline: None,
221 },
222 CardVariant::Filled => CardStyle {
223 container: SURFACE_CONTAINER_HIGHEST,
224 elevation: 0,
225 outline: None,
226 },
227 CardVariant::Outlined => CardStyle {
228 container: SURFACE,
229 elevation: 0,
230 outline: Some(OUTLINE_VARIANT),
231 },
232 }
233}
234
235pub const CARD_PADDING: Rems = rem(16.);
238
239pub struct CardSurface {
250 inner: Div,
251 outline: Option<ColorRole>,
252 variant: CardVariant,
253 focus_ring: bool,
255}
256impl CardSurface {
257 pub fn new(variant: CardVariant) -> Self {
258 let style = card_style(variant);
259 Self {
260 inner: gpui_base::v_flex()
261 .bg(color(style.container))
262 .text_color(color(ON_SURFACE))
263 .overflow_hidden()
264 .rounded(RADIUS_CARD)
265 .shadow(elevation::level(style.elevation)),
266 outline: style.outline,
267 variant,
268 focus_ring: false,
269 }
270 }
271
272 pub fn state(mut self, state: CardState) -> Self {
275 let container = color(card_style(self.variant).container);
276 self.inner = self
277 .inner
278 .bg(state::layer(
279 container,
280 color(ON_SURFACE),
281 state.layer_opacity(),
282 ))
283 .shadow(elevation::level(card_elevation(self.variant, state)));
284 self.focus_ring = state == CardState::Focused;
285 self
286 }
287
288 pub fn preview_state(self, preview: InteractionPreview) -> Self {
290 self.state(preview.into())
291 }
292
293 pub fn interactive(mut self) -> Self {
297 let container = color(card_style(self.variant).container);
298 let hover = state::hovered(container, color(ON_SURFACE));
299 let shadow = elevation::level(card_elevation(self.variant, CardState::Hovered));
300 self.inner = self
301 .inner
302 .hover(move |style| style.bg(hover).shadow(shadow.clone()));
303 self
304 }
305}
306
307#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
310pub enum CardState {
311 #[default]
312 Enabled,
313 Hovered,
314 Focused,
315 Pressed,
316 Dragged,
317}
318
319impl CardState {
320 pub const ALL: [Self; 5] = [
322 Self::Enabled,
323 Self::Hovered,
324 Self::Focused,
325 Self::Pressed,
326 Self::Dragged,
327 ];
328
329 pub const fn name(self) -> &'static str {
330 match self {
331 Self::Enabled => "Enabled",
332 Self::Hovered => "Hovered",
333 Self::Focused => "Focused",
334 Self::Pressed => "Pressed",
335 Self::Dragged => "Dragged",
336 }
337 }
338
339 pub const fn layer_opacity(self) -> f32 {
341 match self {
342 Self::Enabled => 0.,
343 Self::Hovered => state::HOVER,
344 Self::Focused => state::FOCUS,
345 Self::Pressed => state::PRESS,
346 Self::Dragged => state::DRAG,
347 }
348 }
349}
350
351impl From<InteractionPreview> for CardState {
352 fn from(preview: InteractionPreview) -> Self {
353 match preview {
354 InteractionPreview::Hovered => Self::Hovered,
355 InteractionPreview::Focused => Self::Focused,
356 InteractionPreview::Pressed => Self::Pressed,
357 InteractionPreview::Enabled | InteractionPreview::Disabled => Self::Enabled,
358 }
359 }
360}
361
362fn card_elevation(variant: CardVariant, state: CardState) -> u8 {
366 use CardState::*;
367 match (variant, state) {
368 (CardVariant::Elevated, Hovered) => 2,
369 (CardVariant::Elevated, Dragged) => 4,
370 (CardVariant::Elevated, _) => 1,
371 (_, Dragged) => 3,
372 (_, Hovered) => 1,
373 (CardVariant::Filled, Focused | Pressed) => 1,
374 _ => 0,
375 }
376}
377impl Styled for CardSurface {
378 fn style(&mut self) -> &mut StyleRefinement {
379 self.inner.style()
380 }
381}
382impl ParentElement for CardSurface {
383 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
384 self.inner.extend(elements);
385 }
386}
387impl InteractiveElement for CardSurface {
388 fn interactivity(&mut self) -> &mut Interactivity {
389 self.inner.interactivity()
390 }
391}
392impl IntoElement for CardSurface {
393 type Element = Self;
394 fn into_element(self) -> Self {
395 self
396 }
397}
398impl Element for CardSurface {
399 type RequestLayoutState = <Div as Element>::RequestLayoutState;
400 type PrepaintState = <Div as Element>::PrepaintState;
401 fn id(&self) -> Option<ElementId> {
402 Element::id(&self.inner)
403 }
404 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
405 Element::source_location(&self.inner)
406 }
407 fn a11y_role(&self) -> Option<accesskit::Role> {
408 self.inner.a11y_role()
409 }
410 fn write_a11y_info(&self, node: &mut accesskit::Node) {
411 self.inner.write_a11y_info(node);
412 }
413 fn a11y_synthetic_children(
414 &mut self,
415 prepaint: &mut Self::PrepaintState,
416 builder: &mut A11ySubtreeBuilder,
417 ) {
418 self.inner.a11y_synthetic_children(prepaint, builder);
419 }
420 fn request_layout(
421 &mut self,
422 id: Option<&GlobalElementId>,
423 inspector: Option<&InspectorElementId>,
424 window: &mut Window,
425 cx: &mut App,
426 ) -> (LayoutId, Self::RequestLayoutState) {
427 self.inner.request_layout(id, inspector, window, cx)
428 }
429 fn prepaint(
430 &mut self,
431 id: Option<&GlobalElementId>,
432 inspector: Option<&InspectorElementId>,
433 bounds: Bounds<Pixels>,
434 state: &mut Self::RequestLayoutState,
435 window: &mut Window,
436 cx: &mut App,
437 ) -> Self::PrepaintState {
438 self.inner
439 .prepaint(id, inspector, bounds, state, window, cx)
440 }
441 fn paint(
442 &mut self,
443 id: Option<&GlobalElementId>,
444 inspector: Option<&InspectorElementId>,
445 bounds: Bounds<Pixels>,
446 state: &mut Self::RequestLayoutState,
447 prepaint: &mut Self::PrepaintState,
448 window: &mut Window,
449 cx: &mut App,
450 ) {
451 let style = self
452 .inner
453 .interactivity()
454 .compute_style(id, prepaint.as_ref(), window, cx);
455 self.inner
456 .paint(id, inspector, bounds, state, prepaint, window, cx);
457 if let Some(role) = self.outline
458 && style.visibility != Visibility::Hidden
459 && style.display != Display::None
460 {
461 let radii = style
462 .corner_radii
463 .to_pixels(window.rem_size())
464 .clamp_radii_for_quad_size(bounds.size);
465 let mut outline: Hsla = color(role).into();
468 outline.a *= style.opacity.unwrap_or(1.);
469 window.paint_quad(quad(
470 bounds,
471 radii,
472 transparent_black(),
473 px(1.),
474 outline,
475 BorderStyle::default(),
476 ));
477 }
478 if self.focus_ring {
479 let radius = px(f32::from(RADIUS_CARD) + 5.);
481 window.paint_quad(quad(
482 bounds.dilate(px(5.)),
483 radius,
484 transparent_black(),
485 px(3.),
486 color(SECONDARY),
487 BorderStyle::default(),
488 ));
489 }
490 }
491}
492
493#[derive(Default)]
496struct CardDrag {
497 grab: Option<(Point<Pixels>, Point<Pixels>)>,
499 offset: Point<Pixels>,
501 moved: bool,
504 releases: u64,
506 released_at: Point<Pixels>,
508}
509
510type CardClick = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
512
513const CARD_DRAG_SLOP: Pixels = px(4.);
515
516#[derive(gpui::IntoElement)]
524pub struct MaterialCard {
525 id: ElementId,
526 variant: CardVariant,
527 label: Option<SharedString>,
528 on_click: Option<CardClick>,
529 draggable: bool,
530 preview: Option<CardState>,
531 children: Vec<AnyElement>,
532 style: StyleRefinement,
533}
534
535impl MaterialCard {
536 pub fn new(id: impl Into<ElementId>, variant: CardVariant) -> Self {
537 Self {
538 id: id.into(),
539 variant,
540 label: None,
541 on_click: None,
542 draggable: false,
543 preview: None,
544 children: Vec::new(),
545 style: StyleRefinement::default(),
546 }
547 }
548
549 pub fn on_click(
551 mut self,
552 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
553 ) -> Self {
554 self.on_click = Some(Rc::new(handler));
555 self
556 }
557
558 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
560 self.label = Some(label.into());
561 self
562 }
563
564 pub fn draggable(mut self, draggable: bool) -> Self {
566 self.draggable = draggable;
567 self
568 }
569
570 pub fn state(mut self, state: CardState) -> Self {
572 self.preview = Some(state);
573 self
574 }
575
576 pub fn is_clickable(&self) -> bool {
578 self.on_click.is_some()
579 }
580}
581
582impl Styled for MaterialCard {
583 fn style(&mut self) -> &mut StyleRefinement {
584 &mut self.style
585 }
586}
587
588impl ParentElement for MaterialCard {
589 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
590 self.children.extend(elements);
591 }
592}
593
594impl RenderOnce for MaterialCard {
595 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
596 use super::button::{ButtonFocusRing, ButtonRipple};
597 let id = self.id;
598 let drag =
599 window.use_keyed_state((id.clone(), "card-drag"), cx, |_, _| CardDrag::default());
600 let (grabbed, offset, releases, released_at) = {
601 let drag = drag.read(cx);
602 (
603 drag.grab.is_some(),
604 drag.offset,
605 drag.releases,
606 drag.released_at,
607 )
608 };
609 let dragging = grabbed && drag.read(cx).moved;
610 let clickable = self.on_click.is_some() && self.preview.is_none();
611 let pressed = clickable && grabbed && !dragging;
612 let container = color(card_style(self.variant).container);
613 let group: SharedString = format!("material-card-{id:?}").into();
614
615 let mut surface = CardSurface::new(self.variant)
616 .w_full()
617 .children(self.children);
618 surface = match (self.preview, dragging) {
619 (_, true) => surface.state(CardState::Dragged),
620 (Some(state), _) => surface.state(state),
621 (None, false) if pressed => surface.state(CardState::Pressed),
622 (None, false) if clickable => {
623 let hover = state::hovered(container, color(ON_SURFACE));
624 let hovered = elevation::level(card_elevation(self.variant, CardState::Hovered));
625 surface.group_hover(group.clone(), move |style| {
626 style.bg(hover).shadow(hovered.clone())
627 })
628 }
629 _ => surface,
630 };
631
632 let body: AnyElement = if let Some(on_click) = self.on_click.clone().filter(|_| clickable) {
633 let focus = window
634 .use_keyed_state((id.clone(), "card-focus"), cx, |_, cx| cx.focus_handle())
635 .read(cx)
636 .clone();
637 let click_drag = drag.clone();
638 let button = gpui_base::Button::new((id.clone(), "card-button"))
639 .group(group)
640 .track_focus(&focus)
641 .w_full()
642 .rounded(RADIUS_CARD)
643 .cursor_pointer()
644 .when_some(self.label.clone(), |button, label| {
645 button.accessibility_label(label)
646 })
647 .child(surface)
648 .on_click(move |event, window, cx| {
649 if !click_drag.read(cx).moved {
651 on_click(event, window, cx);
652 }
653 });
654 let ripple = ButtonRipple::new(
655 button,
656 (id.clone(), "card-ripple").into(),
657 true,
658 true,
659 super::navigation::ripple_corners(RADIUS_CARD),
660 Edges::all(px(0.).into()),
661 color(ON_SURFACE),
662 window,
663 cx,
664 );
665 ButtonFocusRing::with_corners(ripple, focus, true, Corners::all(RADIUS_CARD))
666 .into_any_element()
667 } else {
668 surface.into_any_element()
669 };
670
671 let draggable = self.draggable && self.preview.is_none();
672 let mut root = div()
673 .id(id.clone())
674 .relative()
675 .flex()
676 .flex_col()
677 .child(body);
678 root.style().refine(&self.style);
679 if draggable {
680 root = root.cursor(if dragging {
681 CursorStyle::ClosedHand
682 } else {
683 CursorStyle::OpenHand
684 });
685 }
686 if draggable || clickable {
687 let grab = drag.clone();
689 root = root.on_mouse_down(MouseButton::Left, move |event, window, cx| {
690 grab.update(cx, |drag, _| {
691 drag.grab = Some((event.position, drag.offset));
692 drag.moved = false;
693 });
694 window.refresh();
695 });
696 }
697 if grabbed {
698 let follow = drag.clone();
700 root = root.child(
701 canvas(
702 |_, _, _| (),
703 move |_, _, window, _| {
704 let moving = follow.clone();
705 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
706 if phase != DispatchPhase::Capture {
707 return;
708 }
709 moving.update(cx, |drag, _| {
710 let Some((start, from)) = drag.grab else {
711 return;
712 };
713 let delta = event.position - start;
714 if !draggable {
715 return;
716 }
717 if drag.moved
718 || delta.x.abs() > CARD_DRAG_SLOP
719 || delta.y.abs() > CARD_DRAG_SLOP
720 {
721 drag.moved = true;
722 drag.offset = from + delta;
723 }
724 });
725 window.refresh();
726 });
727 let dropping = follow.clone();
728 window.on_mouse_event(move |_: &MouseUpEvent, phase, window, cx| {
729 if phase != DispatchPhase::Capture {
730 return;
731 }
732 dropping.update(cx, |drag, _| {
733 drag.grab = None;
734 if drag.moved {
735 drag.releases += 1;
736 drag.released_at = drag.offset;
737 }
738 drag.offset = Point::default();
739 });
740 let settled = dropping.clone();
743 window.defer(cx, move |_, cx| {
744 settled.update(cx, |drag, _| drag.moved = false);
745 });
746 window.refresh();
747 });
748 },
749 )
750 .absolute()
751 .size_0(),
752 );
753 }
754 let lifted = dragging || grabbed;
755 if lifted && offset != Point::default() {
756 return root.left(offset.x).top(offset.y).into_any_element();
757 }
758 if releases > 0 && released_at != Point::default() && !cx.reduce_motion() {
759 let curve = crate::motion::curve(crate::motion::MotionToken::DefaultSpatial);
760 let ease = curve.ease();
761 return root
762 .with_animation(
763 ElementId::NamedChild(
764 std::sync::Arc::new(id),
765 format!("card-return-{releases}").into(),
766 ),
767 Animation::new(curve.duration()),
768 move |root, t| {
769 let remaining = 1. - ease(t);
770 root.left(released_at.x * remaining)
771 .top(released_at.y * remaining)
772 },
773 )
774 .into_any_element();
775 }
776 root.into_any_element()
777 }
778}
779
780pub fn card(variant: CardVariant) -> CardSurface {
785 CardSurface::new(variant).w_full().p(CARD_PADDING)
786}
787
788pub fn card_surface() -> CardSurface {
790 card(CardVariant::Elevated)
791}
792
793#[cfg(test)]
794#[path = "card_tests.rs"]
795mod card_tests;
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use core::prelude::v1::test;
803 use gpui::{Context, TestAppContext};
804
805 #[test]
806 fn card_variants_use_m3_container_roles_elevation_and_outline() {
807 assert_eq!(
808 card_style(CardVariant::Elevated),
809 CardStyle {
810 container: SURFACE_CONTAINER_LOW,
811 elevation: 1,
812 outline: None,
813 }
814 );
815 assert_eq!(
816 card_style(CardVariant::Filled),
817 CardStyle {
818 container: SURFACE_CONTAINER_HIGHEST,
819 elevation: 0,
820 outline: None,
821 }
822 );
823 assert_eq!(
824 card_style(CardVariant::Outlined),
825 CardStyle {
826 container: SURFACE,
827 elevation: 0,
828 outline: Some(OUTLINE_VARIANT),
829 }
830 );
831 assert_eq!(RADIUS_CARD, px(12.));
832 assert_eq!(CARD_PADDING, rem(16.));
833 }
834
835 #[test]
836 fn card_states_follow_the_kit_layers_and_elevation() {
837 use CardState::*;
838 let levels = |variant| CardState::ALL.map(|s| card_elevation(variant, s));
839 assert_eq!(levels(CardVariant::Elevated), [1, 2, 1, 1, 4]);
840 assert_eq!(levels(CardVariant::Filled), [0, 1, 1, 1, 3]);
841 assert_eq!(levels(CardVariant::Outlined), [0, 1, 0, 0, 3]);
842 assert_eq!(
843 CardState::ALL.map(CardState::layer_opacity),
844 [0., state::HOVER, state::FOCUS, state::PRESS, state::DRAG]
845 );
846 assert_eq!(CardState::from(InteractionPreview::Disabled), Enabled);
847 assert_eq!(CardState::from(InteractionPreview::Focused), Focused);
848 }
849
850 #[test]
851 fn badges_follow_m3_geometry_and_roles() {
852 assert_eq!(SMALL_BADGE_SIZE, px(6.));
853 assert_eq!(LARGE_BADGE_SIZE, rem(16.));
854 assert_eq!(LARGE_BADGE_PADDING, rem(4.));
855 assert_eq!((BADGE_CONTAINER, BADGE_LABEL), (ERROR, ON_ERROR));
856 }
857
858 #[gpui::test]
859 fn large_badge_hugs_short_labels_and_grows_for_long_ones(cx: &mut TestAppContext) {
860 struct Badges;
861 impl Render for Badges {
862 fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
863 window.set_rem_size(px(14.));
864 div()
865 .flex()
866 .items_start()
867 .child(large_badge("one", "3").debug_selector(|| "badge-short".into()))
868 .child(large_badge("many", "999+").debug_selector(|| "badge-long".into()))
869 .child(small_badge().debug_selector(|| "badge-dot".into()))
870 }
871 }
872 let (_, cx) = cx.add_window_view(|_, _| Badges);
873 cx.run_until_parked();
874 let short = cx.debug_bounds("badge-short").unwrap();
875 let long = cx.debug_bounds("badge-long").unwrap();
876 let dot = cx.debug_bounds("badge-dot").unwrap();
877 assert_eq!(short.size.height, px(16.));
878 assert_eq!(short.size.width, px(16.));
879 assert_eq!(long.size.height, px(16.));
880 assert!(long.size.width > px(16.));
881 assert_eq!(dot.size, gpui::size(px(6.), px(6.)));
882 }
883
884 pub(crate) fn redraw(cx: &mut gpui::VisualTestContext) {
885 cx.update(|window, cx| {
886 window.refresh();
887 window.draw(cx).clear(cx);
888 });
889 }
890}