vizia_core/views/
list.rs

1use std::{collections::BTreeSet, ops::Deref, rc::Rc};
2
3use crate::prelude::*;
4
5/// Represents how items can be selected in a list.
6#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Data)]
7pub enum Selectable {
8    #[default]
9    /// Items in the list cannot be selected.
10    None,
11    /// A single item in the list can be selected.
12    Single,
13    /// Multiple items in the list can be selected simultaneously.
14    Multi,
15}
16
17impl_res_simple!(Selectable);
18
19/// Events used by the [List] view
20pub enum ListEvent {
21    /// Selects a list item with the given index.
22    Select(usize),
23    /// Selects the focused list item.
24    SelectFocused,
25    ///  Moves the focus to the next item in the list.
26    FocusNext,
27    ///  Moves the focus to the previous item in the list.
28    FocusPrev,
29    /// Deselects all items from the list
30    ClearSelection,
31    /// Scrolls the list to the given x and y position.
32    Scroll(f32, f32),
33}
34
35/// A view for creating a list of items from a binding to an iteratable list.
36#[derive(Lens)]
37pub struct List {
38    /// The number of items in the list.
39    num_items: usize,
40    /// The set of selected items in the list.
41    selected: BTreeSet<usize>,
42    /// Whether the list items are selectable.
43    selectable: Selectable,
44    /// The index of the currently focused item in the list.
45    focused: Option<usize>,
46    /// Whether the selection should follow the focus.
47    selection_follows_focus: bool,
48    /// The orientation of the list, either vertical or horizontal.
49    orientation: Orientation,
50    /// Whether the scrollview should scroll to the cursor when the scrollbar is pressed.
51    scroll_to_cursor: bool,
52    /// Callback called when a list item is selected.
53    #[lens(ignore)]
54    on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
55    /// Callback called when the scrollview is scrolled.
56    #[lens(ignore)]
57    on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
58    /// The horizontal scroll position of the list.
59    scroll_x: f32,
60    /// The vertical scroll position of the list.
61    scroll_y: f32,
62    /// Whether the horizontal scrollbar should be visible.
63    show_horizontal_scrollbar: bool,
64    /// Whether the vertical scrollbar should be visible.
65    show_vertical_scrollbar: bool,
66}
67
68impl List {
69    /// Creates a new [List] view.
70    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    /// Creates a new [List] view with a provided filter closure.
89    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    /// Creates a new [List] view with a binding to the given lens and a template for constructing the list items.
110    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                // Bind to the list data
185                Binding::new(cx, num_items, move |cx, _| {
186                    // If the number of list items is different to the number of children of the ListView
187                    // then remove and rebuild all the children
188
189                    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    /// Sets the  selected items of the list. Takes a lens to a list of indices.
342    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    /// Sets the callback triggered when a [ListItem] is selected.
359    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    /// Set the selectable state of the [List].
367    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    /// Sets whether the selection should follow the focus.
375    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    /// Sets the orientation of the list.
383    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    /// Sets whether the scrollbar should move to the cursor when pressed.
393    pub fn scroll_to_cursor(self, flag: bool) -> Self {
394        self.modify(|list| {
395            list.scroll_to_cursor = flag;
396        })
397    }
398
399    /// Sets a callback which will be called when a scrollview is scrolled, either with the mouse wheel, touchpad, or using the scroll bars.
400    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    /// Set the horizontal scroll position of the [ScrollView]. Accepts a value or lens to an 'f32' between 0 and 1.
408    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    /// Set the vertical scroll position of the [ScrollView]. Accepts a value or lens to an 'f32' between 0 and 1.
416    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    /// Sets whether the horizontal scrollbar should be visible.
424    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    /// Sets whether the vertical scrollbar should be visible.
432    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
440/// A view which represents a selectable item within a list.
441pub struct ListItem {}
442
443impl ListItem {
444    /// Create a new [ListItem] view.
445    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}