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