ui/components/
multi_select.rs1use std::{cell::Cell, rc::Rc};
2
3use gpui::{AnyElement, Bounds, Context, Pixels, Render, anchored, canvas, deferred, point};
4
5use crate::prelude::*;
6use crate::{Chip, ToggleState};
7
8pub struct MultiSelect {
13 options: Vec<SharedString>,
14 selected: Vec<usize>,
15 open: bool,
16 placeholder: SharedString,
17 trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
22}
23
24impl MultiSelect {
25 pub fn new(options: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
26 Self {
27 options: options.into_iter().map(Into::into).collect(),
28 selected: Vec::new(),
29 open: false,
30 placeholder: "Select options…".into(),
31 trigger_bounds: Rc::new(Cell::new(None)),
32 }
33 }
34
35 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
36 self.placeholder = placeholder.into();
37 self
38 }
39
40 pub fn selected_indices(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
41 self.selected = indices.into_iter().collect();
42 self
43 }
44
45 pub fn values(&self) -> Vec<&SharedString> {
47 self.selected
48 .iter()
49 .filter_map(|&i| self.options.get(i))
50 .collect()
51 }
52}
53
54impl Render for MultiSelect {
55 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
56 let open = self.open;
57 let has_selection = !self.selected.is_empty();
58
59 let mut chips_row = h_flex().flex_1().flex_wrap().gap_1();
60 if has_selection {
61 for &i in &self.selected {
62 if let Some(label) = self.options.get(i).cloned() {
63 chips_row = chips_row.child(Chip::new(label).pill(true).dismissible(
64 cx.listener(move |this, _, _, cx| {
65 this.selected.retain(|&x| x != i);
66 cx.notify();
67 }),
68 ));
69 }
70 }
71 } else {
72 chips_row =
73 chips_row.child(Label::new(self.placeholder.clone()).color(Color::Placeholder));
74 }
75
76 let trigger = h_flex()
77 .id("multi-select-trigger")
78 .debug_selector(|| "MULTI-SELECT-TRIGGER".into())
82 .w_full()
83 .min_h(px(40.))
84 .items_center()
85 .justify_between()
86 .gap_2()
87 .px_3()
88 .py_2()
89 .rounded_md()
90 .bg(semantic::surface(cx))
91 .border_1()
92 .border_color(if open {
93 palette::primary(500)
94 } else {
95 semantic::border(cx)
96 })
97 .cursor_pointer()
98 .on_click(cx.listener(|this, _, _, cx| {
99 this.open = !this.open;
100 cx.notify();
101 }))
102 .child(chips_row)
103 .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
104 .child({
105 let trigger_bounds = self.trigger_bounds.clone();
106 canvas(
107 move |bounds, _window, _cx| trigger_bounds.set(Some(bounds)),
108 |_bounds, _state, _window, _cx| {},
109 )
110 .absolute()
111 .top_0()
112 .left_0()
113 .size_full()
114 });
115
116 let trigger_width = px(280.);
117
118 v_flex()
119 .w(trigger_width)
120 .gap_1()
121 .child(trigger)
122 .when(open, |this| {
123 let hover = semantic::hover_bg(cx);
124 let mut list = v_flex()
125 .w(trigger_width)
126 .p_1()
127 .rounded_md()
128 .bg(semantic::elevated_surface(cx))
129 .border_1()
130 .border_color(semantic::border(cx))
131 .shadow_level(Shadow::Lg);
132
133 for (i, option) in self.options.iter().enumerate() {
134 let checked = self.selected.contains(&i);
135 let option = option.clone();
136 list = list.child(
137 h_flex()
138 .id(("multi-select-option", i))
139 .w_full()
140 .items_center()
141 .gap_2()
142 .px_3()
143 .py_2()
144 .rounded_md()
145 .cursor_pointer()
146 .hover(move |s| s.bg(hover))
147 .on_click(cx.listener(move |this, _, _, cx| {
148 if let Some(pos) = this.selected.iter().position(|&x| x == i) {
149 this.selected.remove(pos);
150 } else {
151 this.selected.push(i);
152 }
153 cx.notify();
154 }))
155 .child(
156 Checkbox::new(
157 ("multi-select-check", i),
158 if checked {
159 ToggleState::Selected
160 } else {
161 ToggleState::Unselected
162 },
163 )
164 .visualization_only(true),
165 )
166 .child(Label::new(option)),
167 );
168 }
169
170 let mut anchor = anchored().snap_to_window_with_margin(px(8.));
177 if let Some(bounds) = self.trigger_bounds.get() {
178 anchor = anchor.position(point(
179 bounds.origin.x,
180 bounds.origin.y + bounds.size.height + px(4.),
181 ));
182 }
183 let floating_list = deferred(
184 anchor.child(
185 div()
186 .occlude()
187 .debug_selector(|| "MULTI-SELECT-LIST".into())
188 .child(list),
189 ),
190 )
191 .with_priority(1);
192 this.child(floating_list)
193 })
194 }
195}
196
197pub fn multi_select_preview(_window: &mut Window, cx: &mut App) -> AnyElement {
201 v_flex()
202 .gap_4()
203 .child(cx.new(|_| {
204 MultiSelect::new(["Design", "Engineering", "Marketing", "Sales", "Support"])
205 .selected_indices([0, 2])
206 }))
207 .into_any_element()
208}