1use std::collections::BTreeSet;
9
10use gpui::prelude::*;
11use gpui::{div, px, Context, EventEmitter, FocusHandle, IntoElement, SharedString, Window};
12
13use crate::devtools::Probed;
14use crate::icon::{Icon, IconName};
15use crate::theme::{theme, Size};
16
17#[derive(Debug, Clone)]
19pub struct TransferEvent(pub Vec<usize>);
20
21fn move_checked(
23 right: &mut BTreeSet<usize>,
24 checked: &mut BTreeSet<usize>,
25 to_right: bool,
26) -> bool {
27 let movers: Vec<usize> = checked
28 .iter()
29 .copied()
30 .filter(|i| right.contains(i) != to_right)
31 .collect();
32 for i in &movers {
33 if to_right {
34 right.insert(*i);
35 } else {
36 right.remove(i);
37 }
38 checked.remove(i);
39 }
40 !movers.is_empty()
41}
42
43pub struct Transfer {
45 items: Vec<SharedString>,
46 right: BTreeSet<usize>,
47 checked: BTreeSet<usize>,
48 titles: (SharedString, SharedString),
49 height: f32,
50 focus: FocusHandle,
51 disabled: bool,
52}
53
54impl EventEmitter<TransferEvent> for Transfer {}
55
56impl Transfer {
57 pub fn new(cx: &mut Context<Self>) -> Self {
58 Transfer {
59 items: Vec::new(),
60 right: BTreeSet::new(),
61 checked: BTreeSet::new(),
62 titles: (
63 SharedString::new_static("Available"),
64 SharedString::new_static("Chosen"),
65 ),
66 height: 200.0,
67 focus: cx.focus_handle(),
68 disabled: false,
69 }
70 }
71
72 pub fn data<I, S>(mut self, items: I) -> Self
73 where
74 I: IntoIterator<Item = S>,
75 S: Into<SharedString>,
76 {
77 self.items = items.into_iter().map(Into::into).collect();
78 self
79 }
80
81 pub fn chosen(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
83 self.right = indices.into_iter().collect();
84 self
85 }
86
87 pub fn titles(mut self, left: impl Into<SharedString>, right: impl Into<SharedString>) -> Self {
88 self.titles = (left.into(), right.into());
89 self
90 }
91
92 pub fn height(mut self, height: f32) -> Self {
93 self.height = height.max(80.0);
94 self
95 }
96
97 pub fn disabled(mut self, disabled: bool) -> Self {
98 self.disabled = disabled;
99 self
100 }
101
102 pub fn chosen_indices(&self) -> Vec<usize> {
104 self.right.iter().copied().collect()
105 }
106
107 fn toggle_checked(&mut self, index: usize, cx: &mut Context<Self>) {
108 if !self.checked.remove(&index) {
109 self.checked.insert(index);
110 }
111 cx.notify();
112 }
113
114 fn transfer(&mut self, to_right: bool, cx: &mut Context<Self>) {
115 if move_checked(&mut self.right, &mut self.checked, to_right) {
116 cx.emit(TransferEvent(self.chosen_indices()));
117 cx.notify();
118 }
119 }
120
121 fn pane(
122 &self,
123 title: &SharedString,
124 indices: Vec<usize>,
125 list_id: &'static str,
126 cx: &mut Context<Self>,
127 ) -> impl IntoElement {
128 let t = theme(cx);
129 let radius = t.radius(t.default_radius);
130 let surface = t.surface().hsla();
131 let surface_hover = t.surface_hover().hsla();
132 let border = t.border().hsla();
133 let text_color = t.text().hsla();
134 let dimmed = t.dimmed().hsla();
135 let checked_bg = t.primary().alpha(0.12);
136 let font = t.font_size(Size::Sm);
137
138 let mut rows = div()
139 .id(list_id)
140 .flex()
141 .flex_col()
142 .gap(px(2.0))
143 .p(px(4.0))
144 .flex_1()
145 .overflow_y_scroll();
146 for i in indices {
147 let is_checked = self.checked.contains(&i);
148 let mut row = div()
149 .id((list_id, i))
150 .px(px(8.0))
151 .py(px(5.0))
152 .rounded(px(4.0))
153 .text_size(px(font))
154 .text_color(text_color)
155 .child(self.items[i].clone())
156 .on_click(cx.listener(move |this, _ev, _window, cx| {
157 if !this.disabled {
158 this.toggle_checked(i, cx);
159 }
160 }));
161 if is_checked {
162 row = row.bg(checked_bg);
163 } else {
164 row = row.hover(move |s| s.bg(surface_hover));
165 }
166 rows = rows.child(row);
167 }
168
169 div()
170 .flex()
171 .flex_col()
172 .flex_1()
173 .h(px(self.height))
174 .rounded(px(radius))
175 .border_1()
176 .border_color(border)
177 .bg(surface)
178 .child(
179 div()
180 .px(px(8.0))
181 .py(px(5.0))
182 .border_b_1()
183 .border_color(border)
184 .text_size(px(font - 1.0))
185 .text_color(dimmed)
186 .child(title.clone()),
187 )
188 .child(rows)
189 }
190}
191
192impl Render for Transfer {
193 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
194 let t = theme(cx);
195 let surface = t.surface().hsla();
196 let surface_hover = t.surface_hover().hsla();
197 let border = t.border().hsla();
198 let dimmed = t.dimmed().hsla();
199
200 let left: Vec<usize> = (0..self.items.len())
201 .filter(|i| !self.right.contains(i))
202 .collect();
203 let right: Vec<usize> = self.chosen_indices();
204
205 let (left_title, right_title) = self.titles.clone();
206 let left_pane = self.pane(&left_title, left, "guise-transfer-left", cx);
207 let right_pane = self.pane(&right_title, right, "guise-transfer-right", cx);
208
209 let mut buttons = div().flex().flex_col().justify_center().gap(px(6.0));
210 for (key, icon, to_right) in [
211 ("guise-transfer-toright", IconName::ChevronRight, true),
212 ("guise-transfer-toleft", IconName::ChevronLeft, false),
213 ] {
214 buttons = buttons.child(
215 div()
216 .id(key)
217 .flex()
218 .items_center()
219 .justify_center()
220 .w(px(26.0))
221 .h(px(26.0))
222 .rounded(px(6.0))
223 .bg(surface)
224 .border_1()
225 .border_color(border)
226 .text_color(dimmed)
227 .hover(move |s| s.bg(surface_hover))
228 .child(Icon::new(icon).size(Size::Xs))
229 .on_click(cx.listener(move |this, _ev, _window, cx| {
230 if !this.disabled {
231 this.transfer(to_right, cx);
232 }
233 })),
234 );
235 }
236
237 let row = div()
238 .track_focus(&self.focus)
239 .flex()
240 .items_center()
241 .gap(px(10.0))
242 .w_full()
243 .child(left_pane)
244 .child(buttons)
245 .child(right_pane);
246
247 let element = if self.disabled { row.opacity(0.6) } else { row };
248
249 element.probe("Transfer")
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn moves_only_checked_items_on_the_source_side() {
259 let mut right: BTreeSet<usize> = [3].into_iter().collect();
260 let mut checked: BTreeSet<usize> = [0, 1, 3].into_iter().collect();
261 assert!(move_checked(&mut right, &mut checked, true));
264 assert_eq!(right.iter().copied().collect::<Vec<_>>(), vec![0, 1, 3]);
265 assert!(checked.contains(&3) && !checked.contains(&0));
266
267 assert!(move_checked(&mut right, &mut checked, false));
269 assert_eq!(right.iter().copied().collect::<Vec<_>>(), vec![0, 1]);
270 assert!(checked.is_empty());
271 }
272
273 #[test]
274 fn no_movers_reports_false() {
275 let mut right = BTreeSet::new();
276 let mut checked = BTreeSet::new();
277 assert!(!move_checked(&mut right, &mut checked, true));
278 checked.insert(2);
279 assert!(!move_checked(&mut right, &mut checked, false));
281 assert!(checked.contains(&2));
282 }
283}