1use std::collections::{BTreeSet, VecDeque};
57use std::fmt;
58use std::ops::Range;
59use std::rc::Rc;
60
61use iced::futures::channel::mpsc;
62use iced::futures::{Stream, StreamExt, stream};
63use iced::keyboard::Modifiers;
64use iced::keyboard::key::Named;
65use iced::widget::{column, container};
66use iced::{Alignment, Border, Color, Element, Length, Padding, Subscription};
67
68use crate::data::interaction::{Look, RowMenu, pressable, ringed, row_menu, scope};
69use crate::data::rows::rows;
70use crate::keys::{self, Chord, Keymap};
71use crate::overlay::context_menu;
72use crate::theme::{Tokens, fade, radius, space};
73
74pub const ROW_HEIGHT: f32 = 32.0;
76pub const CHANNEL_CAPACITY: usize = 256;
78pub const BATCH_SIZE: usize = 256;
80pub const DEFAULT_PAGE: usize = 10;
82const FRAME_PADDING: f32 = 4.0;
84
85pub type RowId = u64;
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92pub enum Mode {
93 None,
95 #[default]
97 Single,
98 Multiple,
100}
101
102impl Mode {
103 pub const ALL: [Mode; 3] = [Mode::None, Mode::Single, Mode::Multiple];
104}
105
106pub type Sender<T> = mpsc::Sender<T>;
109
110#[derive(Debug, Clone)]
112pub enum Event<T> {
113 Ready(Sender<T>),
115 Received(Vec<T>),
117 Press(RowId, Modifiers),
119 Activate(RowId),
121 Next,
122 Previous,
123 First,
124 Last,
125 NextPage,
126 PreviousPage,
127 ExtendNext,
130 ExtendPrevious,
131 ToggleHighlighted,
133 ActivateHighlighted,
134 SelectAll,
136 Follow(bool),
139 Page(usize),
142 Focus(bool),
144}
145
146#[derive(Debug, Clone)]
148pub enum Output<T> {
149 Ready(Sender<T>),
150 Activated(usize),
152 Selected(Vec<usize>),
154}
155
156#[derive(Debug, Clone)]
159pub struct State<T> {
160 items: VecDeque<T>,
161 limit: Option<usize>,
162 dropped: u64,
164 mode: Mode,
165 selected: BTreeSet<RowId>,
166 anchor: Option<RowId>,
168 highlighted: Option<RowId>,
169 focused: bool,
170 tail: bool,
172 following: bool,
173 page: usize,
174 reveal: Option<(RowId, u64)>,
177 requests: u64,
178}
179
180impl<T> Default for State<T> {
181 fn default() -> Self {
182 Self {
183 items: VecDeque::new(),
184 limit: None,
185 dropped: 0,
186 mode: Mode::default(),
187 selected: BTreeSet::new(),
188 anchor: None,
189 highlighted: None,
190 focused: false,
191 tail: false,
192 following: false,
193 page: DEFAULT_PAGE,
194 reveal: None,
195 requests: 0,
196 }
197 }
198}
199
200impl<T> State<T> {
201 pub fn new(items: impl IntoIterator<Item = T>) -> Self {
203 Self {
204 items: items.into_iter().collect(),
205 ..Self::default()
206 }
207 }
208
209 pub fn with_mode(mut self, mode: Mode) -> Self {
210 self.mode = mode;
211 self
212 }
213
214 pub fn with_limit(mut self, limit: usize) -> Self {
217 self.limit = Some(limit.max(1));
218 self.trim();
219 self
220 }
221
222 pub fn with_follow(mut self, follow: bool) -> Self {
226 self.tail = follow;
227 self.following = follow;
228 self
229 }
230
231 pub fn mode(&self) -> Mode {
232 self.mode
233 }
234
235 pub fn len(&self) -> usize {
236 self.items.len()
237 }
238
239 pub fn is_empty(&self) -> bool {
240 self.items.is_empty()
241 }
242
243 pub fn get(&self, index: usize) -> Option<&T> {
244 self.items.get(index)
245 }
246
247 pub fn iter(&self) -> impl Iterator<Item = &T> {
249 self.items.iter()
250 }
251
252 pub fn limit(&self) -> Option<usize> {
254 self.limit
255 }
256
257 pub fn id(&self, index: usize) -> RowId {
259 self.dropped + index as u64
260 }
261
262 pub fn index(&self, id: RowId) -> Option<usize> {
264 let index = usize::try_from(id.checked_sub(self.dropped)?).ok()?;
265 (index < self.items.len()).then_some(index)
266 }
267
268 pub fn dropped(&self) -> u64 {
270 self.dropped
271 }
272
273 pub fn is_selected(&self, index: usize) -> bool {
274 self.selected.contains(&self.id(index))
275 }
276
277 pub fn selected(&self) -> Vec<usize> {
279 self.selected
280 .iter()
281 .filter_map(|&id| self.index(id))
282 .collect()
283 }
284
285 pub fn highlighted(&self) -> Option<usize> {
287 self.highlighted.and_then(|id| self.index(id))
288 }
289
290 pub fn is_focused(&self) -> bool {
291 self.focused
292 }
293
294 pub fn is_following(&self) -> bool {
296 self.tail && self.following
297 }
298
299 pub fn page_size(&self) -> usize {
301 self.page
302 }
303
304 pub fn push(&mut self, item: T) {
306 self.items.push_back(item);
307 self.trim();
308 }
309
310 pub fn extend(&mut self, items: impl IntoIterator<Item = T>) {
312 self.items.extend(items);
313 self.trim();
314 }
315
316 pub fn set_items(&mut self, items: impl IntoIterator<Item = T>) {
319 self.dropped += self.items.len() as u64;
320 self.items = items.into_iter().collect();
321 self.selected.clear();
322 self.anchor = None;
323 self.highlighted = None;
324 self.reveal = None;
325 self.trim();
326 }
327
328 pub fn clear(&mut self) {
330 self.set_items(std::iter::empty());
331 }
332
333 pub fn scroll_to(&mut self, index: usize) {
336 if index >= self.items.len() {
337 return;
338 }
339 self.requests += 1;
340 self.reveal = Some((self.id(index), self.requests));
341 self.following = index + 1 == self.items.len();
342 }
343
344 fn reveal_target(&self) -> Option<(usize, u64)> {
346 let (id, request) = self.reveal?;
347 Some((self.index(id)?, request))
348 }
349
350 pub fn update(&mut self, event: Event<T>) -> Option<Output<T>> {
352 match event {
353 Event::Ready(sender) => return Some(Output::Ready(sender)),
354 Event::Received(items) => self.extend(items),
355 Event::Press(id, modifiers) => return self.press(id, modifiers),
356 Event::Activate(id) => {
357 let index = self.index(id)?;
358 self.highlighted = Some(id);
359 return Some(Output::Activated(index));
360 }
361 Event::Next => self.step(Step::Next),
362 Event::Previous => self.step(Step::Previous),
363 Event::First => self.step(Step::First),
364 Event::Last => self.step(Step::Last),
365 Event::NextPage => self.step(Step::NextPage),
366 Event::PreviousPage => self.step(Step::PreviousPage),
367 Event::ExtendNext => return self.extend_selection(Step::Next),
368 Event::ExtendPrevious => return self.extend_selection(Step::Previous),
369 Event::ToggleHighlighted => return self.toggle_highlighted(),
370 Event::ActivateHighlighted => return self.highlighted().map(Output::Activated),
371 Event::SelectAll => return self.select_all(),
372 Event::Follow(follow) => self.following = follow,
373 Event::Page(rows) => self.page = rows.max(1),
374 Event::Focus(focused) => {
375 self.focused = focused;
376 if focused && self.highlighted().is_none() && !self.items.is_empty() {
379 let last = self.id(self.items.len() - 1);
380 let start = if self.is_following() {
381 last
382 } else {
383 self.dropped
384 };
385 self.highlighted = self.selected.first().copied().or(Some(start));
386 }
387 }
388 }
389 None
390 }
391
392 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<T>> {
394 keymap.resolve_event(key)?.event(self)
395 }
396
397 fn trim(&mut self) {
399 let Some(limit) = self.limit else {
400 return;
401 };
402 let over = self.items.len().saturating_sub(limit);
403 if over == 0 {
404 return;
405 }
406 self.items.drain(..over);
407 self.dropped += over as u64;
408 let first = self.dropped;
409 self.selected = self.selected.split_off(&first);
410 let gone = |id: &RowId| *id < first;
411 if self.highlighted.as_ref().is_some_and(gone) {
412 self.highlighted = None;
413 }
414 if self.anchor.as_ref().is_some_and(gone) {
415 self.anchor = None;
416 }
417 if self.reveal.as_ref().is_some_and(|(id, _)| gone(id)) {
418 self.reveal = None;
419 }
420 }
421
422 fn step(&mut self, step: Step) {
425 let Some(index) = target(step, self.highlighted(), self.items.len(), self.page) else {
426 return;
427 };
428 let id = self.id(index);
429 self.highlighted = Some(id);
430 self.requests += 1;
431 self.reveal = Some((id, self.requests));
432 self.following = index + 1 == self.items.len();
433 }
434
435 fn selection_output(&self, before: &BTreeSet<RowId>) -> Option<Output<T>> {
436 (self.selected != *before).then(|| Output::Selected(self.selected()))
437 }
438
439 fn press(&mut self, id: RowId, modifiers: Modifiers) -> Option<Output<T>> {
440 let index = self.index(id)?;
441 self.highlighted = Some(id);
442 if index + 1 != self.items.len() {
443 self.following = false;
444 }
445 match self.mode {
446 Mode::None => None,
447 Mode::Single => {
448 let before = std::mem::replace(&mut self.selected, BTreeSet::from([id]));
449 self.anchor = Some(id);
450 self.selection_output(&before)
451 }
452 Mode::Multiple => {
453 let before = self.selected.clone();
454 match (modifiers.shift(), modifiers.command(), self.anchor) {
455 (true, additive, Some(anchor)) => {
456 if !additive {
457 self.selected.clear();
458 }
459 self.selected.extend(span(anchor, id));
460 }
461 (_, true, _) => {
462 if !self.selected.remove(&id) {
463 let _ = self.selected.insert(id);
464 }
465 self.anchor = Some(id);
466 }
467 _ => {
468 self.selected = BTreeSet::from([id]);
469 self.anchor = Some(id);
470 }
471 }
472 self.selection_output(&before)
473 }
474 }
475 }
476
477 fn extend_selection(&mut self, step: Step) -> Option<Output<T>> {
478 if self.mode != Mode::Multiple {
479 self.step(step);
480 return None;
481 }
482 let anchor = self.anchor.or(self.highlighted);
483 self.step(step);
484 let (Some(anchor), Some(highlighted)) = (anchor, self.highlighted) else {
485 return None;
486 };
487 self.anchor = Some(anchor);
488 let before = self.selected.clone();
489 self.selected = span(anchor, highlighted).collect();
490 self.selection_output(&before)
491 }
492
493 fn toggle_highlighted(&mut self) -> Option<Output<T>> {
494 let id = self.highlighted?;
495 match self.mode {
496 Mode::None => None,
497 Mode::Single => self.press(id, Modifiers::empty()),
498 Mode::Multiple => self.press(id, Modifiers::COMMAND),
499 }
500 }
501
502 fn select_all(&mut self) -> Option<Output<T>> {
503 if self.mode != Mode::Multiple || self.items.is_empty() {
504 return None;
505 }
506 let before = self.selected.clone();
507 self.selected = (self.dropped..self.id(self.items.len())).collect();
508 self.selection_output(&before)
509 }
510}
511
512fn span(a: RowId, b: RowId) -> impl Iterator<Item = RowId> {
514 a.min(b)..=a.max(b)
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub(crate) enum Step {
520 Next,
521 Previous,
522 First,
523 Last,
524 NextPage,
525 PreviousPage,
526}
527
528pub(crate) fn target(step: Step, current: Option<usize>, len: usize, page: usize) -> Option<usize> {
532 let last = len.checked_sub(1)?;
533 let index = match (step, current) {
534 (Step::First, _) => 0,
535 (Step::Last, _) => last,
536 (Step::Next, None) => 0,
537 (Step::Previous | Step::PreviousPage | Step::NextPage, None) => last,
538 (Step::Next, Some(index)) => (index + 1).min(last),
539 (Step::Previous, Some(index)) => index.saturating_sub(1),
540 (Step::NextPage, Some(index)) => index.saturating_add(page).min(last),
541 (Step::PreviousPage, Some(index)) => index.saturating_sub(page),
542 };
543 Some(index)
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
548pub enum Action {
549 Next,
550 Previous,
551 First,
552 Last,
553 NextPage,
554 PreviousPage,
555 ExtendNext,
556 ExtendPrevious,
557 Activate,
558 Select,
559 SelectAll,
560}
561
562impl Action {
563 pub fn event<T>(self, state: &State<T>) -> Option<Event<T>> {
566 if state.is_empty() {
567 return None;
568 }
569 let highlighted = state.highlighted().is_some();
570 let multiple = state.mode() == Mode::Multiple;
571 match self {
572 Action::Next => Some(Event::Next),
573 Action::Previous => Some(Event::Previous),
574 Action::First => Some(Event::First),
575 Action::Last => Some(Event::Last),
576 Action::NextPage => Some(Event::NextPage),
577 Action::PreviousPage => Some(Event::PreviousPage),
578 Action::ExtendNext if multiple => Some(Event::ExtendNext),
579 Action::ExtendNext => Some(Event::Next),
580 Action::ExtendPrevious if multiple => Some(Event::ExtendPrevious),
581 Action::ExtendPrevious => Some(Event::Previous),
582 Action::Activate => highlighted.then_some(Event::ActivateHighlighted),
583 Action::Select => {
584 (highlighted && state.mode() != Mode::None).then_some(Event::ToggleHighlighted)
585 }
586 Action::SelectAll => multiple.then_some(Event::SelectAll),
587 }
588 }
589}
590
591impl keys::Action for Action {
592 const ALL: &'static [Self] = &[
593 Action::Next,
594 Action::Previous,
595 Action::First,
596 Action::Last,
597 Action::NextPage,
598 Action::PreviousPage,
599 Action::ExtendNext,
600 Action::ExtendPrevious,
601 Action::Activate,
602 Action::Select,
603 Action::SelectAll,
604 ];
605
606 fn defaults() -> Keymap<Self> {
607 default_keymap()
608 }
609
610 fn name(self) -> &'static str {
611 match self {
612 Action::Next => "Next",
613 Action::Previous => "Previous",
614 Action::First => "First",
615 Action::Last => "Last",
616 Action::NextPage => "NextPage",
617 Action::PreviousPage => "PreviousPage",
618 Action::ExtendNext => "ExtendNext",
619 Action::ExtendPrevious => "ExtendPrevious",
620 Action::Activate => "Activate",
621 Action::Select => "Select",
622 Action::SelectAll => "SelectAll",
623 }
624 }
625
626 fn description(self) -> &'static str {
627 match self {
628 Action::Next => "Highlights the next item.",
629 Action::Previous => "Highlights the previous item.",
630 Action::First => "Highlights the first item.",
631 Action::Last => "Highlights the last item, and follows the end in a list that follows.",
632 Action::NextPage => "Moves the highlight down by a page of rows.",
633 Action::PreviousPage => "Moves the highlight up by a page of rows.",
634 Action::ExtendNext => "Moves down and extends the selection, in a multiple selection.",
635 Action::ExtendPrevious => {
636 "Moves up and extends the selection, in a multiple selection."
637 }
638 Action::Activate => "Activates the highlighted item.",
639 Action::Select => "Selects or toggles the highlighted item.",
640 Action::SelectAll => "Selects every item, in a multiple selection.",
641 }
642 }
643}
644
645pub fn default_keymap() -> Keymap<Action> {
665 Keymap::new()
666 .bind(Chord::named(Named::ArrowDown), Action::Next)
667 .bind(Chord::named(Named::ArrowUp), Action::Previous)
668 .bind(Chord::named(Named::Home), Action::First)
669 .bind(Chord::named(Named::End), Action::Last)
670 .bind(Chord::named(Named::PageDown), Action::NextPage)
671 .bind(Chord::named(Named::PageUp), Action::PreviousPage)
672 .bind(Chord::named(Named::ArrowDown).shift(), Action::ExtendNext)
673 .bind(Chord::named(Named::ArrowUp).shift(), Action::ExtendPrevious)
674 .bind(Chord::named(Named::Enter), Action::Activate)
675 .bind(Chord::named(Named::Space), Action::Select)
676 .bind(Chord::character('a').command(), Action::SelectAll)
677}
678
679pub fn subscription<T: Send + 'static>() -> Subscription<Event<T>> {
682 Subscription::run(stream::<T>)
683}
684
685pub fn stream<T: Send + 'static>() -> impl Stream<Item = Event<T>> {
687 let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
688 stream::once(async move { Event::Ready(sender) })
689 .chain(receiver.ready_chunks(BATCH_SIZE).map(Event::Received))
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
694pub struct Status {
695 pub selected: bool,
696 pub hovered: bool,
697 pub highlighted: bool,
699 pub disabled: bool,
700}
701
702#[derive(Debug, Clone, Copy, PartialEq)]
704pub struct RowStyle {
705 pub background: Option<Color>,
706 pub ring: Option<Color>,
708}
709
710pub fn row_style(tokens: &Tokens, status: Status) -> RowStyle {
714 if status.disabled {
715 return RowStyle {
716 background: None,
717 ring: None,
718 };
719 }
720 let background = if status.selected {
721 Some(tokens.accent)
722 } else if status.hovered {
723 Some(fade(tokens.accent, 0.6))
724 } else {
725 None
726 };
727 RowStyle {
728 background,
729 ring: status.highlighted.then_some(tokens.ring),
730 }
731}
732
733pub fn frame_style(tokens: &Tokens) -> container::Style {
735 container::Style {
736 border: Border {
737 color: tokens.border,
738 width: 1.0,
739 radius: radius::MD.into(),
740 },
741 ..container::Style::default()
742 }
743}
744
745type OnEvent<'a, T, Message> = dyn Fn(Event<T>) -> Message + 'a;
746type RowView<'a, T, Message> = dyn Fn(usize, &'a T) -> Element<'a, Message> + 'a;
747type BuildMenu<'a, Message> =
748 dyn FnOnce(Keymap<context_menu::Action>) -> RowMenu<'a, RowId, Message> + 'a;
749
750pub struct VirtualList<'a, T, Message> {
752 state: &'a State<T>,
753 row: Rc<RowView<'a, T, Message>>,
754 row_height: f32,
755 width: Length,
756 height: Length,
757 keymap: Keymap<Action>,
758 menu: Option<Box<BuildMenu<'a, Message>>>,
759 menu_keymap: Keymap<context_menu::Action>,
760 on_event: Option<Box<OnEvent<'a, T, Message>>>,
761}
762
763pub fn virtual_list<'a, T, Message>(
767 state: &'a State<T>,
768 row: impl Fn(usize, &'a T) -> Element<'a, Message> + 'a,
769) -> VirtualList<'a, T, Message> {
770 VirtualList {
771 state,
772 row: Rc::new(row),
773 row_height: ROW_HEIGHT,
774 width: Length::Fill,
775 height: Length::Shrink,
776 keymap: default_keymap(),
777 menu: None,
778 menu_keymap: context_menu::default_keymap(),
779 on_event: None,
780 }
781}
782
783impl<T, Message> fmt::Debug for VirtualList<'_, T, Message> {
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 f.debug_struct("VirtualList")
786 .field("len", &self.state.len())
787 .field("row_height", &self.row_height)
788 .field("width", &self.width)
789 .field("height", &self.height)
790 .field("keymap", &self.keymap)
791 .finish_non_exhaustive()
792 }
793}
794
795impl<'a, T, Message> VirtualList<'a, T, Message> {
796 pub fn row_height(mut self, height: f32) -> Self {
798 self.row_height = height.max(1.0);
799 self
800 }
801
802 pub fn width(mut self, width: impl Into<Length>) -> Self {
803 self.width = width.into();
804 self
805 }
806
807 pub fn height(mut self, height: impl Into<Length>) -> Self {
810 self.height = height.into();
811 self
812 }
813
814 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
816 self.keymap = keymap;
817 self
818 }
819
820 pub fn on_event(mut self, on_event: impl Fn(Event<T>) -> Message + 'a) -> Self {
821 self.on_event = Some(Box::new(on_event));
822 self
823 }
824
825 pub fn context_menu<MenuId>(
830 mut self,
831 menu: &'a context_menu::State<MenuId, RowId>,
832 on_event: impl Fn(context_menu::Event<MenuId, RowId>) -> Message + 'a,
833 ) -> Self
834 where
835 MenuId: Copy + PartialEq + 'a,
836 Message: Clone + 'a,
837 {
838 let on_event: Rc<dyn Fn(context_menu::Event<MenuId, RowId>) -> Message + 'a> =
839 Rc::new(on_event);
840 self.menu = Some(Box::new(move |keymap| row_menu(menu, keymap, on_event)));
841 self
842 }
843
844 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
846 self.menu_keymap = keymap;
847 self
848 }
849
850 pub fn is_enabled(&self) -> bool {
851 self.on_event.is_some()
852 }
853}
854
855impl<'a, T, Message> From<VirtualList<'a, T, Message>> for Element<'a, Message>
856where
857 T: 'a,
858 Message: Clone + 'a,
859{
860 fn from(list: VirtualList<'a, T, Message>) -> Self {
861 let VirtualList {
862 state,
863 row,
864 row_height,
865 width,
866 height,
867 keymap,
868 menu,
869 menu_keymap,
870 on_event,
871 } = list;
872 let on_event: Option<Rc<OnEvent<'a, T, Message>>> = on_event.map(Rc::from);
873 let menu = menu
874 .filter(|_| on_event.is_some())
875 .map(|build| Rc::new(build(menu_keymap)));
876
877 let build = {
878 let on_event = on_event.clone();
879 let menu = menu.clone();
880 let menu_target = menu.as_ref().and_then(|menu| menu.target());
881 move |range: Range<usize>| -> Element<'a, Message> {
882 column(range.filter_map(|index| {
883 let item = state.get(index)?;
884 let content = row(index, item);
885 let id = state.id(index);
886 let ringed = ringed(
887 menu_target.as_ref(),
888 &id,
889 state.focused && state.highlighted == Some(id),
890 );
891 let framed =
892 row_frame(state, index, content, row_height, ringed, on_event.as_ref());
893 Some(match &menu {
894 Some(menu) => menu.wrap(state.id(index), framed),
895 None => framed,
896 })
897 }))
898 .into()
899 }
900 };
901 let inner = if height == Length::Shrink {
902 Length::Shrink
903 } else {
904 Length::Fill
905 };
906 let mut list = rows(state.len(), row_height, build)
907 .reveal(state.reveal_target())
908 .follow(state.is_following())
909 .origin(state.dropped())
910 .height(inner);
911 if let Some(on_event) = on_event.clone() {
912 let on_page = on_event.clone();
913 list = list.on_page(move |rows| on_page(Event::Page(rows)));
914 if state.tail {
915 let on_follow = on_event.clone();
916 list = list.on_follow(move |follow| on_follow(Event::Follow(follow)));
917 }
918 }
919 let framed = container(list)
920 .padding(FRAME_PADDING)
921 .width(width)
922 .height(height)
923 .style(|theme| frame_style(&Tokens::of(theme)));
924
925 let Some(on_event) = on_event else {
926 return framed.into();
927 };
928 let on_focus = on_event.clone();
929 scope(framed)
930 .on_focus(move |focused| on_focus(Event::Focus(focused)))
931 .on_key(move |key| {
932 let target = state.highlighted().map(|index| state.id(index));
933 let opened = menu
934 .as_ref()
935 .zip(target)
936 .and_then(|(menu, target)| menu.key(key, target));
937 opened.or_else(|| state.key_event(&keymap, key).map(&*on_event))
938 })
939 .into()
940 }
941}
942
943fn row_frame<'a, T, Message>(
945 state: &'a State<T>,
946 index: usize,
947 content: Element<'a, Message>,
948 row_height: f32,
949 ringed: bool,
950 on_event: Option<&Rc<OnEvent<'a, T, Message>>>,
951) -> Element<'a, Message>
952where
953 T: 'a,
954 Message: Clone + 'a,
955{
956 let id = state.id(index);
957 let status = Status {
958 selected: state.mode != Mode::None && state.selected.contains(&id),
959 hovered: false,
960 highlighted: ringed,
961 disabled: on_event.is_none(),
962 };
963 let body = container(content)
964 .padding(Padding::ZERO.left(space::SM).right(space::SM))
965 .width(Length::Fill)
966 .height(row_height)
967 .align_y(Alignment::Center);
968 let row = pressable(body, move |theme, hovered| {
969 let style = row_style(&Tokens::of(theme), Status { hovered, ..status });
970 Look {
971 background: style.background,
972 border: Border {
973 color: style.ring.unwrap_or(Color::TRANSPARENT),
974 width: if style.ring.is_some() { 1.0 } else { 0.0 },
975 radius: radius::SM.into(),
976 },
977 divider: None,
978 }
979 });
980 let Some(on_event) = on_event.cloned() else {
981 return row.into();
982 };
983 let activate = on_event(Event::Activate(id));
984 row.on_press(move |modifiers| on_event(Event::Press(id, modifiers)))
985 .on_double_click(Some(activate))
986 .into()
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992 use crate::keys::Action as _;
993 use crate::theme::{dark, light};
994 use iced::futures::executor::block_on;
995 use iced::futures::{SinkExt, StreamExt};
996 use iced::keyboard::Key;
997
998 fn numbers(count: usize) -> State<usize> {
999 State::new(0..count)
1000 }
1001
1002 fn selected(output: Option<Output<usize>>) -> Option<Vec<usize>> {
1003 match output {
1004 Some(Output::Selected(indices)) => Some(indices),
1005 _ => None,
1006 }
1007 }
1008
1009 fn press(key: Key, modifiers: Modifiers) -> keys::Event {
1010 keys::Event { key, modifiers }
1011 }
1012
1013 #[test]
1014 fn a_new_list_holds_its_items_and_selects_one_at_a_time() {
1015 let state = numbers(5);
1016 assert_eq!(state.len(), 5);
1017 assert!(!state.is_empty());
1018 assert_eq!(state.mode(), Mode::Single);
1019 assert_eq!(state.get(2), Some(&2));
1020 assert_eq!(state.get(9), None);
1021 assert_eq!(state.highlighted(), None);
1022 assert!(!state.is_following());
1023 assert_eq!(state.page_size(), DEFAULT_PAGE);
1024 assert_eq!(state.limit(), None);
1025 assert!(State::<u8>::default().is_empty());
1026 assert_eq!(Mode::ALL.len(), 3);
1027 }
1028
1029 #[test]
1030 fn ids_equal_indices_until_items_are_dropped() {
1031 let mut state = numbers(3).with_limit(3);
1032 assert_eq!(state.id(1), 1);
1033 state.extend([3, 4]);
1034 assert_eq!(state.dropped(), 2);
1035 assert_eq!(state.id(0), 2);
1036 assert_eq!(state.index(2), Some(0));
1037 assert_eq!(state.index(1), None, "dropped");
1038 assert_eq!(state.index(9), None, "not yet arrived");
1039 assert_eq!(state.iter().copied().collect::<Vec<_>>(), [2, 3, 4]);
1040 }
1041
1042 #[test]
1043 fn a_press_selects_one_item_in_single_mode() {
1044 let mut state = numbers(5);
1045 let output = state.update(Event::Press(2, Modifiers::empty()));
1046 assert_eq!(selected(output), Some(vec![2]));
1047 assert_eq!(state.highlighted(), Some(2));
1048 let again = state.update(Event::Press(2, Modifiers::empty()));
1049 assert!(again.is_none(), "no change, no output");
1050 assert_eq!(
1051 selected(state.update(Event::Press(4, Modifiers::empty()))),
1052 Some(vec![4])
1053 );
1054 assert!(state.update(Event::Press(99, Modifiers::empty())).is_none());
1055 }
1056
1057 #[test]
1058 fn modifier_presses_toggle_and_extend_in_multiple_mode() {
1059 let mut state = numbers(10).with_mode(Mode::Multiple);
1060 let _ = state.update(Event::Press(2, Modifiers::empty()));
1061 let output = state.update(Event::Press(5, Modifiers::SHIFT));
1062 assert_eq!(selected(output), Some(vec![2, 3, 4, 5]));
1063 let output = state.update(Event::Press(8, Modifiers::COMMAND));
1064 assert_eq!(selected(output), Some(vec![2, 3, 4, 5, 8]));
1065 let output = state.update(Event::Press(3, Modifiers::COMMAND));
1066 assert_eq!(selected(output), Some(vec![2, 4, 5, 8]));
1067 let output = state.update(Event::Press(1, Modifiers::SHIFT | Modifiers::COMMAND));
1068 assert_eq!(
1069 selected(output),
1070 Some(vec![1, 2, 3, 4, 5, 8]),
1071 "added range"
1072 );
1073 let output = state.update(Event::Press(6, Modifiers::empty()));
1074 assert_eq!(selected(output), Some(vec![6]));
1075 }
1076
1077 #[test]
1078 fn mode_none_highlights_without_selecting() {
1079 let mut state = numbers(5).with_mode(Mode::None);
1080 assert!(state.update(Event::Press(2, Modifiers::empty())).is_none());
1081 assert_eq!(state.highlighted(), Some(2));
1082 assert!(state.selected().is_empty());
1083 assert!(Action::Select.event(&state).is_none());
1084 }
1085
1086 #[test]
1087 fn steps_move_the_highlight_within_bounds() {
1088 let mut state = numbers(30);
1089 let _ = state.update(Event::Next);
1090 assert_eq!(state.highlighted(), Some(0), "starts at the top");
1091 let _ = state.update(Event::Previous);
1092 assert_eq!(state.highlighted(), Some(0), "stays at the top");
1093 let _ = state.update(Event::Page(8));
1094 let _ = state.update(Event::NextPage);
1095 assert_eq!(state.highlighted(), Some(8));
1096 let _ = state.update(Event::Last);
1097 assert_eq!(state.highlighted(), Some(29));
1098 let _ = state.update(Event::Next);
1099 assert_eq!(state.highlighted(), Some(29), "stays at the end");
1100 let _ = state.update(Event::NextPage);
1101 assert_eq!(state.highlighted(), Some(29));
1102 let _ = state.update(Event::PreviousPage);
1103 assert_eq!(state.highlighted(), Some(21));
1104 let _ = state.update(Event::First);
1105 assert_eq!(state.highlighted(), Some(0));
1106 let _ = state.update(Event::PreviousPage);
1107 assert_eq!(state.highlighted(), Some(0));
1108 }
1109
1110 #[test]
1111 fn up_from_nothing_starts_at_the_end() {
1112 let mut state = numbers(5);
1113 let _ = state.update(Event::Previous);
1114 assert_eq!(state.highlighted(), Some(4));
1115 let mut empty = State::<u8>::default();
1116 let _ = empty.update(Event::Next);
1117 assert_eq!(empty.highlighted(), None);
1118 }
1119
1120 #[test]
1121 fn a_page_is_at_least_one_row() {
1122 let mut state = numbers(5);
1123 let _ = state.update(Event::Page(0));
1124 assert_eq!(state.page_size(), 1);
1125 }
1126
1127 #[test]
1128 fn steps_reveal_the_highlight_but_focus_does_not_scroll() {
1129 let mut state = numbers(100);
1130 let _ = state.update(Event::Focus(true));
1131 assert_eq!(state.highlighted(), Some(0));
1132 assert_eq!(state.reveal_target(), None, "focus never scrolls");
1133 let _ = state.update(Event::Last);
1134 assert_eq!(state.reveal_target(), Some((99, 1)));
1135 let _ = state.update(Event::Last);
1136 assert_eq!(
1137 state.reveal_target(),
1138 Some((99, 2)),
1139 "the same row is revealed again"
1140 );
1141 }
1142
1143 #[test]
1144 fn focus_starts_at_the_selection() {
1145 let mut state = numbers(10);
1146 let _ = state.update(Event::Press(6, Modifiers::empty()));
1147 state.highlighted = None;
1148 let _ = state.update(Event::Focus(true));
1149 assert!(state.is_focused());
1150 assert_eq!(state.highlighted(), Some(6));
1151 let _ = state.update(Event::Focus(false));
1152 assert!(!state.is_focused());
1153 }
1154
1155 #[test]
1156 fn scroll_to_reveals_without_highlighting() {
1157 let mut state = numbers(100).with_follow(true);
1158 state.scroll_to(40);
1159 assert_eq!(state.reveal_target(), Some((40, 1)));
1160 assert_eq!(state.highlighted(), None);
1161 assert!(!state.is_following());
1162 state.scroll_to(99);
1163 assert!(state.is_following(), "the last row is the end");
1164 state.scroll_to(500);
1165 assert_eq!(
1166 state.reveal_target(),
1167 Some((99, 2)),
1168 "out of range is ignored"
1169 );
1170 }
1171
1172 #[test]
1173 fn extending_selects_from_the_anchor() {
1174 let mut state = numbers(10).with_mode(Mode::Multiple);
1175 let _ = state.update(Event::Press(3, Modifiers::empty()));
1176 let _ = state.update(Event::ExtendNext);
1177 let output = state.update(Event::ExtendNext);
1178 assert_eq!(selected(output), Some(vec![3, 4, 5]));
1179 let output = state.update(Event::ExtendPrevious);
1180 assert_eq!(selected(output), Some(vec![3, 4]));
1181
1182 let mut single = numbers(10);
1183 assert!(single.update(Event::ExtendNext).is_none());
1184 assert_eq!(single.highlighted(), Some(0), "moves without selecting");
1185 }
1186
1187 #[test]
1188 fn select_all_and_toggle_follow_the_mode() {
1189 let mut state = numbers(4).with_mode(Mode::Multiple);
1190 assert_eq!(
1191 selected(state.update(Event::SelectAll)),
1192 Some(vec![0, 1, 2, 3])
1193 );
1194 let _ = state.update(Event::Previous);
1195 assert_eq!(state.highlighted(), Some(3));
1196 assert_eq!(
1197 selected(state.update(Event::ToggleHighlighted)),
1198 Some(vec![0, 1, 2])
1199 );
1200
1201 let mut single = numbers(4);
1202 assert!(single.update(Event::SelectAll).is_none());
1203 let _ = single.update(Event::Next);
1204 assert_eq!(
1205 selected(single.update(Event::ToggleHighlighted)),
1206 Some(vec![0])
1207 );
1208 }
1209
1210 #[test]
1211 fn activating_reports_the_index() {
1212 let mut state = numbers(4);
1213 assert!(matches!(
1214 state.update(Event::Activate(2)),
1215 Some(Output::Activated(2))
1216 ));
1217 assert_eq!(state.highlighted(), Some(2));
1218 assert!(matches!(
1219 state.update(Event::ActivateHighlighted),
1220 Some(Output::Activated(2))
1221 ));
1222 assert!(state.update(Event::Activate(40)).is_none());
1223 }
1224
1225 #[test]
1226 fn the_limit_drops_the_oldest_items_and_their_selection() {
1227 let mut state = numbers(5).with_mode(Mode::Multiple).with_limit(5);
1228 let _ = state.update(Event::Press(1, Modifiers::empty()));
1229 let _ = state.update(Event::Press(3, Modifiers::COMMAND));
1230 state.scroll_to(0);
1231 let _ = state.update(Event::Received(vec![5, 6]));
1232 assert_eq!(state.len(), 5);
1233 assert_eq!(state.get(0), Some(&2));
1234 assert_eq!(state.selected(), [1], "item 3 is now at index 1");
1235 assert_eq!(state.highlighted(), Some(1));
1236 assert_eq!(state.reveal_target(), None, "the revealed row is gone");
1237 let _ = state.update(Event::Received(vec![7, 8]));
1238 assert!(state.selected().is_empty());
1239 assert_eq!(state.highlighted(), None);
1240 assert!(state.anchor.is_none());
1241 assert_eq!(numbers(5).with_limit(0).len(), 1, "a zero limit keeps one");
1242 }
1243
1244 #[test]
1245 fn set_items_and_clear_reset_the_selection() {
1246 let mut state = numbers(5);
1247 let _ = state.update(Event::Press(2, Modifiers::empty()));
1248 state.set_items([10, 11]);
1249 assert_eq!(state.len(), 2);
1250 assert!(state.selected().is_empty());
1251 assert_eq!(state.highlighted(), None);
1252 assert_eq!(state.id(0), 5, "ids carry on");
1253 assert!(
1254 state.update(Event::Press(2, Modifiers::empty())).is_none(),
1255 "a press from the old items is ignored"
1256 );
1257 state.push(12);
1258 assert_eq!(state.len(), 3);
1259 state.clear();
1260 assert!(state.is_empty());
1261 }
1262
1263 #[test]
1264 fn following_stops_off_the_end_and_resumes_at_it() {
1265 let mut state = numbers(50).with_follow(true);
1266 assert!(state.is_following());
1267 let _ = state.update(Event::Follow(false));
1268 assert!(!state.is_following());
1269 let _ = state.update(Event::Follow(true));
1270 assert!(state.is_following());
1271 let _ = state.update(Event::Previous);
1272 assert_eq!(state.highlighted(), Some(49));
1273 assert!(state.is_following(), "the last row is still the end");
1274 let _ = state.update(Event::Previous);
1275 assert!(!state.is_following());
1276 let _ = state.update(Event::Last);
1277 assert!(state.is_following());
1278 let _ = state.update(Event::Press(10, Modifiers::empty()));
1279 assert!(!state.is_following(), "pressing a row off the end stops");
1280 let _ = state.update(Event::Received(vec![50]));
1281 assert!(!state.is_following());
1282 }
1283
1284 #[test]
1285 fn a_list_without_follow_never_follows() {
1286 let mut state = numbers(5);
1287 let _ = state.update(Event::Last);
1288 assert!(!state.is_following());
1289 let _ = state.update(Event::Follow(true));
1290 assert!(!state.is_following());
1291 }
1292
1293 #[test]
1294 fn focus_on_a_following_list_lands_at_the_end() {
1295 let mut state = numbers(20).with_follow(true);
1296 let _ = state.update(Event::Focus(true));
1297 assert_eq!(state.highlighted(), Some(19));
1298 }
1299
1300 #[test]
1301 fn the_default_keymap_maps_to_events() {
1302 let mut state = numbers(10).with_mode(Mode::Multiple);
1303 let keymap = default_keymap();
1304 let resolve = |state: &State<usize>, key: Key, modifiers: Modifiers| {
1305 state.key_event(&keymap, &press(key, modifiers))
1306 };
1307 let named = |named: Named| Key::Named(named);
1308 assert!(matches!(
1309 resolve(&state, named(Named::ArrowDown), Modifiers::empty()),
1310 Some(Event::Next)
1311 ));
1312 assert!(matches!(
1313 resolve(&state, named(Named::PageUp), Modifiers::empty()),
1314 Some(Event::PreviousPage)
1315 ));
1316 assert!(matches!(
1317 resolve(&state, named(Named::End), Modifiers::empty()),
1318 Some(Event::Last)
1319 ));
1320 assert!(matches!(
1321 resolve(&state, named(Named::ArrowDown), Modifiers::SHIFT),
1322 Some(Event::ExtendNext)
1323 ));
1324 assert!(matches!(
1325 resolve(&state, Key::Character("a".into()), Modifiers::COMMAND),
1326 Some(Event::SelectAll)
1327 ));
1328 assert!(
1329 resolve(&state, named(Named::Enter), Modifiers::empty()).is_none(),
1330 "nothing highlighted to activate"
1331 );
1332 let _ = state.update(Event::Next);
1333 assert!(matches!(
1334 resolve(&state, named(Named::Enter), Modifiers::empty()),
1335 Some(Event::ActivateHighlighted)
1336 ));
1337 assert!(matches!(
1338 resolve(&state, named(Named::Space), Modifiers::empty()),
1339 Some(Event::ToggleHighlighted)
1340 ));
1341 assert!(
1342 resolve(
1343 &State::<usize>::default(),
1344 named(Named::ArrowDown),
1345 Modifiers::empty()
1346 )
1347 .is_none(),
1348 "an empty list leaves keys alone"
1349 );
1350 }
1351
1352 #[test]
1353 fn keymaps_can_be_overridden_and_unbound() {
1354 let keymap = default_keymap()
1355 .bind(Chord::character('j'), Action::Next)
1356 .unbind(&Chord::named(Named::ArrowDown));
1357 let state = numbers(3);
1358 let j = press(Key::Character("j".into()), Modifiers::empty());
1359 assert!(matches!(state.key_event(&keymap, &j), Some(Event::Next)));
1360 let down = press(Key::Named(Named::ArrowDown), Modifiers::empty());
1361 assert!(state.key_event(&keymap, &down).is_none());
1362 }
1363
1364 #[test]
1365 fn extend_actions_fall_back_to_moving_in_single_mode() {
1366 let state = numbers(3);
1367 assert!(matches!(
1368 Action::ExtendNext.event(&state),
1369 Some(Event::Next)
1370 ));
1371 assert!(matches!(
1372 Action::ExtendPrevious.event(&state),
1373 Some(Event::Previous)
1374 ));
1375 assert!(Action::SelectAll.event(&state).is_none());
1376 }
1377
1378 #[test]
1379 fn every_action_is_named_and_described() {
1380 for action in Action::ALL {
1381 assert!(!action.name().is_empty());
1382 assert!(action.description().ends_with('.'));
1383 assert!(!default_keymap().chords(action).is_empty(), "{action:?}");
1384 }
1385 }
1386
1387 #[test]
1388 fn row_styles_cover_every_status_in_both_themes() {
1389 for theme in [light(), dark()] {
1390 let tokens = Tokens::of(&theme);
1391 let idle = row_style(&tokens, Status::default());
1392 assert_eq!(idle.background, None);
1393 assert_eq!(idle.ring, None);
1394 let selected = row_style(
1395 &tokens,
1396 Status {
1397 selected: true,
1398 ..Status::default()
1399 },
1400 );
1401 assert_eq!(selected.background, Some(tokens.accent));
1402 let hovered = row_style(
1403 &tokens,
1404 Status {
1405 hovered: true,
1406 ..Status::default()
1407 },
1408 );
1409 assert_eq!(hovered.background, Some(fade(tokens.accent, 0.6)));
1410 let highlighted = row_style(
1411 &tokens,
1412 Status {
1413 highlighted: true,
1414 ..Status::default()
1415 },
1416 );
1417 assert_eq!(highlighted.ring, Some(tokens.ring));
1418 let disabled = row_style(
1419 &tokens,
1420 Status {
1421 disabled: true,
1422 hovered: true,
1423 highlighted: true,
1424 selected: true,
1425 },
1426 );
1427 assert_eq!(disabled.background, None);
1428 assert_eq!(disabled.ring, None);
1429 #[cfg(feature = "tree")]
1430 {
1431 use crate::data::tree;
1432 let selected_node = tree::Status {
1433 selected: true,
1434 ..tree::Status::default()
1435 };
1436 let node = tree::row_style(&tokens, selected_node);
1437 assert_eq!(selected.background, node.background, "rows match a tree's");
1438 }
1439 assert_eq!(frame_style(&tokens).border.color, tokens.border);
1440 }
1441 }
1442
1443 #[test]
1444 fn the_builder_has_sensible_defaults() {
1445 let state = numbers(3);
1446 let list: VirtualList<'_, usize, ()> =
1447 virtual_list(&state, |_, _| iced::widget::text("").into());
1448 assert!(!list.is_enabled());
1449 assert_eq!(list.row_height, ROW_HEIGHT);
1450 assert_eq!(list.height, Length::Shrink);
1451 assert!(list.menu.is_none());
1452 assert_eq!(list.menu_keymap, context_menu::default_keymap());
1453 let menu: context_menu::State<u8, RowId> = context_menu::State::new([]);
1454 let list = list
1455 .row_height(0.0)
1456 .height(200)
1457 .context_menu(&menu, |_| ())
1458 .context_menu_keymap(Keymap::new())
1459 .on_event(|_| ());
1460 assert!(list.menu.is_some());
1461 assert!(list.menu_keymap.is_empty());
1462 assert!(list.is_enabled());
1463 assert_eq!(list.row_height, 1.0);
1464 assert_eq!(list.height, Length::Fixed(200.0));
1465 assert!(format!("{list:?}").contains("VirtualList"));
1466 }
1467
1468 #[test]
1469 fn stream_emits_ready_first_then_batches() {
1470 let mut events = Box::pin(stream::<u32>());
1471 let Some(Event::Ready(mut sender)) = block_on(events.next()) else {
1472 panic!("first event must be Ready");
1473 };
1474 block_on(async {
1475 for item in 0..3 {
1476 sender.send(item).await.expect("channel open");
1477 }
1478 });
1479 let Some(Event::Received(batch)) = block_on(events.next()) else {
1480 panic!("then a batch");
1481 };
1482 assert_eq!(batch, [0, 1, 2]);
1483 }
1484
1485 #[test]
1486 fn bursts_arrive_in_batches_of_the_batch_size() {
1487 let items = stream::iter(0..BATCH_SIZE + 3);
1488 let sizes: Vec<usize> = block_on(
1489 items
1490 .ready_chunks(BATCH_SIZE)
1491 .map(|batch| batch.len())
1492 .collect(),
1493 );
1494 assert_eq!(sizes, [BATCH_SIZE, 3]);
1495 }
1496
1497 #[test]
1498 fn ready_passes_the_sender_on() {
1499 let (sender, _receiver) = mpsc::channel(1);
1500 let output = State::<u8>::default().update(Event::Ready(sender));
1501 assert!(matches!(output, Some(Output::Ready(_))));
1502 }
1503}