1use std::{collections::BTreeSet, ops::Deref, rc::Rc};
2use vizia_reactive::{Scope, SignalGet, SignalWith, UpdaterEffect};
3
4use crate::prelude::*;
5use crate::{binding::BindingHandler, context::SIGNAL_REBUILDS};
6
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
9pub enum Selectable {
10 #[default]
11 None,
13 Single,
15 Multi,
17}
18
19impl_res_simple!(Selectable);
20
21pub enum ListEvent {
23 Select(usize),
25 SelectFocused,
27 FocusNext,
29 FocusPrev,
31 ClearSelection,
33 Scroll(f32, f32),
35}
36
37pub struct List {
39 num_items: usize,
41 selection: Signal<BTreeSet<usize>>,
43 selectable: Signal<Selectable>,
45 focused: Signal<Option<usize>>,
47 selection_follows_focus: Signal<bool>,
49 min_selected: Signal<usize>,
51 max_selected: Signal<usize>,
53 orientation: Signal<Orientation>,
55 scroll_to_cursor: Signal<bool>,
57 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
59 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
61 scroll_x: Signal<f32>,
63 scroll_y: Signal<f32>,
65 show_horizontal_scrollbar: Signal<bool>,
67 show_vertical_scrollbar: Signal<bool>,
69}
70
71struct ListItemsBinding<T: 'static> {
78 entity: Entity,
79 list_entity: Entity,
80 get_fn: Box<dyn Fn() -> Vec<T>>,
81 item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
82 selection: Signal<BTreeSet<usize>>,
83 focused: Signal<Option<usize>>,
84 item_signals: Vec<Signal<T>>,
86 item_entities: Vec<Entity>,
88 prev_values: Vec<T>,
90 scope: Scope,
91}
92
93impl<T: PartialEq + Clone + 'static> ListItemsBinding<T> {
94 fn create<S, V>(
95 cx: &mut Context,
96 list_entity: Entity,
97 list: S,
98 selection: Signal<BTreeSet<usize>>,
99 focused: Signal<Option<usize>>,
100 item_content: Rc<dyn Fn(&mut Context, usize, Signal<T>)>,
101 ) where
102 S: SignalGet<V> + SignalWith<V> + Copy + 'static,
103 V: Deref<Target = [T]> + Clone + 'static,
104 {
105 let entity = cx.entity_manager.create();
106 cx.tree.add(entity, cx.current()).expect("Failed to add to tree");
107 cx.tree.set_ignored(entity, true);
108
109 let scope = Scope::new();
110 let initial_values: Vec<T> = scope.enter(|| {
111 UpdaterEffect::new(
112 move || list.with(|list| list.deref().to_vec()),
113 move |_new_value| {
114 SIGNAL_REBUILDS.with_borrow_mut(|set| {
115 set.insert(entity);
116 });
117 },
118 )
119 });
120
121 let mut binding = Self {
122 entity,
123 list_entity,
124 get_fn: Box::new(move || list.with_untracked(|list| list.deref().to_vec())),
125 item_content,
126 selection,
127 focused,
128 item_signals: Vec::new(),
129 item_entities: Vec::new(),
130 prev_values: Vec::new(),
131 scope,
132 };
133
134 for (index, value) in initial_values.iter().enumerate() {
136 let signal = Signal::new(value.clone());
137 let entity = binding.create_item_entity(cx, index, signal);
138 binding.item_signals.push(signal);
139 binding.item_entities.push(entity);
140 binding.prev_values.push(value.clone());
141 }
142 binding.update_list_metadata(cx, initial_values.len());
143
144 cx.bindings.insert(entity, Box::new(binding));
145
146 let _: Handle<Self> =
147 Handle { current: entity, entity, p: Default::default(), cx }.ignore();
148 }
149
150 fn update_list_metadata(&self, cx: &mut Context, len: usize) {
151 if let Some(view) = cx.views.get_mut(&self.list_entity) {
152 if let Some(list) = view.downcast_mut::<List>() {
153 list.num_items = len;
154 list.normalize_selection_state();
155 }
156 }
157 }
158
159 fn create_item_entity(&self, cx: &mut Context, index: usize, signal: Signal<T>) -> Entity {
160 let mut created = Entity::null();
161 let item_content = self.item_content.clone();
162 let selection = self.selection;
163 let focused = self.focused;
164
165 cx.with_current(self.entity, |cx| {
166 created = ListItem::new(cx, index, signal, selection, focused, {
167 let item_content = item_content.clone();
168 move |cx, index, item| (item_content)(cx, index, item)
169 })
170 .entity();
171 });
172
173 created
174 }
175}
176
177impl<T: PartialEq + Clone + 'static> BindingHandler for ListItemsBinding<T> {
178 fn update(&mut self, cx: &mut Context) {
179 let new_values = (self.get_fn)();
180 let new_len = new_values.len();
181
182 let first_diff = self
184 .prev_values
185 .iter()
186 .zip(new_values.iter())
187 .position(|(old, new)| old != new)
188 .unwrap_or(self.prev_values.len().min(new_len));
189
190 for entity in self.item_entities.drain(first_diff..) {
192 cx.remove(entity);
193 }
194 self.item_signals.truncate(first_diff);
195
196 for (i, value) in new_values[first_diff..].iter().enumerate() {
198 let index = first_diff + i;
199 if index < self.item_signals.len() {
200 self.item_signals[index].set(value.clone());
202 } else {
203 let signal = Signal::new(value.clone());
205 let entity = self.create_item_entity(cx, index, signal);
206 self.item_signals.push(signal);
207 self.item_entities.push(entity);
208 }
209 }
210
211 self.prev_values = new_values;
212 self.update_list_metadata(cx, new_len);
213 }
214
215 fn remove(&self, _cx: &mut Context) {
216 self.scope.dispose();
217 }
218
219 fn debug(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
220 f.write_str("ListItemsBinding")
221 }
222}
223
224impl List {
225 fn selection_limits(&self) -> (usize, usize) {
226 let mut min_selected = self.min_selected.get();
227 let mut max_selected = self.max_selected.get();
228
229 match self.selectable.get() {
230 Selectable::None => {
231 min_selected = 0;
232 max_selected = 0;
233 }
234
235 Selectable::Single => {
236 min_selected = min_selected.min(1);
237 max_selected = 1;
238 }
239
240 Selectable::Multi => {}
241 }
242
243 max_selected = max_selected.min(self.num_items);
244 min_selected = min_selected.min(max_selected);
245
246 (min_selected, max_selected)
247 }
248
249 fn normalize_selection_state(&mut self) {
250 let (min_selected, max_selected) = self.selection_limits();
251
252 let mut selection = self.selection.get();
253 selection.retain(|index| *index < self.num_items);
254
255 while selection.len() > max_selected {
256 if let Some(last) = selection.iter().next_back().copied() {
257 selection.remove(&last);
258 } else {
259 break;
260 }
261 }
262
263 if selection.len() < min_selected {
264 for index in 0..self.num_items {
265 selection.insert(index);
266 if selection.len() >= min_selected {
267 break;
268 }
269 }
270 }
271
272 let mut focused = self.focused.get();
273 if focused.is_some_and(|index| index >= self.num_items) {
274 focused = self.num_items.checked_sub(1);
275 }
276
277 self.selection.set(selection);
278 self.focused.set(focused);
279 }
280
281 pub fn new<S, V, T>(
289 cx: &mut Context,
290 list: S,
291 item_content: impl 'static + Fn(&mut Context, usize, Signal<T>),
292 ) -> Handle<Self>
293 where
294 S: Res<V> + 'static,
295 V: Deref<Target = [T]> + Clone + 'static,
296 T: PartialEq + Clone + 'static,
297 {
298 let content: Rc<dyn Fn(&mut Context, usize, Signal<T>)> = Rc::new(item_content);
299 let selection = Signal::new(BTreeSet::default());
300 let selectable = Signal::new(Selectable::None);
301 let focused = Signal::new(None);
302 let min_selected = Signal::new(0);
303 let max_selected = Signal::new(usize::MAX);
304 let orientation = Signal::new(Orientation::Vertical);
305 let scroll_to_cursor = Signal::new(false);
306 let scroll_x = Signal::new(0.0);
307 let scroll_y = Signal::new(0.0);
308 let show_horizontal_scrollbar = Signal::new(true);
309 let show_vertical_scrollbar = Signal::new(true);
310
311 Self {
312 num_items: 0,
313 selection,
314 selectable,
315 focused,
316 selection_follows_focus: Signal::new(false),
317 min_selected,
318 max_selected,
319 orientation,
320 scroll_to_cursor,
321 on_select: None,
322 on_scroll: None,
323 scroll_x,
324 scroll_y,
325 show_horizontal_scrollbar,
326 show_vertical_scrollbar,
327 }
328 .build(cx, move |cx| {
329 let list_entity = cx.current();
330
331 Keymap::from(vec![
332 (
333 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
334 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
335 ),
336 (
337 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
338 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
339 ),
340 (
341 KeyChord::new(Modifiers::empty(), Code::Space),
342 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
343 ),
344 (
345 KeyChord::new(Modifiers::empty(), Code::Enter),
346 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
347 ),
348 ])
349 .build(cx);
350
351 Binding::new(cx, orientation, move |cx| {
352 let orientation = orientation.get();
353 if orientation == Orientation::Horizontal {
354 cx.emit(KeymapEvent::RemoveAction(
355 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
356 "Focus Next",
357 ));
358
359 cx.emit(KeymapEvent::RemoveAction(
360 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
361 "Focus Previous",
362 ));
363
364 cx.emit(KeymapEvent::InsertAction(
365 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
366 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
367 ));
368
369 cx.emit(KeymapEvent::InsertAction(
370 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
371 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
372 ));
373 }
374 });
375
376 let list_signal = list.to_signal(cx);
377 ScrollView::new(cx, move |cx| {
378 ListItemsBinding::create(
379 cx,
380 list_entity,
381 list_signal,
382 selection,
383 focused,
384 content.clone(),
385 );
386 })
387 .show_horizontal_scrollbar(show_horizontal_scrollbar)
388 .show_vertical_scrollbar(show_vertical_scrollbar)
389 .scroll_to_cursor(scroll_to_cursor)
390 .scroll_x(scroll_x)
391 .scroll_y(scroll_y)
392 .on_scroll(|cx, x, y| {
393 if y.is_finite() {
394 cx.emit(ListEvent::Scroll(x, y));
395 }
396 });
397 })
398 .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
399 .orientation(orientation)
400 .navigable(true)
401 .role(Role::ListBox)
402 }
403}
404
405impl View for List {
406 fn element(&self) -> Option<&'static str> {
407 Some("list")
408 }
409
410 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
411 event.take(|list_event, meta| match list_event {
412 ListEvent::Select(index) => {
413 cx.focus();
414 let selectable = self.selectable.get();
415 let (min_selected, max_selected) = self.selection_limits();
416 let mut selection = self.selection.get();
417 let mut focused = self.focused.get();
418 match selectable {
419 Selectable::Single => {
420 if selection.contains(&index) {
421 if min_selected == 0 {
422 selection.clear();
423 focused = None;
424 }
425 } else {
426 selection.clear();
427 selection.insert(index);
428 focused = Some(index);
429 if let Some(on_select) = &self.on_select {
430 on_select(cx, index);
431 }
432 }
433 }
434
435 Selectable::Multi => {
436 if selection.contains(&index) {
437 if selection.len() > min_selected {
438 selection.remove(&index);
439 if focused == Some(index) {
440 focused = selection.iter().next_back().copied();
441 }
442 }
443 } else {
444 if selection.len() < max_selected {
445 selection.insert(index);
446 focused = Some(index);
447 if let Some(on_select) = &self.on_select {
448 on_select(cx, index);
449 }
450 }
451 }
452 }
453
454 Selectable::None => {}
455 }
456
457 self.selection.set(selection);
458 self.focused.set(focused);
459
460 meta.consume();
461 }
462
463 ListEvent::SelectFocused => {
464 if let Some(focused) = self.focused.get() {
465 cx.emit(ListEvent::Select(focused))
466 }
467 meta.consume();
468 }
469
470 ListEvent::ClearSelection => {
471 let (min_selected, _) = self.selection_limits();
472 if min_selected == 0 {
473 self.selection.set(BTreeSet::default());
474 }
475 meta.consume();
476 }
477
478 ListEvent::FocusNext => {
479 let mut focused = self.focused.get();
480 if let Some(f) = &mut focused {
481 if *f < self.num_items.saturating_sub(1) {
482 *f = f.saturating_add(1);
483 if self.selection_follows_focus.get() {
484 cx.emit(ListEvent::SelectFocused);
485 }
486 }
487 } else {
488 focused = Some(0);
489 if self.selection_follows_focus.get() {
490 cx.emit(ListEvent::SelectFocused);
491 }
492 }
493
494 self.focused.set(focused);
495
496 meta.consume();
497 }
498
499 ListEvent::FocusPrev => {
500 let mut focused = self.focused.get();
501 if let Some(f) = &mut focused {
502 if *f > 0 {
503 *f = f.saturating_sub(1);
504 if self.selection_follows_focus.get() {
505 cx.emit(ListEvent::SelectFocused);
506 }
507 }
508 } else {
509 focused = Some(self.num_items.saturating_sub(1));
510 if self.selection_follows_focus.get() {
511 cx.emit(ListEvent::SelectFocused);
512 }
513 }
514
515 self.focused.set(focused);
516
517 meta.consume();
518 }
519
520 ListEvent::Scroll(x, y) => {
521 self.scroll_x.set(x);
522 self.scroll_y.set(y);
523 if let Some(callback) = &self.on_scroll {
524 (callback)(cx, x, y);
525 }
526
527 meta.consume();
528 }
529 })
530 }
531}
532
533pub trait ListModifiers: Sized {
535 fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
537 where
538 R: Deref<Target = [usize]> + Clone + 'static;
539
540 fn on_select<F>(self, callback: F) -> Self
542 where
543 F: 'static + Fn(&mut EventContext, usize);
544
545 fn selectable<U: Into<Selectable> + Clone + 'static>(
547 self,
548 selectable: impl Res<U> + 'static,
549 ) -> Self;
550
551 fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self;
553
554 fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self;
556
557 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
559 self,
560 flag: impl Res<U> + 'static,
561 ) -> Self;
562
563 fn horizontal<U: Into<bool> + Clone + 'static>(self, horizontal: impl Res<U> + 'static)
565 -> Self;
566
567 fn scroll_to_cursor(self, flag: bool) -> Self;
569
570 fn on_scroll(
572 self,
573 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
574 ) -> Self;
575
576 fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self;
578
579 fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self;
581
582 fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
584
585 fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self;
587}
588
589impl ListModifiers for Handle<'_, List> {
590 fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
591 where
592 R: Deref<Target = [usize]> + Clone + 'static,
593 {
594 let selection = selection.to_signal(self.cx);
595 self.bind(selection, move |handle| {
596 selection.with(|selected_indices| {
597 handle.modify(|list| {
598 let mut selection = BTreeSet::default();
599 let mut focused = None;
600 for idx in selected_indices.deref().iter().copied() {
601 selection.insert(idx);
602 focused = Some(idx);
603 }
604 list.selection.set(selection);
605 list.focused.set(focused);
606 list.normalize_selection_state();
607 });
608 });
609 })
610 }
611
612 fn on_select<F>(self, callback: F) -> Self
613 where
614 F: 'static + Fn(&mut EventContext, usize),
615 {
616 self.modify(|list: &mut List| list.on_select = Some(Box::new(callback)))
617 }
618
619 fn selectable<U: Into<Selectable> + Clone + 'static>(
620 self,
621 selectable: impl Res<U> + 'static,
622 ) -> Self {
623 let selectable = selectable.to_signal(self.cx);
624 self.bind(selectable, move |handle| {
625 let selectable = selectable.get();
626 let s = selectable.into();
627 handle.modify(|list: &mut List| {
628 list.selectable.set(s);
629 list.normalize_selection_state();
630 });
631 })
632 }
633
634 fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
635 let min_selected = min_selected.to_signal(self.cx);
636 self.bind(min_selected, move |handle| {
637 let min_selected = min_selected.get();
638 handle.modify(|list: &mut List| {
639 list.min_selected.set(min_selected);
640 list.normalize_selection_state();
641 });
642 })
643 }
644
645 fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
646 let max_selected = max_selected.to_signal(self.cx);
647 self.bind(max_selected, move |handle| {
648 let max_selected = max_selected.get();
649 handle.modify(|list: &mut List| {
650 list.max_selected.set(max_selected);
651 list.normalize_selection_state();
652 });
653 })
654 }
655
656 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
657 self,
658 flag: impl Res<U> + 'static,
659 ) -> Self {
660 let flag = flag.to_signal(self.cx);
661 self.bind(flag, move |handle| {
662 let selection_follows_focus = flag.get();
663 let s = selection_follows_focus.into();
664 handle.modify(|list: &mut List| list.selection_follows_focus.set(s));
665 })
666 }
667
668 fn horizontal<U: Into<bool> + Clone + 'static>(
669 self,
670 horizontal: impl Res<U> + 'static,
671 ) -> Self {
672 let horizontal = horizontal.to_signal(self.cx);
673 self.bind(horizontal, move |handle| {
674 let horizontal = horizontal.get();
675 let horizontal = horizontal.into();
676 handle.modify(|list: &mut List| {
677 list.orientation.set(if horizontal {
678 Orientation::Horizontal
679 } else {
680 Orientation::Vertical
681 });
682 });
683 })
684 }
685
686 fn scroll_to_cursor(self, flag: bool) -> Self {
687 self.modify(|list| {
688 list.scroll_to_cursor.set(flag);
689 })
690 }
691
692 fn on_scroll(
693 self,
694 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
695 ) -> Self {
696 self.modify(|list: &mut List| list.on_scroll = Some(Box::new(callback)))
697 }
698
699 fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
700 let scrollx = scrollx.to_signal(self.cx);
701 self.bind(scrollx, move |handle| {
702 let scrollx = scrollx.get();
703 let sx = scrollx;
704 handle.modify(|list| {
705 list.scroll_x.set(sx);
706 });
707 })
708 }
709
710 fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self {
711 let scrollx = scrollx.to_signal(self.cx);
712 self.bind(scrollx, move |handle| {
713 let scrolly = scrollx.get();
714 let sy = scrolly;
715 handle.modify(|list| {
716 list.scroll_y.set(sy);
717 });
718 })
719 }
720
721 fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
722 let flag = flag.to_signal(self.cx);
723 self.bind(flag, move |handle| {
724 let show_scrollbar = flag.get();
725 let s = show_scrollbar;
726 handle.modify(|list| {
727 list.show_horizontal_scrollbar.set(s);
728 });
729 })
730 }
731
732 fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
733 let flag = flag.to_signal(self.cx);
734 self.bind(flag, move |handle| {
735 let show_scrollbar = flag.get();
736 let s = show_scrollbar;
737 handle.modify(|list| {
738 list.show_vertical_scrollbar.set(s);
739 });
740 })
741 }
742}
743
744pub struct ListItem {
746 selected: Memo<bool>,
747}
748
749impl ListItem {
750 pub fn new<'a, T: Clone + 'static, M: SignalGet<T> + 'static>(
752 cx: &'a mut Context,
753 index: usize,
754 item: M,
755 selection: impl SignalMap<BTreeSet<usize>> + SignalGet<BTreeSet<usize>>,
756 focused: impl SignalMap<Option<usize>>,
757 item_content: impl 'static + Fn(&mut Context, usize, M),
758 ) -> Handle<'a, Self> {
759 let is_focused =
760 focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index)).get();
761 let focused_signal =
762 focused.map(move |focused| focused.as_ref().is_some_and(|f| *f == index));
763 let is_selected = selection.map(move |selection| selection.contains(&index));
764 Self { selected: is_selected }
765 .build(cx, move |cx| {
766 item_content(cx, index, item);
767 })
768 .role(Role::ListBoxOption)
769 .toggle_class("focused", focused_signal)
770 .checked(selection.map(move |selection| selection.contains(&index)))
771 .bind(focused_signal, move |handle| {
772 let focused = focused_signal.get();
773 if focused != is_focused {
774 handle.cx.emit(ScrollEvent::ScrollToView(handle.entity()));
775 }
776 })
777 .on_press(move |cx| cx.emit(ListEvent::Select(index)))
778 }
779}
780
781impl View for ListItem {
782 fn element(&self) -> Option<&'static str> {
783 Some("list-item")
784 }
785
786 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
787 event.map(|window_event, _| match window_event {
788 WindowEvent::GeometryChanged(geo) => {
789 if self.selected.get() && geo.contains(GeoChanged::HEIGHT_CHANGED) {
790 cx.emit(ScrollEvent::ScrollToView(cx.current()));
791 }
792 }
793
794 _ => {}
795 });
796 }
797}