Skip to main content

iced_cube/data/
virtual_list.rs

1//! A long list of equally tall rows that only builds the rows in view, so
2//! a hundred thousand items scroll as smoothly as ten.
3//!
4//! [`State`] holds the items, the selection in the chosen [`Mode`] and the
5//! keyboard highlight. Its [`update`](State::update) handles presses,
6//! keyboard navigation and items arriving from the channel that
7//! [`subscription`] owns. The app draws each row with a closure that gets
8//! the item's index and the item.
9//!
10//! A list made [`with_follow`](State::with_follow) sticks to its end as
11//! items arrive, like a feed or a log. Scrolling up stops following, and
12//! scrolling back to the end, or pressing End, starts again. A list made
13//! [`with_limit`](State::with_limit) drops its oldest items once it holds
14//! that many, so a stream never grows without bound.
15//!
16//! The list resolves its keys through a [`Keymap`] of [`Action`]s while it
17//! has focus, which it takes when pressed; see [`default_keymap`].
18//!
19//! ```no_run
20//! use iced::widget::text;
21//! use iced::{Element, Subscription};
22//! use iced_cube::data::virtual_list::{self, Event, Output, State, virtual_list};
23//!
24//! #[derive(Debug, Clone)]
25//! enum Message {
26//!     Feed(Event<String>),
27//! }
28//!
29//! struct App {
30//!     feed: State<String>,
31//!     sender: Option<virtual_list::Sender<String>>,
32//! }
33//!
34//! impl App {
35//!     fn update(&mut self, message: Message) {
36//!         let Message::Feed(event) = message;
37//!         if let Some(Output::Ready(sender)) = self.feed.update(event) {
38//!             // Hand clones of the sender to producers.
39//!             self.sender = Some(sender);
40//!         }
41//!     }
42//!
43//!     fn view(&self) -> Element<'_, Message> {
44//!         virtual_list(&self.feed, |_, line: &String| text(line.as_str()).into())
45//!             .height(320)
46//!             .on_event(Message::Feed)
47//!             .into()
48//!     }
49//!
50//!     fn subscription(&self) -> Subscription<Message> {
51//!         virtual_list::subscription().map(Message::Feed)
52//!     }
53//! }
54//! ```
55
56use 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
74/// The default height of every row.
75pub const ROW_HEIGHT: f32 = 32.0;
76/// How many items a producer can send before it has to wait.
77pub const CHANNEL_CAPACITY: usize = 256;
78/// The most items delivered in one [`Event::Received`].
79pub const BATCH_SIZE: usize = 256;
80/// Rows a page key moves until the list reports how many fit.
81pub const DEFAULT_PAGE: usize = 10;
82/// Space between the frame and the rows.
83const FRAME_PADDING: f32 = 4.0;
84
85/// Names an item for as long as it is in the list: its position counted
86/// from the first item the list ever held, so it stays the same when
87/// older items are dropped. Without a limit it equals the index.
88pub type RowId = u64;
89
90/// How items are selected.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92pub enum Mode {
93    /// Items can be highlighted and activated, but not selected.
94    None,
95    /// One item at a time.
96    #[default]
97    Single,
98    /// Ctrl or Cmd click toggles an item, Shift click selects a range.
99    Multiple,
100}
101
102impl Mode {
103    pub const ALL: [Mode; 3] = [Mode::None, Mode::Single, Mode::Multiple];
104}
105
106/// The sending half of the channel items arrive on. Clone it for each
107/// producer.
108pub type Sender<T> = mpsc::Sender<T>;
109
110/// Everything that changes a list.
111#[derive(Debug, Clone)]
112pub enum Event<T> {
113    /// The channel is open. Keep the sender and hand clones to producers.
114    Ready(Sender<T>),
115    /// Items from the channel, oldest first. They are appended.
116    Received(Vec<T>),
117    /// The pointer pressed a row with these modifiers held.
118    Press(RowId, Modifiers),
119    /// A row was double clicked.
120    Activate(RowId),
121    Next,
122    Previous,
123    First,
124    Last,
125    NextPage,
126    PreviousPage,
127    /// Moves the highlight and extends the selection to it, in
128    /// [`Mode::Multiple`].
129    ExtendNext,
130    ExtendPrevious,
131    /// Selects or toggles the highlighted item, by [`Mode`].
132    ToggleHighlighted,
133    ActivateHighlighted,
134    /// Selects every item, in [`Mode::Multiple`].
135    SelectAll,
136    /// Starts or stops following the end. The list sends `false` when the
137    /// user scrolls away from the end, and `true` when they scroll back.
138    Follow(bool),
139    /// How many rows fit in the view. The list sends this itself, and the
140    /// page keys move by it.
141    Page(usize),
142    /// The list gained or lost focus. The list sends this itself.
143    Focus(bool),
144}
145
146/// What the app may need to act on after an [`Event`].
147#[derive(Debug, Clone)]
148pub enum Output<T> {
149    Ready(Sender<T>),
150    /// An item was activated with Enter or a double click. Holds its index.
151    Activated(usize),
152    /// The selection changed. Holds every selected index, in order.
153    Selected(Vec<usize>),
154}
155
156/// The items, the selection, the highlight and whether the list follows
157/// its end.
158#[derive(Debug, Clone)]
159pub struct State<T> {
160    items: VecDeque<T>,
161    limit: Option<usize>,
162    /// Items dropped from the front so far, which is the first item's id.
163    dropped: u64,
164    mode: Mode,
165    selected: BTreeSet<RowId>,
166    /// Where a Shift click or Shift arrow range starts.
167    anchor: Option<RowId>,
168    highlighted: Option<RowId>,
169    focused: bool,
170    /// Whether the list can follow its end at all.
171    tail: bool,
172    following: bool,
173    page: usize,
174    /// The item to scroll into view, and a count that changes with every
175    /// request so the same item can be revealed again.
176    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    /// A list in [`Mode::Single`] holding `items`.
202    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    /// Keeps at most `limit` items, dropping the oldest as new ones
215    /// arrive. A limit of zero is treated as one.
216    pub fn with_limit(mut self, limit: usize) -> Self {
217        self.limit = Some(limit.max(1));
218        self.trim();
219        self
220    }
221
222    /// Follows the end as items arrive, starting now. Scrolling away stops
223    /// following, and scrolling back to the end or pressing End starts it
224    /// again.
225    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    /// The items, oldest first.
248    pub fn iter(&self) -> impl Iterator<Item = &T> {
249        self.items.iter()
250    }
251
252    /// The most items kept, if there is a limit.
253    pub fn limit(&self) -> Option<usize> {
254        self.limit
255    }
256
257    /// The id of the item at `index`.
258    pub fn id(&self, index: usize) -> RowId {
259        self.dropped + index as u64
260    }
261
262    /// The index of the item with `id`, if it is still in the list.
263    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    /// How many items have been dropped from the front so far.
269    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    /// Every selected index, in order.
278    pub fn selected(&self) -> Vec<usize> {
279        self.selected
280            .iter()
281            .filter_map(|&id| self.index(id))
282            .collect()
283    }
284
285    /// The index the keyboard acts on.
286    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    /// Whether the list is following its end.
295    pub fn is_following(&self) -> bool {
296        self.tail && self.following
297    }
298
299    /// How many rows the page keys move.
300    pub fn page_size(&self) -> usize {
301        self.page
302    }
303
304    /// Appends an item, dropping the oldest when over the limit.
305    pub fn push(&mut self, item: T) {
306        self.items.push_back(item);
307        self.trim();
308    }
309
310    /// Appends items, dropping the oldest when over the limit.
311    pub fn extend(&mut self, items: impl IntoIterator<Item = T>) {
312        self.items.extend(items);
313        self.trim();
314    }
315
316    /// Replaces every item. The selection and highlight are cleared, and
317    /// ids carry on from where the old items ended.
318    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    /// Removes every item.
329    pub fn clear(&mut self) {
330        self.set_items(std::iter::empty());
331    }
332
333    /// Scrolls the item at `index` into view without highlighting it. A
334    /// following list stops following unless it is the last item.
335    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    /// The row to keep in view and its request count, for the view.
345    fn reveal_target(&self) -> Option<(usize, u64)> {
346        let (id, request) = self.reveal?;
347        Some((self.index(id)?, request))
348    }
349
350    /// Applies an event and returns what the app may need to act on.
351    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                // Focus lands on the first selected item, or the item at
377                // the end being followed, or the first. It does not scroll.
378                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    /// Turns a key press into an event through `keymap`.
393    pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<T>> {
394        keymap.resolve_event(key)?.event(self)
395    }
396
397    /// Drops the oldest items over the limit, and forgets them.
398    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    /// Moves the highlight and scrolls it into view. In a following list,
423    /// landing on the last item follows the end, and anywhere else stops.
424    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
512/// The ids from `a` to `b`, in either order.
513fn span(a: RowId, b: RowId) -> impl Iterator<Item = RowId> {
514    a.min(b)..=a.max(b)
515}
516
517/// A move of the keyboard highlight.
518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub(crate) enum Step {
520    Next,
521    Previous,
522    First,
523    Last,
524    NextPage,
525    PreviousPage,
526}
527
528/// The index a step from `current` lands on in a list of `len` rows, or
529/// `None` when the list is empty. Down from nothing starts at the top, and
530/// up from nothing at the bottom.
531pub(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/// What a list keyboard shortcut does.
547#[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    /// The [`Event`] this action sends to `state`, or `None` when it does
564    /// nothing, so the key is left for others.
565    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
645/// The default list shortcuts:
646///
647/// | Keys | Action |
648/// | --- | --- |
649/// | `ArrowDown` | [`Action::Next`] |
650/// | `ArrowUp` | [`Action::Previous`] |
651/// | `Home` | [`Action::First`] |
652/// | `End` | [`Action::Last`] |
653/// | `PageDown` | [`Action::NextPage`] |
654/// | `PageUp` | [`Action::PreviousPage`] |
655/// | `Shift+ArrowDown` | [`Action::ExtendNext`] |
656/// | `Shift+ArrowUp` | [`Action::ExtendPrevious`] |
657/// | `Enter` | [`Action::Activate`] |
658/// | `Space` | [`Action::Select`] |
659/// | `Mod+A` (Ctrl, or Cmd on macOS) | [`Action::SelectAll`] |
660///
661/// The list resolves these itself, only while it has focus, so they never
662/// clash with other components. Pass a changed keymap with
663/// [`VirtualList::keymap`].
664pub 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
679/// Owns the channel items arrive on. Emits [`Event::Ready`] first, then
680/// batches of up to [`BATCH_SIZE`] items.
681pub fn subscription<T: Send + 'static>() -> Subscription<Event<T>> {
682    Subscription::run(stream::<T>)
683}
684
685/// The stream behind [`subscription`], for driving it without a runtime.
686pub 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/// How a row is shown.
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
694pub struct Status {
695    pub selected: bool,
696    pub hovered: bool,
697    /// Carries the keyboard highlight while the list has focus.
698    pub highlighted: bool,
699    pub disabled: bool,
700}
701
702/// The colours of a row.
703#[derive(Debug, Clone, Copy, PartialEq)]
704pub struct RowStyle {
705    pub background: Option<Color>,
706    /// The ring around the row carrying the keyboard highlight.
707    pub ring: Option<Color>,
708}
709
710/// Resolves a row's colours as a tree's rows do: the accent behind a
711/// selected row, a lighter tint under the pointer and a ring around the
712/// keyboard highlight. Disabled rows are plain.
713pub 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
733/// The frame around the rows: a subtle border with rounded corners.
734pub 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
750/// A virtual list builder. Convert it into an [`Element`] to render.
751pub 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
763/// Renders `state`, drawing each row with `row`, which gets the item's
764/// index and the item. Without [`on_event`](VirtualList::on_event) the
765/// rows render disabled.
766pub 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    /// The height of every row, [`ROW_HEIGHT`] by default.
797    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    /// A fixed or filling height scrolls the rows. Defaults to shrinking to
808    /// fit them, which builds every row, so give a long list a height.
809    pub fn height(mut self, height: impl Into<Length>) -> Self {
810        self.height = height.into();
811        self
812    }
813
814    /// Replaces the [`default_keymap`] the focused list resolves keys with.
815    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    /// Gives every row the context menu of `menu`, keyed by the row's
826    /// [`RowId`]; turn it into an index with [`State::index`]. It opens on a
827    /// right-click or a long press, and on the highlighted row with its
828    /// opening chord while the list has focus.
829    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    /// Replaces the context menu's default keymap.
845    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
943/// Wraps a row's content so it paints its status and reports presses.
944fn 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}