floem/views/
list.rs

1use super::{Decorators, v_stack_from_iter};
2use crate::context::StyleCx;
3use crate::event::EventPropagation;
4use crate::id::ViewId;
5use crate::style::Style;
6use crate::style_class;
7use crate::view::IntoView;
8use crate::{
9    event::{Event, EventListener},
10    view::View,
11};
12use floem_reactive::{Effect, RwSignal, SignalGet, SignalUpdate};
13use ui_events::keyboard::{Key, KeyState, KeyboardEvent, NamedKey};
14
15style_class!(pub ListClass);
16style_class!(pub ListItemClass);
17
18enum ListUpdate {
19    SelectionChanged(Option<usize>),
20    Accept,
21}
22
23pub(crate) struct Item {
24    pub(crate) id: ViewId,
25    pub(crate) index: usize,
26    pub(crate) selection: RwSignal<Option<usize>>,
27    pub(crate) child: ViewId,
28}
29
30/// A list of views that support the selection of items. See [`list`].
31pub struct List {
32    id: ViewId,
33    selection: RwSignal<Option<usize>>,
34    onaccept: Option<Box<dyn Fn(Option<usize>)>>,
35    child: ViewId,
36}
37
38impl List {
39    /// Returns the index of the current selection (if any).
40    pub fn selection(&self) -> RwSignal<Option<usize>> {
41        self.selection
42    }
43
44    /// Adds a callback to the [List] that is updated when the current selected item changes.
45    pub fn on_select(self, on_select: impl Fn(Option<usize>) + 'static) -> Self {
46        Effect::new(move |_| {
47            let selection = self.selection.get();
48            on_select(selection);
49        });
50        self
51    }
52
53    /// Adds a callback for user list selection with the `Enter` key.
54    pub fn on_accept(mut self, on_accept: impl Fn(Option<usize>) + 'static) -> Self {
55        self.onaccept = Some(Box::new(on_accept));
56        self
57    }
58}
59
60/// A list of views built from an iterator which remains static and always contains the same elements in the same order.
61///
62/// A list is like a [stack](super::stack()) but also has built-in support for the selection of items:
63/// up and down using arrow keys, top and bottom control using the home and end keys,
64/// and "acceptance" of an item using the Enter key.
65///
66/// ## Example
67/// ```rust
68/// use floem::views::*;
69/// list(
70///     vec![1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9]
71///         .iter()
72///         .map(|val| text(val)),
73/// );
74/// ```
75pub fn list<V>(iterator: impl IntoIterator<Item = V>) -> List
76where
77    V: IntoView + 'static,
78{
79    let list_id = ViewId::new();
80    let selection = RwSignal::new(Some(0));
81    Effect::new(move |old_idx: Option<Option<usize>>| {
82        let selection = selection.get();
83        list_id.update_state(ListUpdate::SelectionChanged(old_idx.flatten()));
84        selection
85    });
86    let stack = v_stack_from_iter(iterator.into_iter().enumerate().map(move |(index, v)| {
87        let id = ViewId::new();
88        let v = v.into_view().class(ListItemClass);
89        let child = v.id();
90        id.set_children([v]);
91        Item {
92            id,
93            selection,
94            index,
95            child,
96        }
97        .on_click_stop(move |_| {
98            if selection.get_untracked() != Some(index) {
99                selection.set(Some(index));
100                list_id.update_state(ListUpdate::Accept);
101            }
102        })
103    }))
104    .style(|s| s.width_full().height_full());
105    let length = stack.id().children().len();
106    let child = stack.id();
107    list_id.set_children([stack]);
108    List {
109        id: list_id,
110        selection,
111        child,
112        onaccept: None,
113    }
114    .on_event(EventListener::KeyDown, move |e| {
115        if let Event::Key(KeyboardEvent {
116            state: KeyState::Down,
117            key,
118            ..
119        }) = e
120        {
121            match key {
122                Key::Named(NamedKey::Home) => {
123                    if length > 0 {
124                        selection.set(Some(0));
125                    }
126                    EventPropagation::Stop
127                }
128                Key::Named(NamedKey::End) => {
129                    if length > 0 {
130                        selection.set(Some(length - 1));
131                    }
132                    EventPropagation::Stop
133                }
134                Key::Named(NamedKey::ArrowUp) => {
135                    let current = selection.get_untracked();
136                    match current {
137                        Some(i) => {
138                            if i > 0 {
139                                selection.set(Some(i - 1));
140                            }
141                        }
142                        None => {
143                            if length > 0 {
144                                selection.set(Some(length - 1));
145                            }
146                        }
147                    }
148                    EventPropagation::Stop
149                }
150                Key::Named(NamedKey::Enter) => {
151                    list_id.update_state(ListUpdate::Accept);
152                    EventPropagation::Stop
153                }
154                Key::Character(c) if c == " " => {
155                    list_id.update_state(ListUpdate::Accept);
156                    EventPropagation::Stop
157                }
158                Key::Named(NamedKey::ArrowDown) => {
159                    let current = selection.get_untracked();
160                    match current {
161                        Some(i) => {
162                            if i < length - 1 {
163                                selection.set(Some(i + 1));
164                            }
165                        }
166                        None => {
167                            if length > 0 {
168                                selection.set(Some(0));
169                            }
170                        }
171                    }
172                    EventPropagation::Stop
173                }
174                _ => EventPropagation::Continue,
175            }
176        } else {
177            EventPropagation::Continue
178        }
179    })
180    .class(ListClass)
181}
182
183impl View for List {
184    fn id(&self) -> ViewId {
185        self.id
186    }
187
188    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
189        if let Ok(change) = state.downcast::<ListUpdate>() {
190            match *change {
191                ListUpdate::SelectionChanged(old_idx) => {
192                    if let Some(old_idx) = old_idx {
193                        let child = self.child.children()[old_idx];
194                        child.request_style_recursive();
195                    }
196                    if let Some(index) = self.selection.get_untracked() {
197                        let child = self.child.children()[index];
198                        child.request_style_recursive();
199                        child.scroll_to(None);
200                    }
201                }
202                ListUpdate::Accept => {
203                    if let Some(on_accept) = &self.onaccept {
204                        on_accept(self.selection.get_untracked());
205                    }
206                }
207            }
208        }
209    }
210}
211
212impl View for Item {
213    fn id(&self) -> ViewId {
214        self.id
215    }
216
217    fn view_style(&self) -> Option<crate::style::Style> {
218        Some(Style::new().flex_col())
219    }
220
221    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
222        "List Item".into()
223    }
224
225    fn style_pass(&mut self, cx: &mut StyleCx<'_>) {
226        let selected = self.selection.get_untracked();
227        if Some(self.index) == selected {
228            cx.save();
229            cx.selected();
230            cx.style_view(self.child);
231            cx.restore();
232        } else {
233            cx.style_view(self.child);
234        }
235    }
236}
237
238/// A trait that adds a `list` method to any generic type `T` that implements [`IntoIterator`] where
239/// `T::Item` implements [IntoView].
240pub trait ListExt {
241    fn list(self) -> List;
242}
243impl<V: IntoView + 'static, T: IntoIterator<Item = V> + 'static> ListExt for T {
244    fn list(self) -> List {
245        list(self)
246    }
247}