1use super::InteractionPreview;
11use super::focus::{RingPlacement, RingedButton};
12use crate::color::ColorRole::*;
13use crate::icons::{Icon, IconName};
14use crate::motion;
15use crate::theme::color;
16use crate::tokens::{elevation, shape::RADIUS_SMALL, state, typography::*};
17use gpui::{prelude::*, *};
18use std::rc::Rc;
19
20pub const CHIP_HEIGHT: Rems = rem(32.);
23const CHIP_ICON: f32 = 18.;
24const CHIP_GAP: f32 = 8.;
25const AVATAR: f32 = 24.;
26const ASSIST_TEXT_EDGE: f32 = 16.;
28const INPUT_TEXT_EDGE: f32 = 12.;
29const ICON_EDGE: f32 = 8.;
30const AVATAR_EDGE: f32 = 4.;
31const ELEVATED_LEVEL: u8 = 1;
33const HOVERED_LEVEL: u8 = 2;
34const DRAGGED_LEVEL: u8 = 4;
35
36pub fn chip(leading_icon: bool) -> Div {
40 gpui_base::h_flex()
41 .flex_shrink_0()
42 .h(CHIP_HEIGHT)
43 .pl(rem(if leading_icon {
44 ICON_EDGE
45 } else {
46 ASSIST_TEXT_EDGE
47 }))
48 .pr(rem(ASSIST_TEXT_EDGE))
49 .gap(rem(CHIP_GAP))
50 .items_center()
51 .rounded(RADIUS_SMALL)
52 .border_1()
53 .border_color(color(OUTLINE_VARIANT))
54 .whitespace_nowrap()
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum ChipStyle {
60 #[default]
62 Flat,
63 Elevated,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68enum ChipKind {
69 Assist,
70 Filter,
71 Input,
72 Suggestion,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
77struct ChipPaint {
78 container: Rgba,
79 outline: Rgba,
80 label: Rgba,
81 leading: Rgba,
82 trailing: Rgba,
83 layer: Rgba,
85 elevation: u8,
86}
87
88fn clear() -> Rgba {
89 Rgba {
90 r: 0.,
91 g: 0.,
92 b: 0.,
93 a: 0.,
94 }
95}
96
97fn chip_paint(kind: ChipKind, style: ChipStyle, selected: bool, disabled: bool) -> ChipPaint {
102 let selected = selected && kind != ChipKind::Assist;
103 let elevated = style == ChipStyle::Elevated;
104 let on_surface = color(ON_SURFACE);
105 if disabled {
106 let content = state::disabled(on_surface);
107 let faint = on_surface.opacity(state::DISABLED_CONTAINER);
108 return ChipPaint {
109 container: if selected || elevated { faint } else { clear() },
110 outline: if selected || elevated { clear() } else { faint },
111 label: content,
112 leading: content,
113 trailing: content,
114 layer: on_surface,
115 elevation: 0,
116 };
117 }
118 let elevation = if elevated { ELEVATED_LEVEL } else { 0 };
119 if selected {
120 let ink = color(ON_SECONDARY_CONTAINER);
121 return ChipPaint {
122 container: color(SECONDARY_CONTAINER),
123 outline: clear(),
124 label: ink,
125 leading: if kind == ChipKind::Input {
126 color(PRIMARY)
127 } else {
128 ink
129 },
130 trailing: ink,
131 layer: ink,
132 elevation,
133 };
134 }
135 let (label, leading) = match kind {
136 ChipKind::Assist => (ON_SURFACE, PRIMARY),
137 ChipKind::Filter | ChipKind::Suggestion => (ON_SURFACE_VARIANT, PRIMARY),
138 ChipKind::Input => (ON_SURFACE_VARIANT, ON_SURFACE_VARIANT),
139 };
140 let label = color(label);
141 ChipPaint {
142 container: if elevated {
143 color(SURFACE_CONTAINER_LOW)
144 } else {
145 clear()
146 },
147 outline: if elevated {
148 clear()
149 } else {
150 color(OUTLINE_VARIANT)
151 },
152 label,
153 leading: color(leading),
154 trailing: color(ON_SURFACE_VARIANT),
155 layer: label,
156 elevation,
157 }
158}
159
160type ClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
161type RemoveHandler = Rc<dyn Fn(&mut Window, &mut App)>;
162type ToggleHandler = Rc<dyn Fn(&bool, &mut Window, &mut App)>;
163
164struct ChipBase {
166 id: ElementId,
167 kind: ChipKind,
168 label: SharedString,
169 style: ChipStyle,
170 selected: bool,
171 disabled: bool,
172 dragged: bool,
173 preview: Option<InteractionPreview>,
174 leading: Option<Icon>,
175 avatar: Option<AnyElement>,
176 trailing: Option<Icon>,
177 remove: Option<(SharedString, RemoveHandler)>,
178 on_click: Option<ClickHandler>,
179 on_toggle: Option<ToggleHandler>,
182}
183
184impl ChipBase {
185 fn new(id: ElementId, kind: ChipKind, label: SharedString) -> Self {
186 Self {
187 id,
188 kind,
189 label,
190 style: ChipStyle::Flat,
191 selected: false,
192 disabled: false,
193 dragged: false,
194 preview: None,
195 leading: None,
196 avatar: None,
197 trailing: None,
198 remove: None,
199 on_click: None,
200 on_toggle: None,
201 }
202 }
203
204 fn render(self, window: &mut Window, cx: &mut App) -> RingedButton {
205 let focus = window
206 .use_keyed_state((self.id.clone(), "chip-focus"), cx, |_, cx| {
207 cx.focus_handle()
208 })
209 .read(cx)
210 .clone();
211 let preview = self.preview;
212 let disabled = self.disabled || preview.is_some_and(|p| p.is_disabled());
213 let enabled = !disabled;
214 let live = enabled && preview.is_none();
215 let forced = |state| enabled && preview == Some(state);
216 let keyboard_focus = forced(InteractionPreview::Focused)
217 || (live && focus.is_focused(window) && window.last_input_was_keyboard());
218 let selected = self.selected && self.kind != ChipKind::Assist;
219 let paint = chip_paint(self.kind, self.style, selected, disabled);
220 let dragged = self.dragged && enabled;
221 let slide = self.kind == ChipKind::Filter && self.leading.is_none();
223 let check = if slide {
224 let target = if selected { 1. } else { 0. };
225 if preview.is_some() {
226 target
227 } else {
228 motion::selection_value((self.id.clone(), "check"), target, window, cx)
229 }
230 } else {
231 0.
232 };
233 let check = if check.is_finite() { check.max(0.) } else { 0. };
234 let base = if dragged {
235 let lifted = if paint.container.a > 0. {
238 paint.container
239 } else {
240 color(SURFACE_CONTAINER_LOW)
241 };
242 state::layer(lifted, paint.layer, state::DRAG)
243 } else if keyboard_focus {
244 state::layer(paint.container, paint.layer, state::FOCUS)
245 } else {
246 paint.container
247 };
248 let (hover, press) = (
249 state::hovered(paint.container, paint.layer),
250 state::pressed(paint.container, paint.layer),
251 );
252 let elevation = if dragged {
253 DRAGGED_LEVEL
254 } else if paint.elevation > 0 && forced(InteractionPreview::Hovered) {
255 HOVERED_LEVEL
256 } else {
257 paint.elevation
258 };
259 let outline = if keyboard_focus && paint.outline.a > 0. {
261 color(ON_SURFACE_VARIANT)
262 } else {
263 paint.outline
264 };
265 let left = if self.avatar.is_some() {
266 AVATAR_EDGE
267 } else if self.leading.is_some() {
268 ICON_EDGE
269 } else {
270 let text = self.text_edge();
271 if slide {
272 text + (ICON_EDGE - text) * check.min(1.)
274 } else {
275 text
276 }
277 };
278 let has_trailing = self.trailing.is_some() || self.remove.is_some();
279 let right = if has_trailing {
280 ICON_EDGE
281 } else {
282 self.text_edge()
283 };
284 let scale = TypeRole::LabelLarge.scale();
285 let icon_size = rem(CHIP_ICON);
286 let leading_icon = self.leading.map(|icon| {
287 let icon = if selected && self.kind == ChipKind::Filter {
289 IconName::Check.icon()
290 } else {
291 icon
292 };
293 icon.size(icon_size).color(paint.leading)
294 });
295 let check_slot = slide.then(|| {
296 div()
297 .flex_shrink_0()
298 .overflow_hidden()
299 .w(rem((CHIP_ICON + CHIP_GAP) * check))
300 .opacity(check.min(1.))
301 .when(cfg!(test), |slot| {
302 slot.debug_selector(|| "chip-check".into())
303 })
304 .child(
305 div()
306 .w(rem(CHIP_ICON + CHIP_GAP))
307 .child(IconName::Check.icon().size(icon_size).color(paint.leading)),
308 )
309 });
310 let avatar = self.avatar.map(|avatar| {
311 div()
312 .flex()
313 .flex_shrink_0()
314 .items_center()
315 .justify_center()
316 .size(rem(AVATAR))
317 .mr(rem(CHIP_GAP))
318 .rounded_full()
319 .overflow_hidden()
320 .child(avatar)
321 });
322 let trailing_icon = self.trailing.map(|icon| {
323 div()
324 .flex_shrink_0()
325 .ml(rem(CHIP_GAP))
326 .child(icon.size(icon_size).color(paint.trailing))
327 });
328 let key_remove = self.remove.as_ref().map(|(_, handler)| handler.clone());
329 let remove = self.remove.map(|(label, handler)| {
330 let on_press = handler;
331 div()
332 .id("chip-remove")
333 .flex_shrink_0()
334 .ml(rem(CHIP_GAP))
335 .role(Role::Button)
336 .aria_label(label)
337 .when(cfg!(test), |remove| {
338 remove.debug_selector(|| "chip-remove".into())
339 })
340 .child(IconName::Close.icon().size(icon_size).color(paint.trailing))
341 .when(live, |remove| {
342 remove
343 .cursor_pointer()
344 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
347 .on_click(move |_, window, cx| {
350 cx.stop_propagation();
351 on_press(window, cx);
352 })
353 })
354 .into_any_element()
355 });
356 let label = self.label;
357 let next = !self.selected;
358 let on_click = self.on_toggle.map(|toggle| -> ClickHandler {
359 Rc::new(move |_, window, cx| toggle(&next, window, cx))
360 });
361 let on_click = on_click.or(self.on_click);
362 let id = self.id;
363 RingedButton::new(id, RingPlacement::Outer(Corners::all(RADIUS_SMALL)))
364 .track_focus(&focus)
365 .map(move |button| {
366 button
367 .flex_shrink_0()
368 .h(CHIP_HEIGHT)
369 .pl(rem(left))
370 .pr(rem(right))
371 .rounded(RADIUS_SMALL)
372 .border_1()
373 .border_color(outline)
374 .bg(base)
375 .shadow(elevation::level(elevation))
376 .text_color(paint.label)
377 .text_size(scale.font_size)
378 .line_height(scale.line_height)
379 .font_weight(FontWeight(scale.font_weight as f32))
380 .whitespace_nowrap()
381 .selected(selected)
382 .accessibility_label(label.clone())
383 .when(live, |button| button.cursor_pointer())
384 .children(check_slot)
385 .children(avatar)
386 .when_some(leading_icon, |button, icon| {
387 button.child(div().flex_shrink_0().mr(rem(CHIP_GAP)).child(icon))
388 })
389 .child(label)
390 .children(trailing_icon)
391 .children(remove)
392 .when_some(on_click, |button, handler| {
393 button.on_click(move |event, window, cx| handler(event, window, cx))
394 })
395 })
396 .when_some(key_remove.filter(|_| live), |button, remove| {
397 button.on_key_down(move |event, window, cx| {
399 if matches!(event.keystroke.key.as_str(), "backspace" | "delete") {
400 cx.stop_propagation();
401 remove(window, cx);
402 }
403 })
404 })
405 .state_layers(hover, press)
406 .disabled(disabled)
407 .when_some(preview, |button, preview| button.preview_state(preview))
408 }
409
410 fn text_edge(&self) -> f32 {
411 if self.kind == ChipKind::Input {
412 INPUT_TEXT_EDGE
413 } else {
414 ASSIST_TEXT_EDGE
415 }
416 }
417}
418
419macro_rules! chip_common {
421 ($chip:ident) => {
422 impl $chip {
423 pub fn preview_state(mut self, state: InteractionPreview) -> Self {
427 self.0.preview = Some(state);
428 self
429 }
430 pub fn dragged(mut self, dragged: bool) -> Self {
433 self.0.dragged = dragged;
434 self
435 }
436 pub fn disabled(mut self, disabled: bool) -> Self {
437 self.0.disabled = disabled;
438 self
439 }
440 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
442 self.0.leading = Some(icon.into());
443 self
444 }
445 }
446 impl RenderOnce for $chip {
447 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
448 self.0.render(window, cx)
449 }
450 }
451 };
452}
453
454#[derive(gpui::IntoElement)]
457pub struct AssistChip(ChipBase);
458chip_common!(AssistChip);
459
460impl AssistChip {
461 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
462 Self(ChipBase::new(id.into(), ChipKind::Assist, label.into()))
463 }
464 pub fn elevated(mut self, elevated: bool) -> Self {
465 self.0.style = if elevated {
466 ChipStyle::Elevated
467 } else {
468 ChipStyle::Flat
469 };
470 self
471 }
472 pub fn on_click(
473 mut self,
474 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
475 ) -> Self {
476 self.0.on_click = Some(Rc::new(handler));
477 self
478 }
479}
480
481#[derive(gpui::IntoElement)]
484pub struct FilterChip(ChipBase);
485chip_common!(FilterChip);
486
487impl FilterChip {
488 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
489 Self(ChipBase::new(id.into(), ChipKind::Filter, label.into()))
490 }
491 pub fn selected(mut self, selected: bool) -> Self {
492 self.0.selected = selected;
493 self
494 }
495 pub fn elevated(mut self, elevated: bool) -> Self {
496 self.0.style = if elevated {
497 ChipStyle::Elevated
498 } else {
499 ChipStyle::Flat
500 };
501 self
502 }
503 pub fn trailing_icon(mut self, icon: impl Into<Icon>) -> Self {
505 self.0.trailing = Some(icon.into());
506 self
507 }
508 pub fn on_toggle(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
510 self.0.on_toggle = Some(Rc::new(handler));
511 self
512 }
513}
514
515#[derive(gpui::IntoElement)]
518pub struct InputChip(ChipBase);
519chip_common!(InputChip);
520
521impl InputChip {
522 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
523 Self(ChipBase::new(id.into(), ChipKind::Input, label.into()))
524 }
525 pub fn selected(mut self, selected: bool) -> Self {
526 self.0.selected = selected;
527 self
528 }
529 pub fn avatar(mut self, avatar: impl IntoElement) -> Self {
531 self.0.avatar = Some(avatar.into_any_element());
532 self
533 }
534 pub fn on_remove(
537 mut self,
538 label: impl Into<SharedString>,
539 handler: impl Fn(&mut Window, &mut App) + 'static,
540 ) -> Self {
541 self.0.remove = Some((label.into(), Rc::new(handler)));
542 self
543 }
544 pub fn on_click(
545 mut self,
546 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
547 ) -> Self {
548 self.0.on_click = Some(Rc::new(handler));
549 self
550 }
551}
552
553#[derive(gpui::IntoElement)]
555pub struct SuggestionChip(ChipBase);
556chip_common!(SuggestionChip);
557
558impl SuggestionChip {
559 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
560 Self(ChipBase::new(id.into(), ChipKind::Suggestion, label.into()))
561 }
562 pub fn selected(mut self, selected: bool) -> Self {
563 self.0.selected = selected;
564 self
565 }
566 pub fn elevated(mut self, elevated: bool) -> Self {
567 self.0.style = if elevated {
568 ChipStyle::Elevated
569 } else {
570 ChipStyle::Flat
571 };
572 self
573 }
574 pub fn on_click(
575 mut self,
576 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
577 ) -> Self {
578 self.0.on_click = Some(Rc::new(handler));
579 self
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use core::prelude::v1::test;
587
588 #[test]
589 fn chips_use_m3_geometry() {
590 assert_eq!(CHIP_HEIGHT, rem(32.));
591 assert_eq!(RADIUS_SMALL, px(8.));
592 assert_eq!((CHIP_ICON, CHIP_GAP, AVATAR), (18., 8., 24.));
593 assert_eq!(
594 (ASSIST_TEXT_EDGE, INPUT_TEXT_EDGE, ICON_EDGE, AVATAR_EDGE),
595 (16., 12., 8., 4.)
596 );
597 }
598
599 #[test]
600 fn chips_use_m3_roles() {
601 let c = color;
602 let assist = chip_paint(ChipKind::Assist, ChipStyle::Flat, false, false);
603 assert_eq!(assist.outline, c(OUTLINE_VARIANT));
604 assert_eq!(assist.label, c(ON_SURFACE));
605 assert_eq!(assist.leading, c(PRIMARY));
606 assert_eq!(assist.container.a, 0.);
607 let elevated = chip_paint(ChipKind::Assist, ChipStyle::Elevated, false, false);
608 assert_eq!(elevated.container, c(SURFACE_CONTAINER_LOW));
609 assert_eq!(elevated.outline.a, 0.);
610 assert_eq!(elevated.elevation, 1);
611 let filter = chip_paint(ChipKind::Filter, ChipStyle::Flat, true, false);
612 assert_eq!(filter.container, c(SECONDARY_CONTAINER));
613 assert_eq!(filter.label, c(ON_SECONDARY_CONTAINER));
614 assert_eq!(filter.outline.a, 0.);
615 let input = chip_paint(ChipKind::Input, ChipStyle::Flat, false, false);
616 assert_eq!(input.label, c(ON_SURFACE_VARIANT));
617 assert_eq!(input.trailing, c(ON_SURFACE_VARIANT));
618 let suggestion = chip_paint(ChipKind::Suggestion, ChipStyle::Flat, false, false);
619 assert_eq!(suggestion.label, c(ON_SURFACE_VARIANT));
620 let disabled = chip_paint(ChipKind::Assist, ChipStyle::Flat, false, true);
621 assert_eq!(disabled.label, state::disabled(c(ON_SURFACE)));
622 assert_eq!(
623 disabled.outline,
624 c(ON_SURFACE).opacity(state::DISABLED_CONTAINER)
625 );
626 let disabled = chip_paint(ChipKind::Filter, ChipStyle::Elevated, true, true);
627 assert_eq!(
628 disabled.container,
629 c(ON_SURFACE).opacity(state::DISABLED_CONTAINER)
630 );
631 assert_eq!(disabled.elevation, 0, "disabled chips lie flat");
632 assert_eq!(
633 chip_paint(ChipKind::Assist, ChipStyle::Flat, true, false),
634 assist,
635 "assist chips have no selection"
636 );
637 }
638
639 struct ChipHarness {
640 selected: bool,
641 removed: usize,
642 clicks: usize,
643 }
644 impl Render for ChipHarness {
645 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
646 window.set_rem_size(px(14.));
647 div()
648 .flex()
649 .items_start()
650 .child(
651 div()
652 .debug_selector(|| "plain-chip".into())
653 .child(AssistChip::new("plain-chip", "Plain")),
654 )
655 .child(
656 div()
657 .debug_selector(|| "icon-chip".into())
658 .child(AssistChip::new("icon-chip", "Plain").icon(IconName::Info)),
659 )
660 .child(
661 div().debug_selector(|| "filter-chip".into()).child(
662 FilterChip::new("filter-chip", "Plain")
665 .on_toggle(cx.listener(|this, next: &bool, _, cx| {
666 this.selected = *next;
667 cx.notify();
668 }))
669 .selected(self.selected),
670 ),
671 )
672 .child(
673 div().debug_selector(|| "input-chip".into()).child(
674 InputChip::new("input-chip", "Plain")
675 .on_click(cx.listener(|this, _, _, cx| {
676 this.clicks += 1;
677 cx.notify();
678 }))
679 .on_remove("Remove", {
680 let view = cx.entity();
681 move |_, cx| {
682 view.update(cx, |this, cx| {
683 this.removed += 1;
684 cx.notify();
685 })
686 }
687 }),
688 ),
689 )
690 }
691 }
692
693 fn draw(cx: &mut VisualTestContext) {
694 cx.update(|window, cx| {
695 window.refresh();
696 window.draw(cx).clear(cx);
697 });
698 }
699
700 #[gpui::test]
701 fn a_leading_icon_takes_the_chip_padding_it_replaces(cx: &mut TestAppContext) {
702 cx.update(gpui_base::init);
703 let (_, cx) = cx.add_window_view(|_, _| ChipHarness {
704 selected: false,
705 removed: 0,
706 clicks: 0,
707 });
708 draw(cx);
709 let plain = cx.debug_bounds("plain-chip").unwrap();
710 let icon = cx.debug_bounds("icon-chip").unwrap();
711 assert_eq!(plain.size.height, px(32.));
712 assert_eq!(icon.size.width - plain.size.width, px(18. + 8. - 8.));
714 }
715
716 #[gpui::test]
717 fn selecting_a_filter_chip_slides_its_check_in(cx: &mut TestAppContext) {
718 cx.update(|cx| {
719 gpui_base::init(cx);
720 cx.set_reduce_motion(true);
721 });
722 let (view, cx) = cx.add_window_view(|_, _| ChipHarness {
723 selected: false,
724 removed: 0,
725 clicks: 0,
726 });
727 draw(cx);
728 let rest = cx.debug_bounds("filter-chip").unwrap();
729 assert_eq!(cx.debug_bounds("chip-check").unwrap().size.width, px(0.));
730 cx.simulate_click(rest.center(), Modifiers::default());
731 draw(cx);
732 view.read_with(cx, |h, _| assert!(h.selected));
733 let selected = cx.debug_bounds("filter-chip").unwrap();
734 assert_eq!(selected.size.width - rest.size.width, px(18. + 8. - 8.));
736 cx.simulate_click(selected.center(), Modifiers::default());
737 draw(cx);
738 view.read_with(cx, |h, _| assert!(!h.selected, "a second click clears it"));
739 }
740
741 #[gpui::test]
742 fn the_remove_icon_does_not_click_the_chip(cx: &mut TestAppContext) {
743 cx.update(|cx| {
744 gpui_base::init(cx);
745 cx.set_reduce_motion(true);
746 });
747 let (view, cx) = cx.add_window_view(|_, _| ChipHarness {
748 selected: false,
749 removed: 0,
750 clicks: 0,
751 });
752 draw(cx);
753 let remove = cx.debug_bounds("chip-remove").unwrap();
754 cx.simulate_click(remove.center(), Modifiers::default());
755 draw(cx);
756 view.read_with(cx, |h, _| assert_eq!((h.removed, h.clicks), (1, 0)));
757 let chip = cx.debug_bounds("input-chip").unwrap();
758 cx.simulate_click(
759 point(chip.left() + px(14.), chip.center().y),
760 Modifiers::default(),
761 );
762 draw(cx);
763 view.read_with(cx, |h, _| assert_eq!((h.removed, h.clicks), (1, 1)));
764 }
765
766 #[gpui::test]
767 fn a_press_released_over_the_remove_icon_removes_nothing(cx: &mut TestAppContext) {
768 cx.update(|cx| {
769 gpui_base::init(cx);
770 cx.set_reduce_motion(true);
771 });
772 let (view, cx) = cx.add_window_view(|_, _| ChipHarness {
773 selected: false,
774 removed: 0,
775 clicks: 0,
776 });
777 draw(cx);
778 let remove = cx.debug_bounds("chip-remove").unwrap().center();
779 let chip = cx.debug_bounds("input-chip").unwrap();
780 let label = point(chip.left() + px(14.), chip.center().y);
781 cx.simulate_mouse_down(label, MouseButton::Left, Modifiers::default());
782 cx.simulate_mouse_move(remove, Some(MouseButton::Left), Modifiers::default());
783 cx.simulate_mouse_up(remove, MouseButton::Left, Modifiers::default());
784 draw(cx);
785 view.read_with(cx, |h, _| assert_eq!(h.removed, 0));
786 }
787}