floem/views/
virtual_list.rs

1use floem_reactive::create_effect;
2use taffy::FlexDirection;
3use ui_events::keyboard::{Key, NamedKey};
4
5use crate::event::{Event, EventListener, EventPropagation};
6use crate::{ViewId, prelude::*};
7
8use std::hash::{DefaultHasher, Hash, Hasher};
9use std::ops::{Deref, DerefMut};
10
11pub struct VirtualList<T: 'static> {
12    stack: VirtualStack<(usize, T)>,
13    pub selection: RwSignal<Option<usize>>,
14}
15
16impl<T> VirtualList<T> {
17    // For types that implement all constraints
18    pub fn new<DF, I>(data_fn: DF) -> Self
19    where
20        DF: Fn() -> I + 'static,
21        I: VirtualVector<T>,
22        T: Hash + Eq + IntoView + 'static,
23    {
24        Self::full(
25            data_fn,
26            |item| {
27                let mut hasher = DefaultHasher::new();
28                item.hash(&mut hasher);
29                hasher.finish()
30            },
31            |_index, item| item.into_view(),
32        )
33    }
34
35    // For types that are hashable but need custom view
36    pub fn with_view<DF, I, V>(data_fn: DF, view_fn: impl Fn(T) -> V + 'static) -> Self
37    where
38        DF: Fn() -> I + 'static,
39        I: VirtualVector<T>,
40        T: Hash + Eq + 'static,
41        V: IntoView,
42    {
43        Self::full(
44            data_fn,
45            |item| {
46                let mut hasher = DefaultHasher::new();
47                item.hash(&mut hasher);
48                hasher.finish()
49            },
50            move |_index, item| view_fn(item).into_view(),
51        )
52    }
53
54    // For types that implement IntoView but need custom keys
55    pub fn with_key<DF, I, K>(data_fn: DF, key_fn: impl Fn(&T) -> K + 'static) -> Self
56    where
57        DF: Fn() -> I + 'static,
58        I: VirtualVector<T>,
59        T: IntoView + 'static,
60        K: Hash + Eq + 'static,
61    {
62        Self::full(data_fn, key_fn, |_index, item| item.into_view())
63    }
64
65    pub fn full<DF, I, KF, K, VF, V>(data_fn: DF, key_fn: KF, view_fn: VF) -> Self
66    where
67        DF: Fn() -> I + 'static,
68        I: VirtualVector<T>,
69        KF: Fn(&T) -> K + 'static,
70        K: Eq + Hash + 'static,
71        VF: Fn(usize, T) -> V + 'static,
72        V: IntoView + 'static,
73        T: 'static,
74    {
75        virtual_list(data_fn, key_fn, view_fn)
76    }
77}
78
79impl<T: 'static> Deref for VirtualList<T> {
80    type Target = VirtualStack<(usize, T)>;
81    fn deref(&self) -> &VirtualStack<(usize, T)> {
82        &self.stack
83    }
84}
85
86impl<T: 'static> DerefMut for VirtualList<T> {
87    fn deref_mut(&mut self) -> &mut VirtualStack<(usize, T)> {
88        &mut self.stack
89    }
90}
91
92impl<T: 'static> VirtualList<T> {
93    pub fn selection(&self) -> RwSignal<Option<usize>> {
94        self.selection
95    }
96
97    /// Sets a callback function to be called whenever the selection changes in the virtual list.
98    ///
99    /// The callback function receives an `Option<usize>` parameter representing the currently
100    /// selected item index. When `None`, no item is selected. When `Some(index)`, the item
101    /// at that index is currently selected.
102    ///
103    /// This is a convenience helper that creates a new effect internally. Calling this method
104    /// multiple times will not override previous `on_select` calls - each call creates a separate
105    /// effect that will all be triggered on selection changes. For more control, you can manually
106    /// create effects using the selection signal returned by [`selection()`](Self::selection).
107    ///
108    /// # Parameters
109    ///
110    /// * `on_select` - A function that takes `Option<usize>` and will be called on selection changes
111    ///
112    /// # Returns
113    ///
114    /// Returns `self` to allow method chaining.
115    ///
116    /// # Example
117    ///
118    /// ```rust
119    /// use floem::prelude::*;
120    ///
121    /// virtual_list(
122    ///     move || 1..=1000000,
123    ///     |item| *item,
124    ///     |index, item| format!("{index}: {item}")
125    /// )
126    /// .on_select(|selection| {
127    ///     match selection {
128    ///         Some(index) => println!("Selected item at index: {index}"),
129    ///         None => println!("No item selected"),
130    ///     }
131    /// });
132    /// ```
133    pub fn on_select(self, on_select: impl Fn(Option<usize>) + 'static) -> Self {
134        create_effect(move |_| {
135            let selection = self.selection.get();
136            on_select(selection);
137        });
138        self
139    }
140}
141
142/// A view that supports virtual scrolling with item selection.
143/// Selection is done using arrow keys, home/end for top/bottom.
144pub fn virtual_list<T, DF, I, KF, K, VF, V>(data_fn: DF, key_fn: KF, view_fn: VF) -> VirtualList<T>
145where
146    DF: Fn() -> I + 'static,
147    I: VirtualVector<T>,
148    KF: Fn(&T) -> K + 'static,
149    K: Eq + Hash + 'static,
150    VF: Fn(usize, T) -> V + 'static,
151    V: IntoView + 'static,
152{
153    let selection = RwSignal::new(None::<usize>);
154    let length = RwSignal::new(0);
155
156    let stack = virtual_stack(
157        move || {
158            let vector = data_fn().enumerate();
159            length.set(vector.total_len());
160            vector
161        },
162        move |(_i, d)| key_fn(d),
163        move |(index, e)| {
164            let child = view_fn(index, e).class(ListItemClass);
165            let child_id = child.id();
166            child.on_click_cont(move |_| {
167                if selection.get_untracked() != Some(index) {
168                    selection.set(Some(index));
169                    child_id.scroll_to(None);
170                    let Some(parent) = child_id.parent() else {
171                        return;
172                    };
173                    parent.update_state(index);
174                    parent.request_style_recursive();
175                }
176            })
177        },
178    )
179    .style(|s| s.size_full());
180
181    let stack_id = stack.id();
182
183    create_effect(move |_| {
184        if let Some(idx) = selection.get() {
185            stack_id.update_state(idx);
186        }
187    });
188
189    let direction = stack.direction;
190
191    let stack = stack
192        .class(ListClass)
193        .on_event(EventListener::KeyDown, move |e| {
194            if let Event::Key(key_event) = e {
195                match key_event.key {
196                    Key::Named(NamedKey::Home) => {
197                        if length.get_untracked() > 0 {
198                            selection.set(Some(0));
199                            stack_id.update_state(0_usize); // Must be usize to match state type
200                        }
201                        EventPropagation::Stop
202                    }
203                    Key::Named(NamedKey::End) => {
204                        let len = length.get_untracked();
205                        if len > 0 {
206                            selection.set(Some(len - 1));
207                            stack_id.update_state(len - 1);
208                        }
209                        EventPropagation::Stop
210                    }
211                    Key::Named(
212                        named_key @ (NamedKey::ArrowUp
213                        | NamedKey::ArrowDown
214                        | NamedKey::ArrowLeft
215                        | NamedKey::ArrowRight),
216                    ) => handle_arrow_key(
217                        selection,
218                        length.get_untracked(),
219                        direction.get_untracked(),
220                        stack_id,
221                        named_key,
222                    ),
223                    _ => EventPropagation::Continue,
224                }
225            } else {
226                EventPropagation::Continue
227            }
228        });
229    VirtualList { stack, selection }
230}
231
232fn handle_arrow_key(
233    selection: RwSignal<Option<usize>>,
234    len: usize,
235    direction: FlexDirection,
236    stack_id: ViewId,
237    key: NamedKey,
238) -> EventPropagation {
239    let current = selection.get();
240
241    // Determine if we should move forward or backward based on direction and key
242    let should_move_forward = matches!(
243        (direction, key),
244        (FlexDirection::Row, NamedKey::ArrowRight)
245            | (FlexDirection::RowReverse, NamedKey::ArrowLeft)
246            | (FlexDirection::Column, NamedKey::ArrowDown)
247            | (FlexDirection::ColumnReverse, NamedKey::ArrowUp)
248    );
249
250    let should_move_backward = matches!(
251        (direction, key),
252        (FlexDirection::Row, NamedKey::ArrowLeft)
253            | (FlexDirection::RowReverse, NamedKey::ArrowRight)
254            | (FlexDirection::Column, NamedKey::ArrowUp)
255            | (FlexDirection::ColumnReverse, NamedKey::ArrowDown)
256    );
257
258    // Handle cross-axis navigation (e.g., up/down in Row mode)
259    let is_cross_axis = matches!(
260        (direction, key),
261        (
262            FlexDirection::Row | FlexDirection::RowReverse,
263            NamedKey::ArrowUp | NamedKey::ArrowDown
264        ) | (
265            FlexDirection::Column | FlexDirection::ColumnReverse,
266            NamedKey::ArrowLeft | NamedKey::ArrowRight
267        )
268    );
269
270    if is_cross_axis {
271        return EventPropagation::Continue;
272    }
273
274    match current {
275        Some(i) => {
276            if should_move_backward && i > 0 {
277                selection.set(Some(i - 1));
278                stack_id.update_state(i - 1);
279            } else if should_move_forward && i < len - 1 {
280                selection.set(Some(i + 1));
281                stack_id.update_state(i + 1);
282            }
283        }
284        None => {
285            if len > 0 {
286                let res = if should_move_backward { len - 1 } else { 0 };
287                selection.set(Some(res));
288                stack_id.update_state(res);
289            }
290        }
291    }
292    EventPropagation::Stop
293}
294
295impl<T: 'static> IntoView for VirtualList<T> {
296    type V = VirtualStack<(usize, T)>;
297
298    fn into_view(self) -> Self::V {
299        self.stack
300    }
301}