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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
14pub enum Selectable {
15 #[default]
16 None,
18 Single,
20 Multi,
22}
23
24impl_res_simple!(Selectable);
25
26pub enum ListEvent {
28 Select(usize),
30 SelectFocused,
32 FocusNext,
34 FocusPrev,
36 FocusFirst,
38 FocusLast,
40 ClearSelection,
42 Scroll(f32, f32),
44}
45
46pub struct List {
48 num_items: usize,
50 selection: Signal<BTreeSet<usize>>,
52 selectable: Signal<Selectable>,
54 focused: Signal<Option<usize>>,
56 selection_follows_focus: Signal<bool>,
58 min_selected: Signal<usize>,
60 max_selected: Signal<usize>,
62 orientation: Signal<Orientation>,
64 scroll_to_cursor: Signal<bool>,
66 focus_first_item_on_focus_in: Signal<bool>,
68 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
70 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
72 scroll_x: Signal<f32>,
74 scroll_y: Signal<f32>,
76 show_horizontal_scrollbar: Signal<bool>,
78 show_vertical_scrollbar: Signal<bool>,
80 focus_visibility: Signal<bool>,
82 type_ahead_text: Option<Box<dyn Fn(&mut EventContext, usize) -> Option<String>>>,
84 type_ahead_buffer: String,
86 type_ahead_last_input: Option<Instant>,
88 type_ahead_timeout: Duration,
90}
91
92struct 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 item_signals: Vec<Signal<T>>,
108 item_entities: Vec<Entity>,
110 prev_values: Vec<T>,
112 scope: Scope,
113}
114
115struct 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 item_signals: Vec<Signal<T>>,
127 item_entities: Vec<Entity>,
129 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 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 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 for entity in self.item_entities.drain(first_diff..) {
237 cx.remove(entity);
238 }
239 self.item_signals.truncate(first_diff);
240
241 for (i, value) in new_values[first_diff..].iter().enumerate() {
243 let index = first_diff + i;
244 if index < self.item_signals.len() {
245 self.item_signals[index].set(value.clone());
247 } else {
248 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 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 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 for entity in self.item_entities.drain(first_diff..) {
363 cx.remove(entity);
364 }
365 self.item_signals.truncate(first_diff);
366
367 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 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 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 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 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 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
1072pub trait ListModifiers: Sized {
1074 fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1076 where
1077 R: Deref<Target = [usize]> + Clone + 'static;
1078
1079 fn on_select<F>(self, callback: F) -> Self
1081 where
1082 F: 'static + Fn(&mut EventContext, usize);
1083
1084 fn selectable<U: Into<Selectable> + Clone + 'static>(
1086 self,
1087 selectable: impl Res<U> + 'static,
1088 ) -> Self;
1089
1090 fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self;
1092
1093 fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self;
1095
1096 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1098 self,
1099 flag: impl Res<U> + 'static,
1100 ) -> Self;
1101
1102 fn horizontal<U: Into<bool> + Clone + 'static>(self, horizontal: impl Res<U> + 'static)
1104 -> Self;
1105
1106 fn scroll_to_cursor(self, flag: bool) -> Self;
1108
1109 fn focus_first_item_on_focus_in(self, flag: impl Res<bool> + 'static) -> Self;
1111
1112 fn on_scroll(
1114 self,
1115 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
1116 ) -> Self;
1117
1118 fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self;
1120
1121 fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self;
1123
1124 fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
1126
1127 fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
1129
1130 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
1308pub struct ListItem {
1310 selected: Memo<bool>,
1311}
1312
1313impl ListItem {
1314 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}