1use std::{collections::BTreeSet, ops::Deref, rc::Rc};
2
3use crate::prelude::*;
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Data)]
7pub enum Selectable {
8 #[default]
9 None,
11 Single,
13 Multi,
15}
16
17impl_res_simple!(Selectable);
18
19pub enum ListEvent {
21 Select(usize),
23 SelectFocused,
25 FocusNext,
27 FocusPrev,
29 ClearSelection,
31 Scroll(f32, f32),
33}
34
35#[derive(Lens)]
37pub struct List {
38 num_items: usize,
40 selected: BTreeSet<usize>,
42 selectable: Selectable,
44 focused: Option<usize>,
46 selection_follows_focus: bool,
48 orientation: Orientation,
50 scroll_to_cursor: bool,
52 #[lens(ignore)]
54 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
55 #[lens(ignore)]
57 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
58 scroll_x: f32,
60 scroll_y: f32,
62 show_horizontal_scrollbar: bool,
64 show_vertical_scrollbar: bool,
66}
67
68impl List {
69 pub fn new<L: Lens, T: 'static>(
71 cx: &mut Context,
72 list: L,
73 item_content: impl 'static + Fn(&mut Context, usize, MapRef<L, T>),
74 ) -> Handle<Self>
75 where
76 L::Target: Deref<Target = [T]> + Data,
77 {
78 Self::new_generic(
79 cx,
80 list,
81 |list| list.len(),
82 |list, index| &list[index],
83 |_| true,
84 item_content,
85 )
86 }
87
88 pub fn new_filtered<L: Lens, T: 'static>(
90 cx: &mut Context,
91 list: L,
92 filter: impl 'static + Clone + FnMut(&&T) -> bool,
93 item_content: impl 'static + Fn(&mut Context, usize, MapRef<L, T>),
94 ) -> Handle<Self>
95 where
96 L::Target: Deref<Target = [T]> + Data,
97 {
98 let f = filter.clone();
99 Self::new_generic(
100 cx,
101 list,
102 move |list| list.iter().filter(filter.clone()).count(),
103 move |list, index| &list[index],
104 f,
105 item_content,
106 )
107 }
108
109 pub fn new_generic<L: Lens, T: 'static>(
111 cx: &mut Context,
112 list: L,
113 list_len: impl 'static + Fn(&L::Target) -> usize,
114 list_index: impl 'static + Clone + Fn(&L::Target, usize) -> &T,
115 filter: impl 'static + Clone + FnMut(&&T) -> bool,
116 item_content: impl 'static + Fn(&mut Context, usize, MapRef<L, T>),
117 ) -> Handle<Self>
118 where
119 L::Target: Deref<Target = [T]> + Data,
120 {
121 let content = Rc::new(item_content);
122 let num_items = list.map(list_len);
123 Self {
124 num_items: num_items.get(cx),
125 selected: BTreeSet::default(),
126 selectable: Selectable::None,
127 focused: None,
128 selection_follows_focus: false,
129 orientation: Orientation::Vertical,
130 scroll_to_cursor: false,
131 on_select: None,
132 on_scroll: None,
133 scroll_x: 0.0,
134 scroll_y: 0.0,
135 show_horizontal_scrollbar: true,
136 show_vertical_scrollbar: true,
137 }
138 .build(cx, move |cx| {
139 Keymap::from(vec![
140 (
141 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
142 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
143 ),
144 (
145 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
146 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
147 ),
148 (
149 KeyChord::new(Modifiers::empty(), Code::Space),
150 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
151 ),
152 (
153 KeyChord::new(Modifiers::empty(), Code::Enter),
154 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
155 ),
156 ])
157 .build(cx);
158
159 Binding::new(cx, List::orientation, |cx, orientation| {
160 if orientation.get(cx) == Orientation::Horizontal {
161 cx.emit(KeymapEvent::RemoveAction(
162 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
163 "Focus Next",
164 ));
165
166 cx.emit(KeymapEvent::RemoveAction(
167 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
168 "Focus Previous",
169 ));
170
171 cx.emit(KeymapEvent::InsertAction(
172 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
173 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
174 ));
175
176 cx.emit(KeymapEvent::InsertAction(
177 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
178 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
179 ));
180 }
181 });
182
183 ScrollView::new(cx, move |cx| {
184 Binding::new(cx, num_items, move |cx, _| {
186 let mut f = filter.clone();
190 let ll = list
191 .get(cx)
192 .iter()
193 .enumerate()
194 .filter(|(_, v)| f(v))
195 .map(|(idx, _)| idx)
196 .collect::<Vec<_>>();
197
198 for index in ll.into_iter() {
199 let ll = list_index.clone();
200 let item = list.map_ref(move |list| ll(list, index));
201 let content = content.clone();
202 ListItem::new(
203 cx,
204 index,
205 item,
206 List::selected,
207 List::focused,
208 move |cx, index, item| {
209 content(cx, index, item);
210 },
211 );
212 }
213 });
214 })
215 .show_horizontal_scrollbar(Self::show_horizontal_scrollbar)
216 .show_vertical_scrollbar(Self::show_vertical_scrollbar)
217 .scroll_to_cursor(Self::scroll_to_cursor)
218 .scroll_x(Self::scroll_x)
219 .scroll_y(Self::scroll_y)
220 .on_scroll(|cx, x, y| {
221 if y.is_finite() {
222 cx.emit(ListEvent::Scroll(x, y));
223 }
224 });
225 })
226 .toggle_class("selectable", List::selectable.map(|s| *s != Selectable::None))
227 .toggle_class(
228 "horizontal",
229 Self::orientation.map(|orientation| *orientation == Orientation::Horizontal),
230 )
231 .navigable(true)
232 .role(Role::List)
233 }
234}
235
236impl View for List {
237 fn element(&self) -> Option<&'static str> {
238 Some("list")
239 }
240
241 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
242 event.take(|list_event, meta| match list_event {
243 ListEvent::Select(index) => {
244 cx.focus();
245 match self.selectable {
246 Selectable::Single => {
247 if self.selected.contains(&index) {
248 self.selected.clear();
249 self.focused = None;
250 } else {
251 self.selected.clear();
252 self.selected.insert(index);
253 self.focused = Some(index);
254 if let Some(on_select) = &self.on_select {
255 on_select(cx, index);
256 }
257 }
258 }
259
260 Selectable::Multi => {
261 if self.selected.contains(&index) {
262 self.selected.remove(&index);
263 self.focused = None;
264 } else {
265 self.selected.insert(index);
266 self.focused = Some(index);
267 if let Some(on_select) = &self.on_select {
268 on_select(cx, index);
269 }
270 }
271 }
272
273 Selectable::None => {}
274 }
275
276 meta.consume();
277 }
278
279 ListEvent::SelectFocused => {
280 if let Some(focused) = &self.focused {
281 cx.emit(ListEvent::Select(*focused))
282 }
283 meta.consume();
284 }
285
286 ListEvent::ClearSelection => {
287 self.selected.clear();
288 meta.consume();
289 }
290
291 ListEvent::FocusNext => {
292 if let Some(focused) = &mut self.focused {
293 if *focused < self.num_items.saturating_sub(1) {
294 *focused = focused.saturating_add(1);
295 if self.selection_follows_focus {
296 cx.emit(ListEvent::SelectFocused);
297 }
298 }
299 } else {
300 self.focused = Some(0);
301 if self.selection_follows_focus {
302 cx.emit(ListEvent::SelectFocused);
303 }
304 }
305
306 meta.consume();
307 }
308
309 ListEvent::FocusPrev => {
310 if let Some(focused) = &mut self.focused {
311 if *focused > 0 {
312 *focused = focused.saturating_sub(1);
313 if self.selection_follows_focus {
314 cx.emit(ListEvent::SelectFocused);
315 }
316 }
317 } else {
318 self.focused = Some(self.num_items.saturating_sub(1));
319 if self.selection_follows_focus {
320 cx.emit(ListEvent::SelectFocused);
321 }
322 }
323
324 meta.consume();
325 }
326
327 ListEvent::Scroll(x, y) => {
328 self.scroll_x = x;
329 self.scroll_y = y;
330 if let Some(callback) = &self.on_scroll {
331 (callback)(cx, x, y);
332 }
333
334 meta.consume();
335 }
336 })
337 }
338}
339
340impl Handle<'_, List> {
341 pub fn selected<S: Lens>(self, selected: S) -> Self
343 where
344 S::Target: Deref<Target = [usize]> + Data,
345 {
346 self.bind(selected, |handle, s| {
347 let ss = s.get(&handle).deref().to_vec();
348 handle.modify(|list| {
349 list.selected.clear();
350 for idx in ss {
351 list.selected.insert(idx);
352 list.focused = Some(idx);
353 }
354 });
355 })
356 }
357
358 pub fn on_select<F>(self, callback: F) -> Self
360 where
361 F: 'static + Fn(&mut EventContext, usize),
362 {
363 self.modify(|list: &mut List| list.on_select = Some(Box::new(callback)))
364 }
365
366 pub fn selectable<U: Into<Selectable>>(self, selectable: impl Res<U>) -> Self {
368 self.bind(selectable, |handle, selectable| {
369 let s = selectable.get(&handle).into();
370 handle.modify(|list: &mut List| list.selectable = s);
371 })
372 }
373
374 pub fn selection_follows_focus<U: Into<bool>>(self, flag: impl Res<U>) -> Self {
376 self.bind(flag, |handle, selection_follows_focus| {
377 let s = selection_follows_focus.get(&handle).into();
378 handle.modify(|list: &mut List| list.selection_follows_focus = s);
379 })
380 }
381
382 pub fn orientation<U: Into<Orientation>>(self, orientation: impl Res<U>) -> Self {
384 self.bind(orientation, |handle, orientation| {
385 let orientation = orientation.get(&handle).into();
386 handle.modify(|list: &mut List| {
387 list.orientation = orientation;
388 });
389 })
390 }
391
392 pub fn scroll_to_cursor(self, flag: bool) -> Self {
394 self.modify(|list| {
395 list.scroll_to_cursor = flag;
396 })
397 }
398
399 pub fn on_scroll(
401 self,
402 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
403 ) -> Self {
404 self.modify(|list: &mut List| list.on_scroll = Some(Box::new(callback)))
405 }
406
407 pub fn scroll_x(self, scrollx: impl Res<f32>) -> Self {
409 self.bind(scrollx, |handle, scrollx| {
410 let sx = scrollx.get(&handle);
411 handle.modify(|list| list.scroll_x = sx);
412 })
413 }
414
415 pub fn scroll_y(self, scrollx: impl Res<f32>) -> Self {
417 self.bind(scrollx, |handle, scrolly| {
418 let sy = scrolly.get(&handle);
419 handle.modify(|list| list.scroll_y = sy);
420 })
421 }
422
423 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool>) -> Self {
425 self.bind(flag, |handle, show_scrollbar| {
426 let s = show_scrollbar.get(&handle);
427 handle.modify(|list| list.show_horizontal_scrollbar = s);
428 })
429 }
430
431 pub fn show_vertical_scrollbar(self, flag: impl Res<bool>) -> Self {
433 self.bind(flag, |handle, show_scrollbar| {
434 let s = show_scrollbar.get(&handle);
435 handle.modify(|list| list.show_vertical_scrollbar = s);
436 })
437 }
438}
439
440pub struct ListItem {}
442
443impl ListItem {
444 pub fn new<L: Lens, T: 'static>(
446 cx: &mut Context,
447 index: usize,
448 item: MapRef<L, T>,
449 selected: impl Lens<Target = BTreeSet<usize>>,
450 focused: impl Lens<Target = Option<usize>>,
451 item_content: impl 'static + Fn(&mut Context, usize, MapRef<L, T>),
452 ) -> Handle<Self> {
453 let is_focused =
454 focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)).get(cx);
455 Self {}
456 .build(cx, move |cx| {
457 item_content(cx, index, item);
458 })
459 .role(Role::ListItem)
460 .toggle_class(
461 "focused",
462 focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)),
463 )
464 .checked(selected.map(move |selected| selected.contains(&index)))
465 .bind(
466 focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)),
467 move |handle, focused| {
468 if focused.get(&handle) != is_focused {
469 handle.cx.emit(ScrollEvent::ScrollToView(handle.entity()));
470 }
471 },
472 )
473 .on_press(move |cx| cx.emit(ListEvent::Select(index)))
474 }
475}
476
477impl View for ListItem {
478 fn element(&self) -> Option<&'static str> {
479 Some("list-item")
480 }
481}