Skip to main content

vizia_core/views/
list.rs

1use std::{
2    collections::BTreeSet,
3    ops::Deref,
4    rc::Rc,
5    time::{Duration, Instant},
6};
7use vizia_reactive::{Scope, SignalGet, SignalWith, UpdaterEffect};
8
9use crate::prelude::*;
10use crate::{binding::BindingHandler, context::SIGNAL_REBUILDS, context::SignalRebuild};
11
12/// Represents how items can be selected in a list.
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
14pub enum Selectable {
15    #[default]
16    /// Items in the list cannot be selected.
17    None,
18    /// A single item in the list can be selected.
19    Single,
20    /// Multiple items in the list can be selected simultaneously.
21    Multi,
22}
23
24impl_res_simple!(Selectable);
25
26/// Events used by the [List] view
27pub enum ListEvent {
28    /// Selects a list item with the given index.
29    Select(usize),
30    /// Selects the focused list item.
31    SelectFocused,
32    /// Moves focus to a specific item index without changing selection.
33    Focus(usize),
34    ///  Moves the focus to the next item in the list.
35    FocusNext,
36    ///  Moves the focus to the previous item in the list.
37    FocusPrev,
38    /// Moves the focus to the first item in the list.
39    FocusFirst,
40    /// Moves the focus to the last item in the list.
41    FocusLast,
42    /// Deselects all items from the list
43    ClearSelection,
44    /// Scrolls the list to the given x and y position.
45    Scroll(f32, f32),
46}
47
48/// A view for creating a list of items from an iterable signal.
49pub struct List {
50    /// The number of items in the list.
51    num_items: usize,
52    /// The set of selected items in the list.
53    selection: Signal<BTreeSet<usize>>,
54    /// Whether the list items are selectable.
55    selectable: Signal<Selectable>,
56    /// The index of the currently focused item in the list.
57    focused: Signal<Option<usize>>,
58    /// Whether the selection should follow the focus.
59    selection_follows_focus: Signal<bool>,
60    /// Whether pressing Space should select the focused item.
61    space_selects_focused: Signal<bool>,
62    /// Minimum number of selected items.
63    min_selected: Signal<usize>,
64    /// Maximum number of selected items.
65    max_selected: Signal<usize>,
66    /// The orientation of the list, either vertical or horizontal.
67    orientation: Signal<Orientation>,
68    /// Whether the scrollview should scroll to the cursor when the scrollbar is pressed.
69    scroll_to_cursor: Signal<bool>,
70    /// Whether the first item should be focused when the list gains focus with no selection.
71    focus_first_item_on_focus_in: Signal<bool>,
72    /// Callback called when a list item is selected.
73    on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
74    /// Callback called when the focused list item changes.
75    on_focus: Option<Box<dyn Fn(&mut EventContext, usize)>>,
76    /// Callback called when the scrollview is scrolled.
77    on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
78    /// The horizontal scroll position of the list.
79    scroll_x: Signal<f32>,
80    /// The vertical scroll position of the list.
81    scroll_y: Signal<f32>,
82    /// Whether the horizontal scrollbar should be visible.
83    show_horizontal_scrollbar: Signal<bool>,
84    /// Whether the vertical scrollbar should be visible.
85    show_vertical_scrollbar: Signal<bool>,
86    /// Whether focused list items should show focus visibility.
87    focus_visibility: Signal<bool>,
88    /// Returns the searchable text for each item index when type-ahead is enabled.
89    type_ahead_text: Option<Box<dyn Fn(&mut EventContext, usize) -> Option<String>>>,
90    /// Buffered type-ahead query built from rapid character input.
91    type_ahead_buffer: String,
92    /// Timestamp of the last accepted type-ahead character.
93    type_ahead_last_input: Option<Instant>,
94    /// Maximum elapsed time before resetting type-ahead buffer.
95    type_ahead_timeout: Duration,
96}
97
98/// A binding handler that manages list item entities for a [List].
99///
100/// The user provides `Vec<T>` wrapped in an outer signal.
101/// This handler creates internal signals for each item and maintains them.
102/// Value changes to existing items update their internal signals (zero entity rebuilds).
103/// Structural changes (add/remove/reorder) are handled by diffing values and rebuilding from the first changed position.
104struct ListItemsBinding<T: 'static> {
105    entity: Entity,
106    list_entity: Entity,
107    get_fn: Box<dyn Fn() -> Vec<T>>,
108    item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
109    selection: Signal<BTreeSet<usize>>,
110    focused: Signal<Option<usize>>,
111    focus_visibility: Signal<bool>,
112    /// Internal signals for each list item.
113    item_signals: Vec<Signal<T>>,
114    /// Entity IDs of the ListItem views.
115    item_entities: Vec<Entity>,
116    /// Previous values, used for value-based diffing.
117    prev_values: Vec<T>,
118    scope: Scope,
119}
120
121/// A binding handler that manages caller-provided list item entities for a [List].
122///
123/// This variant keeps list diffing and metadata updates but does not wrap items in [ListItem],
124/// allowing callers to provide their own semantics and interaction behavior.
125struct CustomListItemsBinding<T: 'static> {
126    entity: Entity,
127    list_entity: Entity,
128    get_fn: Box<dyn Fn() -> Vec<T>>,
129    item_content: Rc<dyn for<'a> Fn(&'a mut Context, usize, Signal<T>, Memo<bool>) -> Entity>,
130    selection: Signal<BTreeSet<usize>>,
131    /// Internal signals for each list item.
132    item_signals: Vec<Signal<T>>,
133    /// Entity IDs of caller-built item views.
134    item_entities: Vec<Entity>,
135    /// Previous values, used for value-based diffing.
136    prev_values: Vec<T>,
137    scope: Scope,
138}
139
140impl<T: PartialEq + Clone + 'static> ListItemsBinding<T> {
141    fn create<S, V>(
142        cx: &mut Context,
143        list_entity: Entity,
144        list: S,
145        selection: Signal<BTreeSet<usize>>,
146        focused: Signal<Option<usize>>,
147        focus_visibility: Signal<bool>,
148        item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
149    ) where
150        S: SignalGet<V> + SignalWith<V> + Copy + 'static,
151        V: Deref<Target = [T]> + Clone + 'static,
152    {
153        let entity = cx.entity_manager.create();
154        let context_id = cx.context_id;
155        cx.tree.add(entity, cx.current()).expect("Failed to add to tree");
156        cx.tree.set_ignored(entity, true);
157
158        let scope = Scope::new();
159        let initial_values: Vec<T> = scope.enter(|| {
160            UpdaterEffect::new(
161                move || list.with(|list| list.deref().to_vec()),
162                move |_new_value| {
163                    SIGNAL_REBUILDS.with_borrow_mut(|set| {
164                        set.insert(SignalRebuild { context_id, entity });
165                    });
166                },
167            )
168        });
169
170        let mut binding = Self {
171            entity,
172            list_entity,
173            get_fn: Box::new(move || list.with_untracked(|list| list.deref().to_vec())),
174            item_content,
175            selection,
176            focused,
177            focus_visibility,
178            item_signals: Vec::new(),
179            item_entities: Vec::new(),
180            prev_values: Vec::new(),
181            scope,
182        };
183
184        // Build initial items.
185        for (index, value) in initial_values.iter().enumerate() {
186            let signal = Signal::new(value.clone());
187            let entity = binding.create_item_entity(cx, index, signal);
188            binding.item_signals.push(signal);
189            binding.item_entities.push(entity);
190            binding.prev_values.push(value.clone());
191        }
192        binding.update_list_metadata(cx, initial_values.len());
193
194        cx.bindings.insert(entity, Box::new(binding));
195
196        let _: Handle<Self> =
197            Handle { current: entity, entity, p: Default::default(), cx }.ignore();
198    }
199
200    fn update_list_metadata(&self, cx: &mut Context, len: usize) {
201        if let Some(view) = cx.views.get_mut(&self.list_entity) {
202            if let Some(list) = view.downcast_mut::<List>() {
203                list.num_items = len;
204                list.normalize_selection_state();
205            }
206        }
207    }
208
209    fn create_item_entity(&self, cx: &mut Context, index: usize, signal: Signal<T>) -> Entity {
210        let mut created = Entity::null();
211        let item_content = self.item_content.clone();
212        let selection = self.selection;
213        let focused = self.focused;
214        let focus_visibility = self.focus_visibility;
215
216        cx.with_current(self.entity, |cx| {
217            created = ListItem::new(cx, index, signal, selection, focused, focus_visibility, {
218                let item_content = item_content.clone();
219                move |cx, index, item| (item_content)(cx, index, item)
220            })
221            .entity();
222        });
223
224        created
225    }
226}
227
228impl<T: PartialEq + Clone + 'static> BindingHandler for ListItemsBinding<T> {
229    fn update(&mut self, cx: &mut Context) {
230        let new_values = (self.get_fn)();
231        let new_len = new_values.len();
232
233        // Find the first position where values differ.
234        let first_diff = self
235            .prev_values
236            .iter()
237            .zip(new_values.iter())
238            .position(|(old, new)| old != new)
239            .unwrap_or(self.prev_values.len().min(new_len));
240
241        // Remove all entities from first_diff onward.
242        for entity in self.item_entities.drain(first_diff..) {
243            cx.remove(entity);
244        }
245        self.item_signals.truncate(first_diff);
246
247        // Update existing signals or create new items from first_diff onward.
248        for (i, value) in new_values[first_diff..].iter().enumerate() {
249            let index = first_diff + i;
250            if index < self.item_signals.len() {
251                // Update existing signal
252                self.item_signals[index].set(value.clone());
253            } else {
254                // Create new signal and item
255                let signal = Signal::new(value.clone());
256                let entity = self.create_item_entity(cx, index, signal);
257                self.item_signals.push(signal);
258                self.item_entities.push(entity);
259            }
260        }
261
262        self.prev_values = new_values;
263        self.update_list_metadata(cx, new_len);
264    }
265
266    fn remove(&self, _cx: &mut Context) {
267        self.scope.dispose();
268    }
269
270    fn debug(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
271        f.write_str("ListItemsBinding")
272    }
273}
274
275impl<T: PartialEq + Clone + 'static> CustomListItemsBinding<T> {
276    fn create<S, V>(
277        cx: &mut Context,
278        list_entity: Entity,
279        list: S,
280        selection: Signal<BTreeSet<usize>>,
281        item_content: Rc<dyn for<'a> Fn(&'a mut Context, usize, Signal<T>, Memo<bool>) -> Entity>,
282    ) where
283        S: SignalGet<V> + SignalWith<V> + Copy + 'static,
284        V: Deref<Target = [T]> + Clone + 'static,
285    {
286        let entity = cx.entity_manager.create();
287        let context_id = cx.context_id;
288        cx.tree.add(entity, cx.current()).expect("Failed to add to tree");
289        cx.tree.set_ignored(entity, true);
290
291        let scope = Scope::new();
292        let initial_values: Vec<T> = scope.enter(|| {
293            UpdaterEffect::new(
294                move || list.with(|list| list.deref().to_vec()),
295                move |_new_value| {
296                    SIGNAL_REBUILDS.with_borrow_mut(|set| {
297                        set.insert(SignalRebuild { context_id, entity });
298                    });
299                },
300            )
301        });
302
303        let mut binding = Self {
304            entity,
305            list_entity,
306            get_fn: Box::new(move || list.with_untracked(|list| list.deref().to_vec())),
307            item_content,
308            selection,
309            item_signals: Vec::new(),
310            item_entities: Vec::new(),
311            prev_values: Vec::new(),
312            scope,
313        };
314
315        // Build initial items.
316        for (index, value) in initial_values.iter().enumerate() {
317            let signal = Signal::new(value.clone());
318            let entity = binding.create_item_entity(cx, index, signal);
319            binding.item_signals.push(signal);
320            binding.item_entities.push(entity);
321            binding.prev_values.push(value.clone());
322        }
323        binding.update_list_metadata(cx, initial_values.len());
324
325        cx.bindings.insert(entity, Box::new(binding));
326
327        let _: Handle<Self> =
328            Handle { current: entity, entity, p: Default::default(), cx }.ignore();
329    }
330
331    fn update_list_metadata(&self, cx: &mut Context, len: usize) {
332        if let Some(view) = cx.views.get_mut(&self.list_entity) {
333            if let Some(list) = view.downcast_mut::<List>() {
334                list.num_items = len;
335                list.normalize_selection_state();
336            }
337        }
338    }
339
340    fn create_item_entity(&self, cx: &mut Context, index: usize, signal: Signal<T>) -> Entity {
341        let item_content = self.item_content.clone();
342        let selection = self.selection;
343        let mut created = Entity::null();
344
345        cx.with_current(self.entity, |cx| {
346            let is_selected = selection.map(move |selection| selection.contains(&index));
347            created = (item_content)(cx, index, signal, is_selected);
348        });
349
350        created
351    }
352}
353
354impl<T: PartialEq + Clone + 'static> BindingHandler for CustomListItemsBinding<T> {
355    fn update(&mut self, cx: &mut Context) {
356        let new_values = (self.get_fn)();
357        let new_len = new_values.len();
358
359        // Find the first position where values differ.
360        let first_diff = self
361            .prev_values
362            .iter()
363            .zip(new_values.iter())
364            .position(|(old, new)| old != new)
365            .unwrap_or(self.prev_values.len().min(new_len));
366
367        // Remove all entities from first_diff onward.
368        for entity in self.item_entities.drain(first_diff..) {
369            cx.remove(entity);
370        }
371        self.item_signals.truncate(first_diff);
372
373        // Update existing signals or create new items from first_diff onward.
374        for (i, value) in new_values[first_diff..].iter().enumerate() {
375            let index = first_diff + i;
376            if index < self.item_signals.len() {
377                self.item_signals[index].set(value.clone());
378            } else {
379                let signal = Signal::new(value.clone());
380                let entity = self.create_item_entity(cx, index, signal);
381                self.item_signals.push(signal);
382                self.item_entities.push(entity);
383            }
384        }
385
386        self.prev_values = new_values;
387        self.update_list_metadata(cx, new_len);
388    }
389
390    fn remove(&self, _cx: &mut Context) {
391        self.scope.dispose();
392    }
393
394    fn debug(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
395        f.write_str("CustomListItemsBinding")
396    }
397}
398
399impl List {
400    fn set_focused_with_callback(&mut self, cx: &mut EventContext, focused: Option<usize>) {
401        let previous = self.focused.get();
402        self.focused.set(focused);
403
404        if previous != focused {
405            if let (Some(index), Some(callback)) = (focused, self.on_focus.as_ref()) {
406                callback(cx, index);
407            }
408        }
409    }
410
411    fn find_type_ahead_match(
412        &self,
413        cx: &mut EventContext,
414        query: &str,
415        start_index: usize,
416    ) -> Option<usize> {
417        let get_text = self.type_ahead_text.as_ref()?;
418        if self.num_items == 0 {
419            return None;
420        }
421
422        for offset in 0..self.num_items {
423            let index = (start_index + offset) % self.num_items;
424            let item_text = get_text(cx, index)
425                .map(|text| text.trim_start().to_lowercase())
426                .unwrap_or_default();
427
428            if !item_text.is_empty() && item_text.starts_with(query) {
429                return Some(index);
430            }
431        }
432
433        None
434    }
435
436    fn try_type_ahead(&mut self, cx: &mut EventContext, typed: char) -> bool {
437        if self.type_ahead_text.is_none() || self.num_items == 0 {
438            return false;
439        }
440
441        if typed.is_control() || typed.is_whitespace() {
442            return false;
443        }
444
445        let now = Instant::now();
446        let within_timeout = self
447            .type_ahead_last_input
448            .is_some_and(|last| now.saturating_duration_since(last) <= self.type_ahead_timeout);
449
450        let ch = typed.to_lowercase().collect::<String>();
451        let query = if within_timeout {
452            let repeated_char_cycle = !self.type_ahead_buffer.is_empty()
453                && self.type_ahead_buffer.chars().all(|c| c == typed.to_ascii_lowercase());
454
455            if repeated_char_cycle {
456                ch.clone()
457            } else {
458                format!("{}{}", self.type_ahead_buffer, ch)
459            }
460        } else {
461            ch.clone()
462        };
463
464        let start_index =
465            self.focused.get().map(|focused| (focused + 1) % self.num_items).unwrap_or(0);
466
467        if let Some(index) = self.find_type_ahead_match(cx, &query, start_index) {
468            self.type_ahead_buffer = query;
469            self.type_ahead_last_input = Some(now);
470            self.focus_visibility.set(true);
471            self.set_focused_with_callback(cx, Some(index));
472
473            if self.selection_follows_focus.get() {
474                cx.emit(ListEvent::SelectFocused);
475            }
476
477            true
478        } else {
479            self.type_ahead_buffer.clear();
480            self.type_ahead_last_input = Some(now);
481            false
482        }
483    }
484
485    fn selection_limits(&self) -> (usize, usize) {
486        let mut min_selected = self.min_selected.get();
487        let mut max_selected = self.max_selected.get();
488
489        match self.selectable.get() {
490            Selectable::None => {
491                min_selected = 0;
492                max_selected = 0;
493            }
494
495            Selectable::Single => {
496                min_selected = min_selected.min(1);
497                max_selected = 1;
498            }
499
500            Selectable::Multi => {}
501        }
502
503        max_selected = max_selected.min(self.num_items);
504        min_selected = min_selected.min(max_selected);
505
506        (min_selected, max_selected)
507    }
508
509    fn normalize_selection_state(&mut self) {
510        let (min_selected, max_selected) = self.selection_limits();
511
512        let mut selection = self.selection.get();
513        selection.retain(|index| *index < self.num_items);
514
515        while selection.len() > max_selected {
516            if let Some(last) = selection.iter().next_back().copied() {
517                selection.remove(&last);
518            } else {
519                break;
520            }
521        }
522
523        if selection.len() < min_selected {
524            for index in 0..self.num_items {
525                selection.insert(index);
526                if selection.len() >= min_selected {
527                    break;
528                }
529            }
530        }
531
532        let mut focused = self.focused.get();
533        if focused.is_some_and(|index| index >= self.num_items) {
534            focused = self.num_items.checked_sub(1);
535        }
536
537        self.selection.set(selection);
538        self.focused.set(focused);
539    }
540
541    /// Creates a new [List] view from a reactive or static list of values.
542    ///
543    /// `list` accepts any [`Res<V>`] source where `V` derefs to `[T]` — for example a
544    /// `Signal<Vec<T>>` for a reactive list, or a plain `Vec<T>` for a static list.
545    /// The list creates and manages internal signals for each item automatically.
546    /// Value changes to existing items update their internal signals with zero entity rebuilds.
547    /// Structural changes (add/remove/reorder) are handled by diffing values and rebuilding from the first changed position.
548    pub fn new<S, V, T>(
549        cx: &mut Context,
550        list: S,
551        item_content: impl 'static + Fn(&mut Context, usize, Signal<T>),
552    ) -> Handle<Self>
553    where
554        S: Res<V> + 'static,
555        V: Deref<Target = [T]> + Clone + 'static,
556        T: PartialEq + Clone + 'static,
557    {
558        let content: Rc<dyn Fn(&mut Context, usize, Signal<T>)> = Rc::new(item_content);
559        let selection = Signal::new(BTreeSet::default());
560        let selectable = Signal::new(Selectable::None);
561        let focused = Signal::new(None);
562        let min_selected = Signal::new(0);
563        let max_selected = Signal::new(usize::MAX);
564        let orientation = Signal::new(Orientation::Vertical);
565        let scroll_to_cursor = Signal::new(false);
566        let focus_first_item_on_focus_in = Signal::new(true);
567        let scroll_x = Signal::new(0.0);
568        let scroll_y = Signal::new(0.0);
569        let show_horizontal_scrollbar = Signal::new(true);
570        let show_vertical_scrollbar = Signal::new(true);
571        let focus_visibility = Signal::new(false);
572
573        Self {
574            num_items: 0,
575            selection,
576            selectable,
577            focused,
578            selection_follows_focus: Signal::new(false),
579            space_selects_focused: Signal::new(true),
580            min_selected,
581            max_selected,
582            orientation,
583            scroll_to_cursor,
584            focus_first_item_on_focus_in,
585            on_select: None,
586            on_focus: None,
587            on_scroll: None,
588            scroll_x,
589            scroll_y,
590            show_horizontal_scrollbar,
591            show_vertical_scrollbar,
592            focus_visibility,
593            type_ahead_text: None,
594            type_ahead_buffer: String::new(),
595            type_ahead_last_input: None,
596            type_ahead_timeout: Duration::from_millis(1000),
597        }
598        .build(cx, move |cx| {
599            let list_entity = cx.current();
600
601            Keymap::from(vec![
602                (
603                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
604                    KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
605                ),
606                (
607                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
608                    KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
609                ),
610                (
611                    KeyChord::new(Modifiers::empty(), Code::Home),
612                    KeymapEntry::new("Focus First", |cx| cx.emit(ListEvent::FocusFirst)),
613                ),
614                (
615                    KeyChord::new(Modifiers::empty(), Code::End),
616                    KeymapEntry::new("Focus Last", |cx| cx.emit(ListEvent::FocusLast)),
617                ),
618                (
619                    KeyChord::new(Modifiers::empty(), Code::Enter),
620                    KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
621                ),
622            ])
623            .build(cx);
624
625            Binding::new(cx, orientation, move |cx| {
626                let orientation = orientation.get();
627                if orientation == Orientation::Horizontal {
628                    cx.emit(KeymapEvent::RemoveAction(
629                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
630                        "Focus Next",
631                    ));
632
633                    cx.emit(KeymapEvent::RemoveAction(
634                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
635                        "Focus Previous",
636                    ));
637
638                    cx.emit(KeymapEvent::InsertAction(
639                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
640                        KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
641                    ));
642
643                    cx.emit(KeymapEvent::InsertAction(
644                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
645                        KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
646                    ));
647                } else {
648                    cx.emit(KeymapEvent::RemoveAction(
649                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
650                        "Focus Next",
651                    ));
652
653                    cx.emit(KeymapEvent::RemoveAction(
654                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
655                        "Focus Previous",
656                    ));
657
658                    cx.emit(KeymapEvent::InsertAction(
659                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
660                        KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
661                    ));
662
663                    cx.emit(KeymapEvent::InsertAction(
664                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
665                        KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
666                    ));
667                }
668            });
669
670            let list_signal = list.to_signal(cx);
671            ScrollView::new(cx, move |cx| {
672                ListItemsBinding::create(
673                    cx,
674                    list_entity,
675                    list_signal,
676                    selection,
677                    focused,
678                    focus_visibility,
679                    content.clone(),
680                );
681            })
682            .show_horizontal_scrollbar(show_horizontal_scrollbar)
683            .show_vertical_scrollbar(show_vertical_scrollbar)
684            .scroll_to_cursor(scroll_to_cursor)
685            .scroll_x(scroll_x)
686            .scroll_y(scroll_y)
687            .on_scroll(|cx, x, y| {
688                if y.is_finite() {
689                    cx.emit(ListEvent::Scroll(x, y));
690                }
691            });
692        })
693        .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
694        .multiselectable(selectable.map(|s| *s == Selectable::Multi))
695        .orientation(orientation)
696        .navigable(true)
697        .role(Role::ListBox)
698    }
699
700    /// Creates a new [List] view from a reactive or static list of values using caller-provided
701    /// item views directly, without wrapping each item in a [ListItem].
702    ///
703    /// This keeps the list's diffing, keyboard, focus, and scrolling behavior while allowing
704    /// custom item semantics.
705    pub fn new_custom_items<S, V, T, H>(
706        cx: &mut Context,
707        list: S,
708        item_content: impl 'static + for<'a> Fn(&'a mut Context, usize, Signal<T>) -> Handle<'a, H>,
709    ) -> Handle<Self>
710    where
711        S: Res<V> + 'static,
712        V: Deref<Target = [T]> + Clone + 'static,
713        T: PartialEq + Clone + 'static,
714        H: View,
715    {
716        Self::new_custom_items_with_selection(cx, list, move |cx, index, item, _is_selected| {
717            item_content(cx, index, item)
718        })
719    }
720
721    /// Creates a new [List] view from a reactive or static list of values using caller-provided
722    /// item views directly, without wrapping each item in a [ListItem], and provides each item
723    /// with a memo of whether it is currently selected in this list.
724    pub fn new_custom_items_with_selection<S, V, T, H>(
725        cx: &mut Context,
726        list: S,
727        item_content: impl 'static
728        + for<'a> Fn(&'a mut Context, usize, Signal<T>, Memo<bool>) -> Handle<'a, H>,
729    ) -> Handle<Self>
730    where
731        S: Res<V> + 'static,
732        V: Deref<Target = [T]> + Clone + 'static,
733        T: PartialEq + Clone + 'static,
734        H: View,
735    {
736        let selection = Signal::new(BTreeSet::default());
737        let selectable = Signal::new(Selectable::None);
738        let focused = Signal::new(None);
739        let min_selected = Signal::new(0);
740        let max_selected = Signal::new(usize::MAX);
741        let orientation = Signal::new(Orientation::Vertical);
742        let scroll_to_cursor = Signal::new(false);
743        let focus_first_item_on_focus_in = Signal::new(true);
744        let scroll_x = Signal::new(0.0);
745        let scroll_y = Signal::new(0.0);
746        let show_horizontal_scrollbar = Signal::new(true);
747        let show_vertical_scrollbar = Signal::new(true);
748        let focus_visibility = Signal::new(false);
749
750        let content: Rc<dyn for<'a> Fn(&'a mut Context, usize, Signal<T>, Memo<bool>) -> Entity> =
751            Rc::new(move |cx, index, item, is_selected| {
752                let is_focused = focused.map(move |focused| focused.is_some_and(|f| f == index));
753                item_content(cx, index, item, is_selected)
754                    .focusable(true)
755                    .navigable(false)
756                    .focused_with_visibility(is_focused, focus_visibility)
757                    .entity()
758            });
759
760        Self {
761            num_items: 0,
762            selection,
763            selectable,
764            focused,
765            selection_follows_focus: Signal::new(false),
766            space_selects_focused: Signal::new(true),
767            min_selected,
768            max_selected,
769            orientation,
770            scroll_to_cursor,
771            focus_first_item_on_focus_in,
772            on_select: None,
773            on_focus: None,
774            on_scroll: None,
775            scroll_x,
776            scroll_y,
777            show_horizontal_scrollbar,
778            show_vertical_scrollbar,
779            focus_visibility,
780            type_ahead_text: None,
781            type_ahead_buffer: String::new(),
782            type_ahead_last_input: None,
783            type_ahead_timeout: Duration::from_millis(1000),
784        }
785        .build(cx, move |cx| {
786            let list_entity = cx.current();
787
788            Keymap::from(vec![
789                (
790                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
791                    KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
792                ),
793                (
794                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
795                    KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
796                ),
797                (
798                    KeyChord::new(Modifiers::empty(), Code::Home),
799                    KeymapEntry::new("Focus First", |cx| cx.emit(ListEvent::FocusFirst)),
800                ),
801                (
802                    KeyChord::new(Modifiers::empty(), Code::End),
803                    KeymapEntry::new("Focus Last", |cx| cx.emit(ListEvent::FocusLast)),
804                ),
805                (
806                    KeyChord::new(Modifiers::empty(), Code::Enter),
807                    KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
808                ),
809            ])
810            .build(cx);
811
812            Binding::new(cx, orientation, move |cx| {
813                let orientation = orientation.get();
814                if orientation == Orientation::Horizontal {
815                    cx.emit(KeymapEvent::RemoveAction(
816                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
817                        "Focus Next",
818                    ));
819
820                    cx.emit(KeymapEvent::RemoveAction(
821                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
822                        "Focus Previous",
823                    ));
824
825                    cx.emit(KeymapEvent::InsertAction(
826                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
827                        KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
828                    ));
829
830                    cx.emit(KeymapEvent::InsertAction(
831                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
832                        KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
833                    ));
834                } else {
835                    cx.emit(KeymapEvent::RemoveAction(
836                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
837                        "Focus Next",
838                    ));
839
840                    cx.emit(KeymapEvent::RemoveAction(
841                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
842                        "Focus Previous",
843                    ));
844
845                    cx.emit(KeymapEvent::InsertAction(
846                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
847                        KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
848                    ));
849
850                    cx.emit(KeymapEvent::InsertAction(
851                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
852                        KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
853                    ));
854                }
855            });
856
857            let list_signal = list.to_signal(cx);
858            ScrollView::new(cx, move |cx| {
859                CustomListItemsBinding::create(
860                    cx,
861                    list_entity,
862                    list_signal,
863                    selection,
864                    content.clone(),
865                );
866            })
867            .show_horizontal_scrollbar(show_horizontal_scrollbar)
868            .show_vertical_scrollbar(show_vertical_scrollbar)
869            .scroll_to_cursor(scroll_to_cursor)
870            .scroll_x(scroll_x)
871            .scroll_y(scroll_y)
872            .on_scroll(|cx, x, y| {
873                if y.is_finite() {
874                    cx.emit(ListEvent::Scroll(x, y));
875                }
876            });
877        })
878        .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
879        .multiselectable(selectable.map(|s| *s == Selectable::Multi))
880        .orientation(orientation)
881        .navigable(true)
882    }
883}
884
885impl View for List {
886    fn element(&self) -> Option<&'static str> {
887        Some("list")
888    }
889
890    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
891        event.map(|window_event, meta| {
892            match window_event {
893                WindowEvent::Press { mouse } => {
894                    self.focus_visibility.set(!*mouse);
895                }
896
897                WindowEvent::FocusIn if meta.target == cx.current() => {
898                    // Focus events originating at root are generated by keyboard navigation
899                    // (e.g. Tab/Shift+Tab), so preserve visible focus in that case.
900                    if meta.origin == Entity::root() {
901                        self.focus_visibility.set(true);
902                    }
903
904                    let next_focused = focus_index_on_focus_in(
905                        &self.selection.get(),
906                        self.num_items,
907                        self.focus_first_item_on_focus_in.get(),
908                    );
909
910                    if let Some(index) = next_focused {
911                        // Force a transition so focused_with_visibility re-applies focus
912                        // to the list item when focus enters the list container.
913                        if self.focused.get() == Some(index) {
914                            self.focused.set(None);
915                        }
916                        self.set_focused_with_callback(cx, Some(index));
917                    }
918                }
919
920                WindowEvent::CharInput(c) => {
921                    if *c == ' ' && meta.target == cx.current() && self.space_selects_focused.get()
922                    {
923                        cx.emit(ListEvent::SelectFocused);
924                        meta.consume();
925                    } else if self.try_type_ahead(cx, *c) {
926                        meta.consume();
927                    }
928                }
929
930                _ => {}
931            }
932        });
933
934        event.take(|list_event, meta| match list_event {
935            ListEvent::Select(index) => {
936                let selectable = self.selectable.get();
937                let (min_selected, max_selected) = self.selection_limits();
938                let mut selection = self.selection.get();
939                let mut focused = self.focused.get();
940                match selectable {
941                    Selectable::Single => {
942                        if selection.contains(&index) {
943                            if min_selected == 0 {
944                                selection.clear();
945                                focused = None;
946                            }
947                        } else {
948                            selection.clear();
949                            selection.insert(index);
950                            focused = Some(index);
951                            if let Some(on_select) = &self.on_select {
952                                on_select(cx, index);
953                            }
954                        }
955                    }
956
957                    Selectable::Multi => {
958                        // In multi-select mode, clicking an item should move focus to that item
959                        // regardless of whether the click selects or deselects it.
960                        focused = Some(index);
961
962                        if selection.contains(&index) {
963                            if selection.len() > min_selected {
964                                selection.remove(&index);
965                                if let Some(on_select) = &self.on_select {
966                                    on_select(cx, index);
967                                }
968                            }
969                        } else if selection.len() < max_selected {
970                            selection.insert(index);
971                            if let Some(on_select) = &self.on_select {
972                                on_select(cx, index);
973                            }
974                        }
975                    }
976
977                    Selectable::None => {}
978                }
979
980                self.selection.set(selection);
981                self.set_focused_with_callback(cx, focused);
982
983                meta.consume();
984            }
985
986            ListEvent::SelectFocused => {
987                if let Some(focused) = self.focused.get() {
988                    self.focus_visibility.set(true);
989                    cx.emit(ListEvent::Select(focused))
990                }
991                meta.consume();
992            }
993
994            ListEvent::Focus(index) => {
995                if index < self.num_items {
996                    self.focus_visibility.set(true);
997                    self.set_focused_with_callback(cx, Some(index));
998                }
999
1000                meta.consume();
1001            }
1002
1003            ListEvent::ClearSelection => {
1004                let (min_selected, _) = self.selection_limits();
1005                if min_selected == 0 {
1006                    self.selection.set(BTreeSet::default());
1007                }
1008                meta.consume();
1009            }
1010
1011            ListEvent::FocusNext => {
1012                println!(
1013                    "FocusNext received, num_items: {}, focused: {:?}",
1014                    self.num_items,
1015                    self.focused.get()
1016                );
1017                let mut focused = self.focused.get();
1018                let mut moved_focus = false;
1019                if let Some(f) = &mut focused {
1020                    if *f < self.num_items.saturating_sub(1) {
1021                        *f = f.saturating_add(1);
1022                        moved_focus = true;
1023                        if self.selection_follows_focus.get() {
1024                            cx.emit(ListEvent::SelectFocused);
1025                        }
1026                    }
1027                } else {
1028                    focused = Some(0);
1029                    moved_focus = true;
1030                    if self.selection_follows_focus.get() {
1031                        cx.emit(ListEvent::SelectFocused);
1032                    }
1033                }
1034
1035                if moved_focus {
1036                    self.focus_visibility.set(true);
1037                }
1038
1039                self.set_focused_with_callback(cx, focused);
1040
1041                meta.consume();
1042            }
1043
1044            ListEvent::FocusPrev => {
1045                let mut focused = self.focused.get();
1046                let mut moved_focus = false;
1047                if let Some(f) = &mut focused {
1048                    if *f > 0 {
1049                        *f = f.saturating_sub(1);
1050                        moved_focus = true;
1051                        if self.selection_follows_focus.get() {
1052                            cx.emit(ListEvent::SelectFocused);
1053                        }
1054                    }
1055                } else {
1056                    focused = Some(self.num_items.saturating_sub(1));
1057                    moved_focus = true;
1058                    if self.selection_follows_focus.get() {
1059                        cx.emit(ListEvent::SelectFocused);
1060                    }
1061                }
1062
1063                if moved_focus {
1064                    self.focus_visibility.set(true);
1065                }
1066
1067                self.set_focused_with_callback(cx, focused);
1068
1069                meta.consume();
1070            }
1071
1072            ListEvent::FocusFirst => {
1073                if self.num_items > 0 {
1074                    self.focus_visibility.set(true);
1075                    self.set_focused_with_callback(cx, Some(0));
1076                    if self.selection_follows_focus.get() {
1077                        cx.emit(ListEvent::SelectFocused);
1078                    }
1079                }
1080
1081                meta.consume();
1082            }
1083
1084            ListEvent::FocusLast => {
1085                if self.num_items > 0 {
1086                    self.focus_visibility.set(true);
1087                    self.set_focused_with_callback(cx, Some(self.num_items.saturating_sub(1)));
1088                    if self.selection_follows_focus.get() {
1089                        cx.emit(ListEvent::SelectFocused);
1090                    }
1091                }
1092
1093                meta.consume();
1094            }
1095
1096            ListEvent::Scroll(x, y) => {
1097                self.scroll_x.set(x);
1098                self.scroll_y.set(y);
1099                if let Some(callback) = &self.on_scroll {
1100                    (callback)(cx, x, y);
1101                }
1102
1103                meta.consume();
1104            }
1105        })
1106    }
1107}
1108
1109/// Modifiers for changing the behavior and selection state of a [List].
1110pub trait ListModifiers: Sized {
1111    /// Sets the selected items of the list from signal of type indices.
1112    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1113    where
1114        R: Deref<Target = [usize]> + Clone + 'static;
1115
1116    /// Sets the focused item of the list from a signal of an optional index.
1117    fn focused_index(self, focused: impl Res<Option<usize>> + 'static) -> Self;
1118
1119    /// Sets the callback triggered when a [ListItem] is selected.
1120    fn on_select<F>(self, callback: F) -> Self
1121    where
1122        F: 'static + Fn(&mut EventContext, usize);
1123
1124    /// Sets the callback triggered when a [ListItem] receives focus.
1125    fn on_focus<F>(self, callback: F) -> Self
1126    where
1127        F: 'static + Fn(&mut EventContext, usize);
1128
1129    /// Set the selectable state of the [List].
1130    fn selectable<U: Into<Selectable> + Clone + 'static>(
1131        self,
1132        selectable: impl Res<U> + 'static,
1133    ) -> Self;
1134
1135    /// Sets the minimum number of selected items.
1136    fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self;
1137
1138    /// Sets the maximum number of selected items.
1139    fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self;
1140
1141    /// Sets whether the selection should follow the focus.
1142    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1143        self,
1144        flag: impl Res<U> + 'static,
1145    ) -> Self;
1146
1147    /// Sets whether pressing Space should select the currently focused item.
1148    fn space_selects_focused<U: Into<bool> + Clone + 'static>(
1149        self,
1150        flag: impl Res<U> + 'static,
1151    ) -> Self;
1152
1153    /// Sets the orientation of the list.
1154    fn horizontal<U: Into<bool> + Clone + 'static>(self, horizontal: impl Res<U> + 'static)
1155    -> Self;
1156
1157    /// Sets whether the scrollbar should move to the cursor when pressed.
1158    fn scroll_to_cursor(self, flag: bool) -> Self;
1159
1160    /// Sets whether the first item should be focused when the list gains focus with no selection.
1161    fn focus_first_item_on_focus_in(self, flag: impl Res<bool> + 'static) -> Self;
1162
1163    /// Sets a callback which will be called when a scrollview is scrolled, either with the mouse wheel, touchpad, or using the scroll bars.
1164    fn on_scroll(
1165        self,
1166        callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
1167    ) -> Self;
1168
1169    /// Set the horizontal scroll position of the [ScrollView]. Accepts a value or signal of type an `f32` between 0 and 1.
1170    fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self;
1171
1172    /// Set the vertical scroll position of the [ScrollView]. Accepts a value or signal of type an `f32` between 0 and 1.
1173    fn scroll_y(self, scrolly: impl Res<f32> + 'static) -> Self;
1174
1175    /// Sets whether the horizontal scrollbar should be visible.
1176    fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
1177
1178    /// Sets whether the vertical scrollbar should be visible.
1179    fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
1180
1181    /// Enables type-ahead navigation by providing searchable text per item index.
1182    fn type_ahead_text<F>(self, callback: F) -> Self
1183    where
1184        F: 'static + Fn(&mut EventContext, usize) -> Option<String>;
1185}
1186
1187impl ListModifiers for Handle<'_, List> {
1188    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1189    where
1190        R: Deref<Target = [usize]> + Clone + 'static,
1191    {
1192        let selection = selection.to_signal(self.cx);
1193        self.bind(selection, move |handle| {
1194            selection.with(|selected_indices| {
1195                handle.modify(|list| {
1196                    let previous_focused = list.focused.get();
1197                    let mut selection = BTreeSet::default();
1198                    for idx in selected_indices.deref().iter().copied() {
1199                        selection.insert(idx);
1200                    }
1201
1202                    let focused = previous_focused
1203                        .filter(|idx| *idx < list.num_items)
1204                        .or_else(|| selection.iter().next_back().copied());
1205
1206                    list.selection.set(selection);
1207                    list.focused.set(focused);
1208                    list.normalize_selection_state();
1209                });
1210            });
1211        })
1212    }
1213
1214    fn focused_index(self, focused: impl Res<Option<usize>> + 'static) -> Self {
1215        let focused = focused.to_signal(self.cx);
1216        self.bind(focused, move |handle| {
1217            let focused = focused.get();
1218            handle.modify(|list| {
1219                list.focused.set(normalize_focused_index(focused, list.num_items));
1220            });
1221        })
1222    }
1223
1224    fn on_select<F>(self, callback: F) -> Self
1225    where
1226        F: 'static + Fn(&mut EventContext, usize),
1227    {
1228        self.modify(|list: &mut List| list.on_select = Some(Box::new(callback)))
1229    }
1230
1231    fn on_focus<F>(self, callback: F) -> Self
1232    where
1233        F: 'static + Fn(&mut EventContext, usize),
1234    {
1235        self.modify(|list: &mut List| list.on_focus = Some(Box::new(callback)))
1236    }
1237
1238    fn selectable<U: Into<Selectable> + Clone + 'static>(
1239        self,
1240        selectable: impl Res<U> + 'static,
1241    ) -> Self {
1242        let selectable = selectable.to_signal(self.cx);
1243        self.bind(selectable, move |handle| {
1244            let selectable = selectable.get();
1245            let s = selectable.into();
1246            handle.modify(|list: &mut List| {
1247                list.selectable.set(s);
1248                list.normalize_selection_state();
1249            });
1250        })
1251    }
1252
1253    fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
1254        let min_selected = min_selected.to_signal(self.cx);
1255        self.bind(min_selected, move |handle| {
1256            let min_selected = min_selected.get();
1257            handle.modify(|list: &mut List| {
1258                list.min_selected.set(min_selected);
1259                list.normalize_selection_state();
1260            });
1261        })
1262    }
1263
1264    fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
1265        let max_selected = max_selected.to_signal(self.cx);
1266        self.bind(max_selected, move |handle| {
1267            let max_selected = max_selected.get();
1268            handle.modify(|list: &mut List| {
1269                list.max_selected.set(max_selected);
1270                list.normalize_selection_state();
1271            });
1272        })
1273    }
1274
1275    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1276        self,
1277        flag: impl Res<U> + 'static,
1278    ) -> Self {
1279        let flag = flag.to_signal(self.cx);
1280        self.bind(flag, move |handle| {
1281            let selection_follows_focus = flag.get();
1282            let s = selection_follows_focus.into();
1283            handle.modify(|list: &mut List| list.selection_follows_focus.set(s));
1284        })
1285    }
1286
1287    fn space_selects_focused<U: Into<bool> + Clone + 'static>(
1288        self,
1289        flag: impl Res<U> + 'static,
1290    ) -> Self {
1291        let flag = flag.to_signal(self.cx);
1292        self.bind(flag, move |handle| {
1293            let space_selects_focused = flag.get();
1294            let s = space_selects_focused.into();
1295            handle.modify(|list: &mut List| list.space_selects_focused.set(s));
1296        })
1297    }
1298
1299    fn horizontal<U: Into<bool> + Clone + 'static>(
1300        self,
1301        horizontal: impl Res<U> + 'static,
1302    ) -> Self {
1303        let horizontal = horizontal.to_signal(self.cx);
1304        self.bind(horizontal, move |handle| {
1305            let horizontal = horizontal.get();
1306            let horizontal = horizontal.into();
1307            handle.modify(|list: &mut List| {
1308                list.orientation.set(if horizontal {
1309                    Orientation::Horizontal
1310                } else {
1311                    Orientation::Vertical
1312                });
1313            });
1314        })
1315    }
1316
1317    fn scroll_to_cursor(self, flag: bool) -> Self {
1318        self.modify(|list| {
1319            list.scroll_to_cursor.set(flag);
1320        })
1321    }
1322
1323    fn focus_first_item_on_focus_in(self, flag: impl Res<bool> + 'static) -> Self {
1324        let flag = flag.to_signal(self.cx);
1325        self.bind(flag, move |handle| {
1326            let focus_first_item_on_focus_in = flag.get();
1327            handle.modify(|list: &mut List| {
1328                list.focus_first_item_on_focus_in.set(focus_first_item_on_focus_in);
1329            });
1330        })
1331    }
1332
1333    fn on_scroll(
1334        self,
1335        callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
1336    ) -> Self {
1337        self.modify(|list: &mut List| list.on_scroll = Some(Box::new(callback)))
1338    }
1339
1340    fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
1341        let scrollx = scrollx.to_signal(self.cx);
1342        self.bind(scrollx, move |handle| {
1343            let scrollx = scrollx.get();
1344            handle.modify(|list| {
1345                list.scroll_x.set(scrollx);
1346            });
1347        })
1348    }
1349
1350    fn scroll_y(self, scrolly: impl Res<f32> + 'static) -> Self {
1351        let scrolly = scrolly.to_signal(self.cx);
1352        self.bind(scrolly, move |handle| {
1353            let scrolly = scrolly.get();
1354            handle.modify(|list| {
1355                list.scroll_y.set(scrolly);
1356            });
1357        })
1358    }
1359
1360    fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
1361        let flag = flag.to_signal(self.cx);
1362        self.bind(flag, move |handle| {
1363            let show_scrollbar = flag.get();
1364            handle.modify(|list| {
1365                list.show_horizontal_scrollbar.set(show_scrollbar);
1366            });
1367        })
1368    }
1369
1370    fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
1371        let flag = flag.to_signal(self.cx);
1372        self.bind(flag, move |handle| {
1373            let show_scrollbar = flag.get();
1374            handle.modify(|list| {
1375                list.show_vertical_scrollbar.set(show_scrollbar);
1376            });
1377        })
1378    }
1379
1380    fn type_ahead_text<F>(self, callback: F) -> Self
1381    where
1382        F: 'static + Fn(&mut EventContext, usize) -> Option<String>,
1383    {
1384        self.modify(|list: &mut List| list.type_ahead_text = Some(Box::new(callback)))
1385    }
1386}
1387
1388/// A view which represents a selectable item within a list.
1389pub struct ListItem {
1390    selected: Memo<bool>,
1391}
1392
1393impl ListItem {
1394    /// Create a new [ListItem] view.
1395    pub fn new<'a, T: Clone + 'static, M: SignalGet<T> + 'static>(
1396        cx: &'a mut Context,
1397        index: usize,
1398        item: M,
1399        selection: impl SignalMap<BTreeSet<usize>> + SignalGet<BTreeSet<usize>>,
1400        focused: impl SignalMap<Option<usize>>,
1401        focus_visibility: impl Res<bool> + Copy + 'static,
1402        item_content: impl 'static + Fn(&mut Context, usize, M),
1403    ) -> Handle<'a, Self> {
1404        let is_focused =
1405            focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)).get();
1406        let focused_signal =
1407            focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index));
1408        let is_selected = selection.map(move |selection| selection.contains(&index));
1409        Self { selected: is_selected }
1410            .build(cx, move |cx| {
1411                item_content(cx, index, item);
1412            })
1413            .role(Role::ListBoxOption)
1414            .focusable(true)
1415            .navigable(false)
1416            .toggle_class("focused", focused_signal)
1417            .focused_with_visibility(focused_signal, focus_visibility)
1418            .checked(selection.map(move |selection| selection.contains(&index)))
1419            .bind(focused_signal, move |handle| {
1420                let focused = focused_signal.get();
1421                if focused != is_focused {
1422                    handle.cx.emit(ScrollEvent::ScrollToView(handle.entity()));
1423                }
1424            })
1425            .on_press(move |cx| cx.emit(ListEvent::Select(index)))
1426    }
1427}
1428
1429impl View for ListItem {
1430    fn element(&self) -> Option<&'static str> {
1431        Some("list-item")
1432    }
1433
1434    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
1435        event.map(|window_event, _| match window_event {
1436            WindowEvent::GeometryChanged(geo) => {
1437                if self.selected.get() && geo.contains(GeoChanged::HEIGHT_CHANGED) {
1438                    cx.emit(ScrollEvent::ScrollToView(cx.current()));
1439                }
1440            }
1441            _ => {}
1442        });
1443    }
1444}
1445
1446fn focus_index_on_focus_in(
1447    selection: &BTreeSet<usize>,
1448    num_items: usize,
1449    focus_first_item_on_focus_in: bool,
1450) -> Option<usize> {
1451    selection
1452        .iter()
1453        .copied()
1454        .find(|index| *index < num_items)
1455        .or_else(|| focus_first_item_on_focus_in.then_some(0).filter(|_| num_items > 0))
1456}
1457
1458fn normalize_focused_index(focused: Option<usize>, num_items: usize) -> Option<usize> {
1459    focused.filter(|index| *index < num_items)
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464    use super::{focus_index_on_focus_in, normalize_focused_index};
1465    use std::collections::BTreeSet;
1466
1467    #[test]
1468    fn focuses_selected_item_on_focus_in() {
1469        let mut selection = BTreeSet::new();
1470        selection.insert(3);
1471
1472        assert_eq!(focus_index_on_focus_in(&selection, 5, false), Some(3));
1473    }
1474
1475    #[test]
1476    fn can_leave_focus_empty_when_nothing_is_selected() {
1477        let selection = BTreeSet::new();
1478
1479        assert_eq!(focus_index_on_focus_in(&selection, 5, false), None);
1480    }
1481
1482    #[test]
1483    fn falls_back_to_first_item_when_enabled() {
1484        let selection = BTreeSet::new();
1485
1486        assert_eq!(focus_index_on_focus_in(&selection, 5, true), Some(0));
1487    }
1488
1489    #[test]
1490    fn keeps_focused_index_when_in_range() {
1491        assert_eq!(normalize_focused_index(Some(3), 5), Some(3));
1492    }
1493
1494    #[test]
1495    fn clears_focused_index_when_out_of_range() {
1496        assert_eq!(normalize_focused_index(Some(5), 5), None);
1497    }
1498}