1use std::{
2 collections::BTreeSet,
3 ops::{Deref, Range},
4 time::{Duration, Instant},
5};
6
7use crate::prelude::*;
8
9pub struct VirtualList {
11 scroll_to_cursor: Signal<bool>,
13 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
15 num_items: Signal<usize>,
17 item_height: f32,
19 orientation: Signal<Orientation>,
21 visible_range: Signal<Range<usize>>,
23 scroll_x: Signal<f32>,
25 scroll_y: Signal<f32>,
27 show_horizontal_scrollbar: Signal<bool>,
29 show_vertical_scrollbar: Signal<bool>,
31 selection: Signal<BTreeSet<usize>>,
33 selectable: Signal<Selectable>,
35 focused: Signal<Option<usize>>,
37 focus_visibility: Signal<bool>,
39 selection_follows_focus: Signal<bool>,
41 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
43 type_ahead_text: Option<Box<dyn Fn(&mut EventContext, usize) -> Option<String>>>,
45 type_ahead_buffer: String,
47 type_ahead_last_input: Option<Instant>,
49 type_ahead_timeout: Duration,
51}
52
53impl VirtualList {
54 fn find_type_ahead_match(
55 &self,
56 cx: &mut EventContext,
57 query: &str,
58 start_index: usize,
59 ) -> Option<usize> {
60 let get_text = self.type_ahead_text.as_ref()?;
61 let num_items = self.num_items.get();
62 if num_items == 0 {
63 return None;
64 }
65
66 for offset in 0..num_items {
67 let index = (start_index + offset) % num_items;
68 let item_text = get_text(cx, index)
69 .map(|text| text.trim_start().to_lowercase())
70 .unwrap_or_default();
71
72 if !item_text.is_empty() && item_text.starts_with(query) {
73 return Some(index);
74 }
75 }
76
77 None
78 }
79
80 fn try_type_ahead(&mut self, cx: &mut EventContext, typed: char) -> bool {
81 if self.type_ahead_text.is_none() {
82 return false;
83 }
84
85 let num_items = self.num_items.get();
86 if num_items == 0 || typed.is_control() || typed.is_whitespace() {
87 return false;
88 }
89
90 let now = Instant::now();
91 let within_timeout = self
92 .type_ahead_last_input
93 .is_some_and(|last| now.saturating_duration_since(last) <= self.type_ahead_timeout);
94
95 let ch = typed.to_lowercase().collect::<String>();
96 let query = if within_timeout {
97 let repeated_char_cycle = !self.type_ahead_buffer.is_empty()
98 && self.type_ahead_buffer.chars().all(|c| c == typed.to_ascii_lowercase());
99
100 if repeated_char_cycle {
101 ch.clone()
102 } else {
103 format!("{}{}", self.type_ahead_buffer, ch)
104 }
105 } else {
106 ch.clone()
107 };
108
109 let start_index = self.focused.get().map(|focused| (focused + 1) % num_items).unwrap_or(0);
110
111 if let Some(index) = self.find_type_ahead_match(cx, &query, start_index) {
112 self.type_ahead_buffer = query;
113 self.type_ahead_last_input = Some(now);
114 self.focus_visibility.set(true);
115 self.focused.set(Some(index));
116
117 if self.selection_follows_focus.get() {
118 cx.emit(ListEvent::SelectFocused);
119 }
120
121 true
122 } else {
123 self.type_ahead_buffer.clear();
124 self.type_ahead_last_input = Some(now);
125 false
126 }
127 }
128
129 fn evaluate_index(index: usize, start: usize, end: usize) -> usize {
130 match end - start {
131 0 => 0,
132 len => start + (len - (start % len) + index) % len,
133 }
134 }
135
136 fn resolve_item<L, T>(
145 list: impl SignalWith<L> + Copy + 'static,
146 index: usize,
147 list_len: impl 'static + Fn(&L) -> usize,
148 list_index: impl 'static + Fn(&L, usize) -> T,
149 ) -> Memo<T>
150 where
151 L: 'static,
152 T: Clone + PartialEq + 'static,
153 {
154 Memo::new(move |prev| {
155 list.with(|list| {
156 let len = list_len(list);
157 if len == 0 {
158 prev.cloned().unwrap_or_else(|| list_index(list, 0))
161 } else {
162 list_index(list, index.min(len - 1))
163 }
164 })
165 })
166 }
167
168 fn recalc(&self, cx: &mut EventContext) {
169 let num_items = self.num_items.get();
170 if num_items == 0 {
171 self.visible_range.set_if_changed(0..0);
172 return;
173 }
174
175 let current = cx.current();
176 let current_extent = match self.orientation.get() {
177 Orientation::Horizontal => cx.cache.get_width(current),
178 Orientation::Vertical => cx.cache.get_height(current),
179 };
180 if current_extent == f32::MAX {
181 return;
182 }
183
184 let item_extent = self.item_height;
185 let total_extent = item_extent * (num_items as f32);
186 let visible_extent = current_extent / cx.scale_factor();
187
188 let mut num_visible_items = (visible_extent / item_extent).ceil();
189 num_visible_items += 1.0; let visible_items_extent = item_extent * num_visible_items;
192 let empty_extent = (total_extent - visible_items_extent).max(0.0);
193
194 let axis_scroll = match self.orientation.get() {
196 Orientation::Horizontal => self.scroll_x.get(),
197 Orientation::Vertical => self.scroll_y.get(),
198 };
199 let visible_start = empty_extent * axis_scroll;
200 let visible_end = visible_start + visible_items_extent;
201
202 let mut start_index = (visible_start / item_extent).trunc() as usize;
204 let mut end_index = 1 + (visible_end / item_extent).trunc() as usize;
205
206 let desired_range_size = (num_visible_items as usize) + 1;
208 end_index = end_index.min(num_items);
209
210 let current_range_size = end_index.saturating_sub(start_index);
211
212 if current_range_size < desired_range_size {
213 match end_index == num_items {
214 true => {
216 start_index =
217 start_index.saturating_sub(desired_range_size - current_range_size);
218 }
219 false if end_index < num_items => {
221 end_index = (start_index + desired_range_size).min(num_items);
222 }
223 _ => {}
224 }
225 }
226
227 self.visible_range.set_if_changed(start_index..end_index);
228 }
229}
230
231impl VirtualList {
232 pub fn new<V: View, S, L, T>(
234 cx: &mut Context,
235 list: S,
236 item_height: f32,
237 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
238 ) -> Handle<Self>
239 where
240 S: Res<L> + 'static,
241 L: Deref<Target = [T]> + Clone + 'static,
242 T: Clone + PartialEq + 'static,
243 {
244 Self::new_generic(
245 cx,
246 list,
247 |list| list.len(),
248 |list, index| list[index].clone(),
249 item_height,
250 item_content,
251 )
252 }
253
254 pub fn new_generic<V: View, S, L, T>(
256 cx: &mut Context,
257 list: S,
258 list_len: impl 'static + Copy + Fn(&L) -> usize,
259 list_index: impl 'static + Copy + Fn(&L, usize) -> T,
260 item_height: f32,
261 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
262 ) -> Handle<Self>
263 where
264 S: Res<L> + 'static,
265 L: Clone + 'static,
266 T: Clone + PartialEq + 'static,
267 {
268 let list = list.to_signal(cx);
269 let num_items = list.map(list_len).to_signal(cx);
270 let visible_range = Signal::new(0..0);
271 let scroll_x = Signal::new(0.0);
272 let scroll_y = Signal::new(0.0);
273 let show_horizontal_scrollbar = Signal::new(false);
274 let show_vertical_scrollbar = Signal::new(true);
275 let orientation = Signal::new(Orientation::Vertical);
276 let selection = Signal::new(BTreeSet::default());
277 let selectable = Signal::new(Selectable::None);
278 let focused = Signal::new(None);
279 let focus_visibility = Signal::new(false);
280 let selection_follows_focus = Signal::new(false);
281 let scroll_to_cursor = Signal::new(true);
282
283 Self {
284 scroll_to_cursor,
285 on_scroll: None,
286 num_items,
287 item_height,
288 orientation,
289 visible_range,
290 scroll_x,
291 scroll_y,
292 show_horizontal_scrollbar,
293 show_vertical_scrollbar,
294 selection,
295 selectable,
296 focused,
297 focus_visibility,
298 selection_follows_focus,
299 on_select: None,
300 type_ahead_text: None,
301 type_ahead_buffer: String::new(),
302 type_ahead_last_input: None,
303 type_ahead_timeout: Duration::from_millis(1000),
304 }
305 .build(cx, |cx| {
306 Keymap::from(vec![
307 (
308 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
309 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
310 ),
311 (
312 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
313 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
314 ),
315 (
316 KeyChord::new(Modifiers::empty(), Code::Home),
317 KeymapEntry::new("Focus First", |cx| cx.emit(ListEvent::FocusFirst)),
318 ),
319 (
320 KeyChord::new(Modifiers::empty(), Code::End),
321 KeymapEntry::new("Focus Last", |cx| cx.emit(ListEvent::FocusLast)),
322 ),
323 (
324 KeyChord::new(Modifiers::empty(), Code::Enter),
325 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
326 ),
327 ])
328 .build(cx);
329
330 Binding::new(cx, orientation, move |cx| {
331 let orientation = orientation.get();
332 if orientation == Orientation::Horizontal {
333 cx.emit(KeymapEvent::RemoveAction(
334 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
335 "Focus Next",
336 ));
337
338 cx.emit(KeymapEvent::RemoveAction(
339 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
340 "Focus Previous",
341 ));
342
343 cx.emit(KeymapEvent::InsertAction(
344 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
345 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
346 ));
347
348 cx.emit(KeymapEvent::InsertAction(
349 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
350 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
351 ));
352 } else {
353 cx.emit(KeymapEvent::RemoveAction(
354 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
355 "Focus Next",
356 ));
357
358 cx.emit(KeymapEvent::RemoveAction(
359 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
360 "Focus Previous",
361 ));
362
363 cx.emit(KeymapEvent::InsertAction(
364 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
365 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
366 ));
367
368 cx.emit(KeymapEvent::InsertAction(
369 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
370 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
371 ));
372 }
373 });
374
375 ScrollView::new(cx, move |cx| {
376 Binding::new(cx, orientation, move |cx| {
377 let orientation = orientation.get();
378 Binding::new(cx, num_items, move |cx| {
379 let num_items = num_items.get();
380
381 match orientation {
382 Orientation::Horizontal => cx.emit(ScrollEvent::SetX(0.0)),
383 Orientation::Vertical => cx.emit(ScrollEvent::SetY(0.0)),
384 }
385
386 let num_visible_items = visible_range.map(Range::len);
387
388 match orientation {
389 Orientation::Horizontal => {
390 HStack::new(cx, |cx| {
391 Binding::new(cx, num_visible_items, move |cx| {
392 for i in 0..num_visible_items.get().min(num_items) {
393 let item_index = visible_range.map(move |range| {
394 Self::evaluate_index(i, range.start, range.end)
395 });
396 Binding::new(cx, item_index, move |cx| {
397 let index = item_index.get();
398 let item = Self::resolve_item(
399 list, index, list_len, list_index,
400 );
401
402 ListItem::new(
403 cx,
404 index,
405 item,
406 selection,
407 focused,
408 focus_visibility,
409 move |cx, index, item| {
410 item_content(cx, index, item)
411 .height(Percentage(100.0));
412 },
413 )
414 .min_size(Auto)
415 .width(Pixels(item_height))
416 .height(Percentage(100.0))
417 .position_type(PositionType::Absolute)
418 .bind(item_index, move |handle| {
419 let index = item_index.get();
420 handle.left(Pixels(index as f32 * item_height));
421 });
422 });
423 }
424 })
425 })
426 .width(Pixels(num_items as f32 * item_height))
427 .height(Stretch(1.0));
428 }
429
430 Orientation::Vertical => {
431 VStack::new(cx, |cx| {
432 Binding::new(cx, num_visible_items, move |cx| {
433 for i in 0..num_visible_items.get().min(num_items) {
434 let item_index = visible_range.map(move |range| {
435 Self::evaluate_index(i, range.start, range.end)
436 });
437 Binding::new(cx, item_index, move |cx| {
438 let index = item_index.get();
439 let item = Self::resolve_item(
440 list, index, list_len, list_index,
441 );
442
443 ListItem::new(
444 cx,
445 index,
446 item,
447 selection,
448 focused,
449 focus_visibility,
450 move |cx, index, item| {
451 item_content(cx, index, item)
452 .height(Percentage(100.0));
453 },
454 )
455 .min_width(Auto)
456 .height(Pixels(item_height))
457 .position_type(PositionType::Absolute)
458 .bind(item_index, move |handle| {
459 let index = item_index.get();
460 handle.top(Pixels(index as f32 * item_height));
461 });
462 });
463 }
464 })
465 })
466 .height(Pixels(num_items as f32 * item_height));
467 }
468 }
469 })
470 })
471 })
472 .show_horizontal_scrollbar(show_horizontal_scrollbar)
473 .show_vertical_scrollbar(show_vertical_scrollbar)
474 .scroll_to_cursor(scroll_to_cursor)
475 .scroll_x(scroll_x)
476 .scroll_y(scroll_y)
477 .on_scroll(|cx, x, y| {
478 if y.is_finite() && x.is_finite() {
479 cx.emit(ListEvent::Scroll(x, y));
480 }
481 });
482 })
483 .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
484 .orientation(orientation)
485 .navigable(true)
486 .role(Role::ListBox)
487 }
488}
489
490impl View for VirtualList {
491 fn element(&self) -> Option<&'static str> {
492 Some("virtual-list")
493 }
494
495 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
496 event.take(|list_event, meta| match list_event {
497 ListEvent::Select(index) => {
498 let selectable = self.selectable.get();
499 let mut selection = self.selection.get();
500 let mut focused = self.focused.get();
501
502 match selectable {
503 Selectable::Single => {
504 if selection.contains(&index) {
505 selection.clear();
506 focused = None;
507 } else {
508 selection.clear();
509 selection.insert(index);
510 focused = Some(index);
511 if let Some(on_select) = &self.on_select {
512 on_select(cx, index);
513 }
514 }
515 }
516
517 Selectable::Multi => {
518 if selection.contains(&index) {
519 selection.remove(&index);
520 focused = None;
521 } else {
522 selection.insert(index);
523 focused = Some(index);
524 if let Some(on_select) = &self.on_select {
525 on_select(cx, index);
526 }
527 }
528 }
529
530 Selectable::None => {}
531 }
532
533 self.selection.set(selection);
534 self.focused.set(focused);
535
536 meta.consume();
537 }
538
539 ListEvent::SelectFocused => {
540 if let Some(focused) = self.focused.get() {
541 self.focus_visibility.set(true);
542 cx.emit(ListEvent::Select(focused))
543 }
544 meta.consume();
545 }
546
547 ListEvent::ClearSelection => {
548 self.selection.set(BTreeSet::default());
549 meta.consume();
550 }
551
552 ListEvent::FocusNext => {
553 let mut focused = self.focused.get();
554 let num_items = self.num_items.get();
555 if let Some(f) = &mut focused {
556 if *f < num_items.saturating_sub(1) {
557 *f = f.saturating_add(1);
558 if self.selection_follows_focus.get() {
559 self.focus_visibility.set(true);
560 cx.emit(ListEvent::SelectFocused);
561 }
562 }
563 } else {
564 focused = Some(0);
565 if self.selection_follows_focus.get() {
566 self.focus_visibility.set(true);
567 cx.emit(ListEvent::SelectFocused);
568 }
569 }
570
571 self.focused.set(focused);
572
573 meta.consume();
574 }
575
576 ListEvent::FocusPrev => {
577 let mut focused = self.focused.get();
578 let num_items = self.num_items.get();
579 if let Some(f) = &mut focused {
580 if *f > 0 {
581 *f = f.saturating_sub(1);
582 if self.selection_follows_focus.get() {
583 self.focus_visibility.set(true);
584 cx.emit(ListEvent::SelectFocused);
585 }
586 }
587 } else {
588 focused = Some(num_items.saturating_sub(1));
589 if self.selection_follows_focus.get() {
590 self.focus_visibility.set(true);
591 cx.emit(ListEvent::SelectFocused);
592 }
593 }
594
595 self.focused.set(focused);
596
597 meta.consume();
598 }
599
600 ListEvent::FocusFirst => {
601 if self.num_items.get() > 0 {
602 self.focus_visibility.set(true);
603 self.focused.set(Some(0));
604 if self.selection_follows_focus.get() {
605 cx.emit(ListEvent::SelectFocused);
606 }
607 }
608
609 meta.consume();
610 }
611
612 ListEvent::FocusLast => {
613 let num_items = self.num_items.get();
614 if num_items > 0 {
615 self.focus_visibility.set(true);
616 self.focused.set(Some(num_items.saturating_sub(1)));
617 if self.selection_follows_focus.get() {
618 cx.emit(ListEvent::SelectFocused);
619 }
620 }
621
622 meta.consume();
623 }
624
625 ListEvent::Scroll(x, y) => {
626 self.scroll_x.set(x);
627 self.scroll_y.set(y);
628
629 self.recalc(cx);
630
631 if let Some(callback) = &self.on_scroll {
632 (callback)(cx, x, y);
633 }
634
635 meta.consume();
636 }
637 });
638
639 event.map(|window_event, meta| match window_event {
640 WindowEvent::Press { mouse } => {
641 self.focus_visibility.set(!*mouse);
642 }
643
644 WindowEvent::CharInput(c) => {
645 if *c == ' ' && meta.target == cx.current() {
646 cx.emit(ListEvent::SelectFocused);
647 meta.consume();
648 } else if self.try_type_ahead(cx, *c) {
649 meta.consume();
650 }
651 }
652
653 WindowEvent::GeometryChanged(geo) => {
654 if geo.intersects(GeoChanged::WIDTH_CHANGED | GeoChanged::HEIGHT_CHANGED) {
655 self.recalc(cx);
656 }
657 }
658
659 _ => {}
660 });
661 }
662}
663
664impl Handle<'_, VirtualList> {
665 pub fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
667 where
668 R: Deref<Target = [usize]> + Clone + 'static,
669 {
670 let selection = selection.to_signal(self.cx);
671 self.bind(selection, move |handle| {
672 selection.with(|selected_indices| {
673 handle.modify(|list| {
674 let mut selection = BTreeSet::default();
675 let mut focused = None;
676 for idx in selected_indices.deref().iter().copied() {
677 selection.insert(idx);
678 focused = Some(idx);
679 }
680 list.selection.set(selection);
681 list.focused.set(focused);
682 });
683 });
684 })
685 }
686
687 pub fn on_select<F>(self, callback: F) -> Self
689 where
690 F: 'static + Fn(&mut EventContext, usize),
691 {
692 self.modify(|list| list.on_select = Some(Box::new(callback)))
693 }
694
695 pub fn selectable<U: Into<Selectable> + Clone + 'static>(
697 self,
698 selectable: impl Res<U> + 'static,
699 ) -> Self {
700 let selectable = selectable.to_signal(self.cx);
701 self.bind(selectable, move |handle| {
702 let selectable = selectable.get();
703 let s = selectable.into();
704 handle.modify(|list| list.selectable.set(s));
705 })
706 }
707
708 pub fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
710 self,
711 flag: impl Res<U> + 'static,
712 ) -> Self {
713 let flag = flag.to_signal(self.cx);
714 self.bind(flag, move |handle| {
715 let selection_follows_focus = flag.get();
716 let s = selection_follows_focus.into();
717 handle.modify(|list| list.selection_follows_focus.set(s));
718 })
719 }
720
721 pub fn horizontal<U: Into<bool> + Clone + 'static>(
723 self,
724 horizontal: impl Res<U> + 'static,
725 ) -> Self {
726 let horizontal = horizontal.to_signal(self.cx);
727 self.bind(horizontal, move |handle| {
728 let horizontal = horizontal.get();
729 let horizontal = horizontal.into();
730 handle.modify(|list| {
731 list.orientation.set(if horizontal {
732 Orientation::Horizontal
733 } else {
734 Orientation::Vertical
735 });
736 });
737 })
738 }
739
740 pub fn scroll_to_cursor(self, flag: bool) -> Self {
742 self.modify(|virtual_list: &mut VirtualList| {
743 virtual_list.scroll_to_cursor.set(flag);
744 })
745 }
746
747 pub fn on_scroll(
749 self,
750 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
751 ) -> Self {
752 self.modify(|list| list.on_scroll = Some(Box::new(callback)))
753 }
754
755 pub fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
757 let scrollx = scrollx.to_signal(self.cx);
758 self.bind(scrollx, move |handle| {
759 let sx = scrollx.get();
760 handle.modify(|list| list.scroll_x.set(sx));
761 })
762 }
763
764 pub fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self {
766 let scrollx = scrollx.to_signal(self.cx);
767 self.bind(scrollx, move |handle| {
768 let sy = scrollx.get();
769 handle.modify(|list| list.scroll_y.set(sy));
770 })
771 }
772
773 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
775 let flag = flag.to_signal(self.cx);
776 self.bind(flag, move |handle| {
777 let s = flag.get();
778 handle.modify(|list| list.show_horizontal_scrollbar.set(s));
779 })
780 }
781
782 pub fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
784 let flag = flag.to_signal(self.cx);
785 self.bind(flag, move |handle| {
786 let s = flag.get();
787 handle.modify(|list| list.show_vertical_scrollbar.set(s));
788 })
789 }
790
791 pub fn type_ahead_text<F>(self, callback: F) -> Self
793 where
794 F: 'static + Fn(&mut EventContext, usize) -> Option<String>,
795 {
796 self.modify(|list: &mut VirtualList| list.type_ahead_text = Some(Box::new(callback)))
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 fn evaluate_indices(range: Range<usize>) -> Vec<usize> {
805 (0..range.len())
806 .map(|index| VirtualList::evaluate_index(index, range.start, range.end))
807 .collect()
808 }
809
810 #[test]
811 fn test_evaluate_index() {
812 assert_eq!(evaluate_indices(0..4), [0, 1, 2, 3]);
814 assert_eq!(evaluate_indices(1..5), [4, 1, 2, 3]);
816 assert_eq!(evaluate_indices(2..6), [4, 5, 2, 3]);
818 assert_eq!(evaluate_indices(3..7), [4, 5, 6, 3]);
820 assert_eq!(evaluate_indices(4..8), [4, 5, 6, 7]);
822 assert_eq!(evaluate_indices(5..9), [8, 5, 6, 7]);
824 assert_eq!(evaluate_indices(6..10), [8, 9, 6, 7]);
826 assert_eq!(evaluate_indices(7..11), [8, 9, 10, 7]);
828 assert_eq!(evaluate_indices(8..12), [8, 9, 10, 11]);
830 assert_eq!(evaluate_indices(9..13), [12, 9, 10, 11]);
832 }
833}