Skip to main content

floem/views/
virtual_list.rs

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