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