Skip to main content

vizia_core/views/
list.rs

1use std::{collections::BTreeSet, ops::Deref, rc::Rc};
2use vizia_reactive::{Scope, SignalGet, SignalWith, UpdaterEffect};
3
4use crate::prelude::*;
5use crate::{binding::BindingHandler, context::SIGNAL_REBUILDS};
6
7/// Represents how items can be selected in a list.
8#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
9pub enum Selectable {
10    #[default]
11    /// Items in the list cannot be selected.
12    None,
13    /// A single item in the list can be selected.
14    Single,
15    /// Multiple items in the list can be selected simultaneously.
16    Multi,
17}
18
19impl_res_simple!(Selectable);
20
21/// Events used by the [List] view
22pub enum ListEvent {
23    /// Selects a list item with the given index.
24    Select(usize),
25    /// Selects the focused list item.
26    SelectFocused,
27    ///  Moves the focus to the next item in the list.
28    FocusNext,
29    ///  Moves the focus to the previous item in the list.
30    FocusPrev,
31    /// Deselects all items from the list
32    ClearSelection,
33    /// Scrolls the list to the given x and y position.
34    Scroll(f32, f32),
35}
36
37/// A view for creating a list of items from an iterable signal.
38pub struct List {
39    /// The number of items in the list.
40    num_items: usize,
41    /// The set of selected items in the list.
42    selection: Signal<BTreeSet<usize>>,
43    /// Whether the list items are selectable.
44    selectable: Signal<Selectable>,
45    /// The index of the currently focused item in the list.
46    focused: Signal<Option<usize>>,
47    /// Whether the selection should follow the focus.
48    selection_follows_focus: Signal<bool>,
49    /// Minimum number of selected items.
50    min_selected: Signal<usize>,
51    /// Maximum number of selected items.
52    max_selected: Signal<usize>,
53    /// The orientation of the list, either vertical or horizontal.
54    orientation: Signal<Orientation>,
55    /// Whether the scrollview should scroll to the cursor when the scrollbar is pressed.
56    scroll_to_cursor: Signal<bool>,
57    /// Callback called when a list item is selected.
58    on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
59    /// Callback called when the scrollview is scrolled.
60    on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
61    /// The horizontal scroll position of the list.
62    scroll_x: Signal<f32>,
63    /// The vertical scroll position of the list.
64    scroll_y: Signal<f32>,
65    /// Whether the horizontal scrollbar should be visible.
66    show_horizontal_scrollbar: Signal<bool>,
67    /// Whether the vertical scrollbar should be visible.
68    show_vertical_scrollbar: Signal<bool>,
69}
70
71/// A binding handler that manages list item entities for a [List].
72///
73/// The user provides `Vec<T>` wrapped in an outer signal.
74/// This handler creates internal signals for each item and maintains them.
75/// Value changes to existing items update their internal signals (zero entity rebuilds).
76/// Structural changes (add/remove/reorder) are handled by diffing values and rebuilding from the first changed position.
77struct ListItemsBinding<T: 'static> {
78    entity: Entity,
79    list_entity: Entity,
80    get_fn: Box<dyn Fn() -> Vec<T>>,
81    item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
82    selection: Signal<BTreeSet<usize>>,
83    focused: Signal<Option<usize>>,
84    /// Internal signals for each list item.
85    item_signals: Vec<Signal<T>>,
86    /// Entity IDs of the ListItem views.
87    item_entities: Vec<Entity>,
88    /// Previous values, used for value-based diffing.
89    prev_values: Vec<T>,
90    scope: Scope,
91}
92
93impl<T: PartialEq + Clone + 'static> ListItemsBinding<T> {
94    fn create<S, V>(
95        cx: &mut Context,
96        list_entity: Entity,
97        list: S,
98        selection: Signal<BTreeSet<usize>>,
99        focused: Signal<Option<usize>>,
100        item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
101    ) where
102        S: SignalGet<V> + SignalWith<V> + Copy + 'static,
103        V: Deref<Target = [T]> + Clone + 'static,
104    {
105        let entity = cx.entity_manager.create();
106        cx.tree.add(entity, cx.current()).expect("Failed to add to tree");
107        cx.tree.set_ignored(entity, true);
108
109        let scope = Scope::new();
110        let initial_values: Vec<T> = scope.enter(|| {
111            UpdaterEffect::new(
112                move || list.with(|list| list.deref().to_vec()),
113                move |_new_value| {
114                    SIGNAL_REBUILDS.with_borrow_mut(|set| {
115                        set.insert(entity);
116                    });
117                },
118            )
119        });
120
121        let mut binding = Self {
122            entity,
123            list_entity,
124            get_fn: Box::new(move || list.with_untracked(|list| list.deref().to_vec())),
125            item_content,
126            selection,
127            focused,
128            item_signals: Vec::new(),
129            item_entities: Vec::new(),
130            prev_values: Vec::new(),
131            scope,
132        };
133
134        // Build initial items.
135        for (index, value) in initial_values.iter().enumerate() {
136            let signal = Signal::new(value.clone());
137            let entity = binding.create_item_entity(cx, index, signal);
138            binding.item_signals.push(signal);
139            binding.item_entities.push(entity);
140            binding.prev_values.push(value.clone());
141        }
142        binding.update_list_metadata(cx, initial_values.len());
143
144        cx.bindings.insert(entity, Box::new(binding));
145
146        let _: Handle<Self> =
147            Handle { current: entity, entity, p: Default::default(), cx }.ignore();
148    }
149
150    fn update_list_metadata(&self, cx: &mut Context, len: usize) {
151        if let Some(view) = cx.views.get_mut(&self.list_entity) {
152            if let Some(list) = view.downcast_mut::<List>() {
153                list.num_items = len;
154                list.normalize_selection_state();
155            }
156        }
157    }
158
159    fn create_item_entity(&self, cx: &mut Context, index: usize, signal: Signal<T>) -> Entity {
160        let mut created = Entity::null();
161        let item_content = self.item_content.clone();
162        let selection = self.selection;
163        let focused = self.focused;
164
165        cx.with_current(self.entity, |cx| {
166            created = ListItem::new(cx, index, signal, selection, focused, {
167                let item_content = item_content.clone();
168                move |cx, index, item| (item_content)(cx, index, item)
169            })
170            .entity();
171        });
172
173        created
174    }
175}
176
177impl<T: PartialEq + Clone + 'static> BindingHandler for ListItemsBinding<T> {
178    fn update(&mut self, cx: &mut Context) {
179        let new_values = (self.get_fn)();
180        let new_len = new_values.len();
181
182        // Find the first position where values differ.
183        let first_diff = self
184            .prev_values
185            .iter()
186            .zip(new_values.iter())
187            .position(|(old, new)| old != new)
188            .unwrap_or(self.prev_values.len().min(new_len));
189
190        // Remove all entities from first_diff onward.
191        for entity in self.item_entities.drain(first_diff..) {
192            cx.remove(entity);
193        }
194        self.item_signals.truncate(first_diff);
195
196        // Update existing signals or create new items from first_diff onward.
197        for (i, value) in new_values[first_diff..].iter().enumerate() {
198            let index = first_diff + i;
199            if index < self.item_signals.len() {
200                // Update existing signal
201                self.item_signals[index].set(value.clone());
202            } else {
203                // Create new signal and item
204                let signal = Signal::new(value.clone());
205                let entity = self.create_item_entity(cx, index, signal);
206                self.item_signals.push(signal);
207                self.item_entities.push(entity);
208            }
209        }
210
211        self.prev_values = new_values;
212        self.update_list_metadata(cx, new_len);
213    }
214
215    fn remove(&self, _cx: &mut Context) {
216        self.scope.dispose();
217    }
218
219    fn debug(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
220        f.write_str("ListItemsBinding")
221    }
222}
223
224impl List {
225    fn selection_limits(&self) -> (usize, usize) {
226        let mut min_selected = self.min_selected.get();
227        let mut max_selected = self.max_selected.get();
228
229        match self.selectable.get() {
230            Selectable::None => {
231                min_selected = 0;
232                max_selected = 0;
233            }
234
235            Selectable::Single => {
236                min_selected = min_selected.min(1);
237                max_selected = 1;
238            }
239
240            Selectable::Multi => {}
241        }
242
243        max_selected = max_selected.min(self.num_items);
244        min_selected = min_selected.min(max_selected);
245
246        (min_selected, max_selected)
247    }
248
249    fn normalize_selection_state(&mut self) {
250        let (min_selected, max_selected) = self.selection_limits();
251
252        let mut selection = self.selection.get();
253        selection.retain(|index| *index < self.num_items);
254
255        while selection.len() > max_selected {
256            if let Some(last) = selection.iter().next_back().copied() {
257                selection.remove(&last);
258            } else {
259                break;
260            }
261        }
262
263        if selection.len() < min_selected {
264            for index in 0..self.num_items {
265                selection.insert(index);
266                if selection.len() >= min_selected {
267                    break;
268                }
269            }
270        }
271
272        let mut focused = self.focused.get();
273        if focused.is_some_and(|index| index >= self.num_items) {
274            focused = self.num_items.checked_sub(1);
275        }
276
277        self.selection.set(selection);
278        self.focused.set(focused);
279    }
280
281    /// Creates a new [List] view from a reactive or static list of values.
282    ///
283    /// `list` accepts any [`Res<V>`] source where `V` derefs to `[T]` — for example a
284    /// `Signal<Vec<T>>` for a reactive list, or a plain `Vec<T>` for a static list.
285    /// The list creates and manages internal signals for each item automatically.
286    /// Value changes to existing items update their internal signals with zero entity rebuilds.
287    /// Structural changes (add/remove/reorder) are handled by diffing values and rebuilding from the first changed position.
288    pub fn new<S, V, T>(
289        cx: &mut Context,
290        list: S,
291        item_content: impl 'static + Fn(&mut Context, usize, Signal<T>),
292    ) -> Handle<Self>
293    where
294        S: Res<V> + 'static,
295        V: Deref<Target = [T]> + Clone + 'static,
296        T: PartialEq + Clone + 'static,
297    {
298        let content: Rc<dyn Fn(&mut Context, usize, Signal<T>)> = Rc::new(item_content);
299        let selection = Signal::new(BTreeSet::default());
300        let selectable = Signal::new(Selectable::None);
301        let focused = Signal::new(None);
302        let min_selected = Signal::new(0);
303        let max_selected = Signal::new(usize::MAX);
304        let orientation = Signal::new(Orientation::Vertical);
305        let scroll_to_cursor = Signal::new(false);
306        let scroll_x = Signal::new(0.0);
307        let scroll_y = Signal::new(0.0);
308        let show_horizontal_scrollbar = Signal::new(true);
309        let show_vertical_scrollbar = Signal::new(true);
310
311        Self {
312            num_items: 0,
313            selection,
314            selectable,
315            focused,
316            selection_follows_focus: Signal::new(false),
317            min_selected,
318            max_selected,
319            orientation,
320            scroll_to_cursor,
321            on_select: None,
322            on_scroll: None,
323            scroll_x,
324            scroll_y,
325            show_horizontal_scrollbar,
326            show_vertical_scrollbar,
327        }
328        .build(cx, move |cx| {
329            let list_entity = cx.current();
330
331            Keymap::from(vec![
332                (
333                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
334                    KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
335                ),
336                (
337                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
338                    KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
339                ),
340                (
341                    KeyChord::new(Modifiers::empty(), Code::Space),
342                    KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
343                ),
344                (
345                    KeyChord::new(Modifiers::empty(), Code::Enter),
346                    KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
347                ),
348            ])
349            .build(cx);
350
351            Binding::new(cx, orientation, move |cx| {
352                let orientation = orientation.get();
353                if orientation == Orientation::Horizontal {
354                    cx.emit(KeymapEvent::RemoveAction(
355                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
356                        "Focus Next",
357                    ));
358
359                    cx.emit(KeymapEvent::RemoveAction(
360                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
361                        "Focus Previous",
362                    ));
363
364                    cx.emit(KeymapEvent::InsertAction(
365                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
366                        KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
367                    ));
368
369                    cx.emit(KeymapEvent::InsertAction(
370                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
371                        KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
372                    ));
373                }
374            });
375
376            let list_signal = list.to_signal(cx);
377            ScrollView::new(cx, move |cx| {
378                ListItemsBinding::create(
379                    cx,
380                    list_entity,
381                    list_signal,
382                    selection,
383                    focused,
384                    content.clone(),
385                );
386            })
387            .show_horizontal_scrollbar(show_horizontal_scrollbar)
388            .show_vertical_scrollbar(show_vertical_scrollbar)
389            .scroll_to_cursor(scroll_to_cursor)
390            .scroll_x(scroll_x)
391            .scroll_y(scroll_y)
392            .on_scroll(|cx, x, y| {
393                if y.is_finite() {
394                    cx.emit(ListEvent::Scroll(x, y));
395                }
396            });
397        })
398        .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
399        .orientation(orientation)
400        .navigable(true)
401        .role(Role::ListBox)
402    }
403}
404
405impl View for List {
406    fn element(&self) -> Option<&'static str> {
407        Some("list")
408    }
409
410    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
411        event.take(|list_event, meta| match list_event {
412            ListEvent::Select(index) => {
413                cx.focus();
414                let selectable = self.selectable.get();
415                let (min_selected, max_selected) = self.selection_limits();
416                let mut selection = self.selection.get();
417                let mut focused = self.focused.get();
418                match selectable {
419                    Selectable::Single => {
420                        if selection.contains(&index) {
421                            if min_selected == 0 {
422                                selection.clear();
423                                focused = None;
424                            }
425                        } else {
426                            selection.clear();
427                            selection.insert(index);
428                            focused = Some(index);
429                            if let Some(on_select) = &self.on_select {
430                                on_select(cx, index);
431                            }
432                        }
433                    }
434
435                    Selectable::Multi => {
436                        if selection.contains(&index) {
437                            if selection.len() > min_selected {
438                                selection.remove(&index);
439                                if focused == Some(index) {
440                                    focused = selection.iter().next_back().copied();
441                                }
442                            }
443                        } else {
444                            if selection.len() < max_selected {
445                                selection.insert(index);
446                                focused = Some(index);
447                                if let Some(on_select) = &self.on_select {
448                                    on_select(cx, index);
449                                }
450                            }
451                        }
452                    }
453
454                    Selectable::None => {}
455                }
456
457                self.selection.set(selection);
458                self.focused.set(focused);
459
460                meta.consume();
461            }
462
463            ListEvent::SelectFocused => {
464                if let Some(focused) = self.focused.get() {
465                    cx.emit(ListEvent::Select(focused))
466                }
467                meta.consume();
468            }
469
470            ListEvent::ClearSelection => {
471                let (min_selected, _) = self.selection_limits();
472                if min_selected == 0 {
473                    self.selection.set(BTreeSet::default());
474                }
475                meta.consume();
476            }
477
478            ListEvent::FocusNext => {
479                let mut focused = self.focused.get();
480                if let Some(f) = &mut focused {
481                    if *f < self.num_items.saturating_sub(1) {
482                        *f = f.saturating_add(1);
483                        if self.selection_follows_focus.get() {
484                            cx.emit(ListEvent::SelectFocused);
485                        }
486                    }
487                } else {
488                    focused = Some(0);
489                    if self.selection_follows_focus.get() {
490                        cx.emit(ListEvent::SelectFocused);
491                    }
492                }
493
494                self.focused.set(focused);
495
496                meta.consume();
497            }
498
499            ListEvent::FocusPrev => {
500                let mut focused = self.focused.get();
501                if let Some(f) = &mut focused {
502                    if *f > 0 {
503                        *f = f.saturating_sub(1);
504                        if self.selection_follows_focus.get() {
505                            cx.emit(ListEvent::SelectFocused);
506                        }
507                    }
508                } else {
509                    focused = Some(self.num_items.saturating_sub(1));
510                    if self.selection_follows_focus.get() {
511                        cx.emit(ListEvent::SelectFocused);
512                    }
513                }
514
515                self.focused.set(focused);
516
517                meta.consume();
518            }
519
520            ListEvent::Scroll(x, y) => {
521                self.scroll_x.set(x);
522                self.scroll_y.set(y);
523                if let Some(callback) = &self.on_scroll {
524                    (callback)(cx, x, y);
525                }
526
527                meta.consume();
528            }
529        })
530    }
531}
532
533/// Modifiers for changing the behavior and selection state of a [List].
534pub trait ListModifiers: Sized {
535    /// Sets the selected items of the list from signal of type indices.
536    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
537    where
538        R: Deref<Target = [usize]> + Clone + 'static;
539
540    /// Sets the callback triggered when a [ListItem] is selected.
541    fn on_select<F>(self, callback: F) -> Self
542    where
543        F: 'static + Fn(&mut EventContext, usize);
544
545    /// Set the selectable state of the [List].
546    fn selectable<U: Into<Selectable> + Clone + 'static>(
547        self,
548        selectable: impl Res<U> + 'static,
549    ) -> Self;
550
551    /// Sets the minimum number of selected items.
552    fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self;
553
554    /// Sets the maximum number of selected items.
555    fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self;
556
557    /// Sets whether the selection should follow the focus.
558    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
559        self,
560        flag: impl Res<U> + 'static,
561    ) -> Self;
562
563    /// Sets the orientation of the list.
564    fn horizontal<U: Into<bool> + Clone + 'static>(self, horizontal: impl Res<U> + 'static)
565    -> Self;
566
567    /// Sets whether the scrollbar should move to the cursor when pressed.
568    fn scroll_to_cursor(self, flag: bool) -> Self;
569
570    /// Sets a callback which will be called when a scrollview is scrolled, either with the mouse wheel, touchpad, or using the scroll bars.
571    fn on_scroll(
572        self,
573        callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
574    ) -> Self;
575
576    /// Set the horizontal scroll position of the [ScrollView]. Accepts a value or signal of type an `f32` between 0 and 1.
577    fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self;
578
579    /// Set the vertical scroll position of the [ScrollView]. Accepts a value or signal of type an `f32` between 0 and 1.
580    fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self;
581
582    /// Sets whether the horizontal scrollbar should be visible.
583    fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
584
585    /// Sets whether the vertical scrollbar should be visible.
586    fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
587}
588
589impl ListModifiers for Handle<'_, List> {
590    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
591    where
592        R: Deref<Target = [usize]> + Clone + 'static,
593    {
594        let selection = selection.to_signal(self.cx);
595        self.bind(selection, move |handle| {
596            selection.with(|selected_indices| {
597                handle.modify(|list| {
598                    let mut selection = BTreeSet::default();
599                    let mut focused = None;
600                    for idx in selected_indices.deref().iter().copied() {
601                        selection.insert(idx);
602                        focused = Some(idx);
603                    }
604                    list.selection.set(selection);
605                    list.focused.set(focused);
606                    list.normalize_selection_state();
607                });
608            });
609        })
610    }
611
612    fn on_select<F>(self, callback: F) -> Self
613    where
614        F: 'static + Fn(&mut EventContext, usize),
615    {
616        self.modify(|list: &mut List| list.on_select = Some(Box::new(callback)))
617    }
618
619    fn selectable<U: Into<Selectable> + Clone + 'static>(
620        self,
621        selectable: impl Res<U> + 'static,
622    ) -> Self {
623        let selectable = selectable.to_signal(self.cx);
624        self.bind(selectable, move |handle| {
625            let selectable = selectable.get();
626            let s = selectable.into();
627            handle.modify(|list: &mut List| {
628                list.selectable.set(s);
629                list.normalize_selection_state();
630            });
631        })
632    }
633
634    fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
635        let min_selected = min_selected.to_signal(self.cx);
636        self.bind(min_selected, move |handle| {
637            let min_selected = min_selected.get();
638            handle.modify(|list: &mut List| {
639                list.min_selected.set(min_selected);
640                list.normalize_selection_state();
641            });
642        })
643    }
644
645    fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
646        let max_selected = max_selected.to_signal(self.cx);
647        self.bind(max_selected, move |handle| {
648            let max_selected = max_selected.get();
649            handle.modify(|list: &mut List| {
650                list.max_selected.set(max_selected);
651                list.normalize_selection_state();
652            });
653        })
654    }
655
656    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
657        self,
658        flag: impl Res<U> + 'static,
659    ) -> Self {
660        let flag = flag.to_signal(self.cx);
661        self.bind(flag, move |handle| {
662            let selection_follows_focus = flag.get();
663            let s = selection_follows_focus.into();
664            handle.modify(|list: &mut List| list.selection_follows_focus.set(s));
665        })
666    }
667
668    fn horizontal<U: Into<bool> + Clone + 'static>(
669        self,
670        horizontal: impl Res<U> + 'static,
671    ) -> Self {
672        let horizontal = horizontal.to_signal(self.cx);
673        self.bind(horizontal, move |handle| {
674            let horizontal = horizontal.get();
675            let horizontal = horizontal.into();
676            handle.modify(|list: &mut List| {
677                list.orientation.set(if horizontal {
678                    Orientation::Horizontal
679                } else {
680                    Orientation::Vertical
681                });
682            });
683        })
684    }
685
686    fn scroll_to_cursor(self, flag: bool) -> Self {
687        self.modify(|list| {
688            list.scroll_to_cursor.set(flag);
689        })
690    }
691
692    fn on_scroll(
693        self,
694        callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
695    ) -> Self {
696        self.modify(|list: &mut List| list.on_scroll = Some(Box::new(callback)))
697    }
698
699    fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
700        let scrollx = scrollx.to_signal(self.cx);
701        self.bind(scrollx, move |handle| {
702            let scrollx = scrollx.get();
703            let sx = scrollx;
704            handle.modify(|list| {
705                list.scroll_x.set(sx);
706            });
707        })
708    }
709
710    fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self {
711        let scrollx = scrollx.to_signal(self.cx);
712        self.bind(scrollx, move |handle| {
713            let scrolly = scrollx.get();
714            let sy = scrolly;
715            handle.modify(|list| {
716                list.scroll_y.set(sy);
717            });
718        })
719    }
720
721    fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
722        let flag = flag.to_signal(self.cx);
723        self.bind(flag, move |handle| {
724            let show_scrollbar = flag.get();
725            let s = show_scrollbar;
726            handle.modify(|list| {
727                list.show_horizontal_scrollbar.set(s);
728            });
729        })
730    }
731
732    fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
733        let flag = flag.to_signal(self.cx);
734        self.bind(flag, move |handle| {
735            let show_scrollbar = flag.get();
736            let s = show_scrollbar;
737            handle.modify(|list| {
738                list.show_vertical_scrollbar.set(s);
739            });
740        })
741    }
742}
743
744/// A view which represents a selectable item within a list.
745pub struct ListItem {
746    selected: Memo<bool>,
747}
748
749impl ListItem {
750    /// Create a new [ListItem] view.
751    pub fn new<'a, T: Clone + 'static, M: SignalGet<T> + 'static>(
752        cx: &'a mut Context,
753        index: usize,
754        item: M,
755        selection: impl SignalMap<BTreeSet<usize>> + SignalGet<BTreeSet<usize>>,
756        focused: impl SignalMap<Option<usize>>,
757        item_content: impl 'static + Fn(&mut Context, usize, M),
758    ) -> Handle<'a, Self> {
759        let is_focused =
760            focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)).get();
761        let focused_signal =
762            focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index));
763        let is_selected = selection.map(move |selection| selection.contains(&index));
764        Self { selected: is_selected }
765            .build(cx, move |cx| {
766                item_content(cx, index, item);
767            })
768            .role(Role::ListBoxOption)
769            .toggle_class("focused", focused_signal)
770            .checked(selection.map(move |selection| selection.contains(&index)))
771            .bind(focused_signal, move |handle| {
772                let focused = focused_signal.get();
773                if focused != is_focused {
774                    handle.cx.emit(ScrollEvent::ScrollToView(handle.entity()));
775                }
776            })
777            .on_press(move |cx| cx.emit(ListEvent::Select(index)))
778    }
779}
780
781impl View for ListItem {
782    fn element(&self) -> Option<&'static str> {
783        Some("list-item")
784    }
785
786    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
787        event.map(|window_event, _| match window_event {
788            WindowEvent::GeometryChanged(geo) => {
789                if self.selected.get() && geo.contains(GeoChanged::HEIGHT_CHANGED) {
790                    cx.emit(ScrollEvent::ScrollToView(cx.current()));
791                }
792            }
793
794            _ => {}
795        });
796    }
797}