1#![forbid(unsafe_code)]
2
3use std::cell::Cell as StdCell;
34use std::collections::VecDeque;
35use std::ops::Range;
36use std::time::Duration;
37
38use crate::scrollbar::{Scrollbar, ScrollbarOrientation, ScrollbarState};
39use crate::{StatefulWidget, set_style_area};
40use ftui_core::geometry::Rect;
41use ftui_render::cell::Cell;
42use ftui_render::frame::Frame;
43use ftui_style::Style;
44
45#[derive(Debug, Clone)]
54pub struct Virtualized<T> {
55 storage: VirtualizedStorage<T>,
57 scroll_offset: usize,
59 visible_count: StdCell<usize>,
61 overscan: usize,
63 item_height: ItemHeight,
65 follow_mode: bool,
67 scroll_velocity: f32,
69}
70
71#[derive(Debug, Clone)]
73pub enum VirtualizedStorage<T> {
74 Owned(VecDeque<T>),
76 External {
79 len: usize,
81 cache_capacity: usize,
83 },
84}
85
86#[derive(Debug, Clone)]
88pub enum ItemHeight {
89 Fixed(u16),
91 Variable(HeightCache),
93 VariableFenwick(VariableHeightsFenwick),
95}
96
97#[derive(Debug, Clone)]
99pub struct HeightCache {
100 cache: Vec<Option<u16>>,
102 base_offset: usize,
104 default_height: u16,
106 capacity: usize,
108}
109
110impl<T> Virtualized<T> {
111 #[must_use]
116 pub fn new(capacity: usize) -> Self {
117 Self {
118 storage: VirtualizedStorage::Owned(VecDeque::with_capacity(capacity.min(1024))),
119 scroll_offset: 0,
120 visible_count: StdCell::new(0),
121 overscan: 2,
122 item_height: ItemHeight::Fixed(1),
123 follow_mode: false,
124 scroll_velocity: 0.0,
125 }
126 }
127
128 #[must_use]
130 pub fn external(len: usize, cache_capacity: usize) -> Self {
131 Self {
132 storage: VirtualizedStorage::External {
133 len,
134 cache_capacity,
135 },
136 scroll_offset: 0,
137 visible_count: StdCell::new(0),
138 overscan: 2,
139 item_height: ItemHeight::Fixed(1),
140 follow_mode: false,
141 scroll_velocity: 0.0,
142 }
143 }
144
145 #[must_use]
147 pub fn with_item_height(mut self, height: ItemHeight) -> Self {
148 self.item_height = height;
149 self
150 }
151
152 #[must_use]
154 pub fn with_fixed_height(mut self, height: u16) -> Self {
155 self.item_height = ItemHeight::Fixed(height);
156 self
157 }
158
159 #[must_use]
164 pub fn with_variable_heights_fenwick(mut self, default_height: u16, capacity: usize) -> Self {
165 self.item_height =
166 ItemHeight::VariableFenwick(VariableHeightsFenwick::new(default_height, capacity));
167 self
168 }
169
170 #[must_use]
172 pub fn with_overscan(mut self, overscan: usize) -> Self {
173 self.overscan = overscan;
174 self
175 }
176
177 #[must_use]
179 pub fn with_follow(mut self, follow: bool) -> Self {
180 self.follow_mode = follow;
181 self
182 }
183
184 #[must_use]
186 pub fn len(&self) -> usize {
187 match &self.storage {
188 VirtualizedStorage::Owned(items) => items.len(),
189 VirtualizedStorage::External { len, .. } => *len,
190 }
191 }
192
193 #[must_use]
195 pub fn is_empty(&self) -> bool {
196 self.len() == 0
197 }
198
199 #[must_use]
201 pub fn scroll_offset(&self) -> usize {
202 self.scroll_offset
203 }
204
205 #[must_use]
207 pub fn visible_count(&self) -> usize {
208 self.visible_count.get()
209 }
210
211 #[must_use]
213 pub fn follow_mode(&self) -> bool {
214 self.follow_mode
215 }
216
217 #[must_use]
219 pub fn visible_range(&self, viewport_height: u16) -> Range<usize> {
220 if self.is_empty() || viewport_height == 0 {
221 self.visible_count.set(0);
222 return 0..0;
223 }
224
225 let items_visible = match &self.item_height {
226 ItemHeight::Fixed(h) if *h > 0 => (viewport_height / h) as usize,
227 ItemHeight::Fixed(_) => viewport_height as usize,
228 ItemHeight::Variable(cache) => {
229 let mut count = 0;
231 let mut total_height = 0u16;
232 let start = self.scroll_offset;
233 while start + count < self.len() {
234 let next = cache.get(start + count);
235 let proposed = total_height.saturating_add(next);
236 if proposed > viewport_height {
237 break;
238 }
239 total_height = proposed;
240 count += 1;
241 }
242 count
243 }
244 ItemHeight::VariableFenwick(tracker) => {
245 tracker.visible_count(self.scroll_offset, viewport_height)
247 }
248 };
249
250 let start = self.scroll_offset;
251 let end = (start + items_visible).min(self.len());
252 self.visible_count.set(items_visible);
253 start..end
254 }
255
256 #[must_use]
258 pub fn render_range(&self, viewport_height: u16) -> Range<usize> {
259 let visible = self.visible_range(viewport_height);
260 let start = visible.start.saturating_sub(self.overscan);
261 let end = visible.end.saturating_add(self.overscan).min(self.len());
262 start..end
263 }
264
265 pub fn scroll(&mut self, delta: i32) {
267 if self.is_empty() {
268 return;
269 }
270 let visible_count = self.visible_count.get();
271 let max_offset = if visible_count > 0 {
272 self.len().saturating_sub(visible_count)
273 } else {
274 self.len().saturating_sub(1)
275 };
276 let new_offset = (self.scroll_offset as i64 + delta as i64)
277 .max(0)
278 .min(max_offset as i64);
279 self.scroll_offset = new_offset as usize;
280
281 if delta != 0 {
283 self.follow_mode = false;
284 }
285 }
286
287 pub fn scroll_to(&mut self, idx: usize) {
289 self.scroll_offset = idx.min(self.len().saturating_sub(1));
290 self.follow_mode = false;
291 }
292
293 pub fn scroll_to_bottom(&mut self) {
295 let visible_count = self.visible_count.get();
296 if self.len() > visible_count && visible_count > 0 {
297 self.scroll_offset = self.len().saturating_sub(visible_count);
298 } else {
299 self.scroll_offset = 0;
300 }
301 }
302
303 pub fn scroll_to_top(&mut self) {
305 self.scroll_offset = 0;
306 self.follow_mode = false;
307 }
308
309 pub fn scroll_to_start(&mut self) {
311 self.scroll_to_top();
312 }
313
314 pub fn scroll_to_end(&mut self) {
316 self.scroll_to_bottom();
317 self.follow_mode = true;
318 }
319
320 pub fn page_up(&mut self) {
322 let visible_count = self.visible_count.get();
323 if visible_count > 0 {
324 let delta = i32::try_from(visible_count).unwrap_or(i32::MAX);
325 self.scroll(-delta);
326 }
327 }
328
329 pub fn page_down(&mut self) {
331 let visible_count = self.visible_count.get();
332 if visible_count > 0 {
333 let delta = i32::try_from(visible_count).unwrap_or(i32::MAX);
334 self.scroll(delta);
335 }
336 }
337
338 pub fn set_follow(&mut self, follow: bool) {
340 self.follow_mode = follow;
341 if follow {
342 self.scroll_to_bottom();
343 }
344 }
345
346 #[must_use]
348 pub fn is_at_bottom(&self) -> bool {
349 let visible_count = self.visible_count.get();
350 if self.len() <= visible_count {
351 true
352 } else {
353 self.scroll_offset >= self.len().saturating_sub(visible_count)
354 }
355 }
356
357 pub fn fling(&mut self, velocity: f32) {
359 self.scroll_velocity = velocity;
360 }
361
362 pub fn tick(&mut self, dt: Duration) {
364 if self.scroll_velocity.abs() > 0.1 {
365 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
366 if delta != 0 {
367 self.scroll(delta);
368 }
369 self.scroll_velocity *= 0.95;
371 } else {
372 self.scroll_velocity = 0.0;
373 }
374 }
375
376 pub fn set_visible_count(&mut self, count: usize) {
378 self.visible_count.set(count);
379 }
380}
381
382impl<T> Virtualized<T> {
383 pub fn push(&mut self, item: T) {
385 if let VirtualizedStorage::Owned(items) = &mut self.storage {
386 items.push_back(item);
387 if self.follow_mode {
388 self.scroll_to_bottom();
389 }
390 }
391 }
392
393 #[must_use = "use the returned item (if any)"]
395 pub fn get(&self, idx: usize) -> Option<&T> {
396 if let VirtualizedStorage::Owned(items) = &self.storage {
397 items.get(idx)
398 } else {
399 None
400 }
401 }
402
403 #[must_use = "use the returned item (if any)"]
405 pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> {
406 if let VirtualizedStorage::Owned(items) = &mut self.storage {
407 items.get_mut(idx)
408 } else {
409 None
410 }
411 }
412
413 pub fn clear(&mut self) {
415 if let VirtualizedStorage::Owned(items) = &mut self.storage {
416 items.clear();
417 }
418 self.scroll_offset = 0;
419 }
420
421 pub fn trim_front(&mut self, max: usize) -> usize {
425 if let VirtualizedStorage::Owned(items) = &mut self.storage
426 && items.len() > max
427 {
428 let to_remove = items.len() - max;
429 items.drain(..to_remove);
430 self.scroll_offset = self.scroll_offset.saturating_sub(to_remove);
432 return to_remove;
433 }
434 0
435 }
436
437 pub fn iter(&self) -> Box<dyn Iterator<Item = &T> + '_> {
440 match &self.storage {
441 VirtualizedStorage::Owned(items) => Box::new(items.iter()),
442 VirtualizedStorage::External { .. } => Box::new(std::iter::empty()),
443 }
444 }
445
446 pub fn set_external_len(&mut self, len: usize) {
448 if let VirtualizedStorage::External { len: l, .. } = &mut self.storage {
449 *l = len;
450 if self.follow_mode {
451 self.scroll_to_bottom();
452 }
453 }
454 }
455}
456
457impl Default for HeightCache {
458 fn default() -> Self {
459 Self::new(1, 1000)
460 }
461}
462
463impl HeightCache {
464 #[must_use]
466 pub fn new(default_height: u16, capacity: usize) -> Self {
467 Self {
468 cache: Vec::new(),
469 base_offset: 0,
470 default_height,
471 capacity,
472 }
473 }
474
475 #[must_use]
477 pub fn get(&self, idx: usize) -> u16 {
478 if idx < self.base_offset {
479 return self.default_height;
480 }
481 let local = idx - self.base_offset;
482 self.cache
483 .get(local)
484 .and_then(|h| *h)
485 .unwrap_or(self.default_height)
486 }
487
488 pub fn set(&mut self, idx: usize, height: u16) {
490 if self.capacity == 0 {
491 return;
492 }
493 if idx < self.base_offset {
494 return;
496 }
497 let mut local = idx - self.base_offset;
498 if local >= self.capacity {
499 self.base_offset = idx.saturating_add(1).saturating_sub(self.capacity);
501 self.cache.clear();
502 local = idx - self.base_offset;
503 }
504 if local >= self.cache.len() {
505 self.cache.resize(local + 1, None);
506 }
507 self.cache[local] = Some(height);
508
509 if self.cache.len() > self.capacity {
511 let to_remove = self.cache.len() - self.capacity;
512 self.cache.drain(0..to_remove);
513 self.base_offset += to_remove;
514 }
515 }
516
517 pub fn clear(&mut self) {
519 self.cache.clear();
520 self.base_offset = 0;
521 }
522}
523
524use crate::fenwick::FenwickTree;
529
530#[derive(Debug, Clone)]
550pub struct VariableHeightsFenwick {
551 tree: FenwickTree,
553 default_height: u16,
555 len: usize,
557}
558
559impl Default for VariableHeightsFenwick {
560 fn default() -> Self {
561 Self::new(1, 0)
562 }
563}
564
565impl VariableHeightsFenwick {
566 #[must_use]
568 pub fn new(default_height: u16, capacity: usize) -> Self {
569 let tree = if capacity > 0 {
570 let heights: Vec<u32> = vec![u32::from(default_height); capacity];
572 FenwickTree::from_values(&heights)
573 } else {
574 FenwickTree::new(0)
575 };
576 Self {
577 tree,
578 default_height,
579 len: capacity,
580 }
581 }
582
583 #[must_use]
585 pub fn from_heights(heights: &[u16], default_height: u16) -> Self {
586 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
587 Self {
588 tree: FenwickTree::from_values(&heights_u32),
589 default_height,
590 len: heights.len(),
591 }
592 }
593
594 #[must_use]
596 pub fn len(&self) -> usize {
597 self.len
598 }
599
600 #[must_use]
602 pub fn is_empty(&self) -> bool {
603 self.len == 0
604 }
605
606 #[must_use]
608 pub fn default_height(&self) -> u16 {
609 self.default_height
610 }
611
612 #[must_use]
614 pub fn get(&self, idx: usize) -> u16 {
615 if idx >= self.len {
616 return self.default_height;
617 }
618 self.tree.get(idx).min(u32::from(u16::MAX)) as u16
620 }
621
622 pub fn set(&mut self, idx: usize, height: u16) {
624 if idx >= self.len {
625 self.resize(idx + 1);
627 }
628 self.tree.set(idx, u32::from(height));
629 }
630
631 #[must_use]
635 pub fn offset_of_item(&self, idx: usize) -> u32 {
636 if idx == 0 || self.len == 0 {
637 return 0;
638 }
639 let clamped = idx.min(self.len);
640 if clamped > 0 {
641 self.tree.prefix(clamped - 1)
642 } else {
643 0
644 }
645 }
646
647 #[must_use]
654 pub fn find_item_at_offset(&self, offset: u32) -> usize {
655 if self.len == 0 {
656 return 0;
657 }
658 if offset == 0 {
659 return 0;
660 }
661 match self.tree.find_prefix(offset) {
669 Some(i) => {
670 (i + 1).min(self.len)
674 }
675 None => {
676 0
678 }
679 }
680 }
681
682 #[must_use]
688 pub fn visible_count(&self, start_idx: usize, viewport_height: u16) -> usize {
689 if self.len == 0 || viewport_height == 0 {
690 return 0;
691 }
692 let start = start_idx.min(self.len);
693 let start_offset = self.offset_of_item(start);
694 let end_offset = start_offset.saturating_add(u32::from(viewport_height));
695
696 let end_idx = self.find_item_at_offset(end_offset);
698
699 if end_idx > start {
701 if end_idx >= self.len {
704 return self.len.saturating_sub(start);
705 }
706 let end_item_start = self.offset_of_item(end_idx);
708 if end_item_start.saturating_add(u32::from(self.get(end_idx))) <= end_offset {
709 end_idx - start + 1
710 } else {
711 end_idx - start
712 }
713 } else {
714 if viewport_height > 0 && start < self.len {
716 1
717 } else {
718 0
719 }
720 }
721 }
722
723 #[must_use]
725 pub fn total_height(&self) -> u32 {
726 self.tree.total()
727 }
728
729 pub fn resize(&mut self, new_len: usize) {
733 if new_len == self.len {
734 return;
735 }
736 self.tree.resize(new_len);
737 if new_len > self.len {
739 for i in self.len..new_len {
740 self.tree.set(i, u32::from(self.default_height));
741 }
742 }
743 self.len = new_len;
744 }
745
746 pub fn clear(&mut self) {
748 self.tree = FenwickTree::new(0);
749 self.len = 0;
750 }
751
752 pub fn rebuild(&mut self, heights: &[u16]) {
754 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
755 self.tree = FenwickTree::from_values(&heights_u32);
756 self.len = heights.len();
757 }
758}
759
760pub trait RenderItem {
768 fn render(&self, area: Rect, frame: &mut Frame, selected: bool);
770
771 fn height(&self) -> u16 {
773 1
774 }
775}
776
777#[derive(Debug, Clone)]
779pub struct VirtualizedListState {
780 pub selected: Option<usize>,
782 scroll_offset: usize,
784 visible_count: usize,
786 overscan: usize,
788 follow_mode: bool,
790 scroll_velocity: f32,
792 persistence_id: Option<String>,
794}
795
796impl Default for VirtualizedListState {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802impl VirtualizedListState {
803 #[must_use]
805 pub fn new() -> Self {
806 Self {
807 selected: None,
808 scroll_offset: 0,
809 visible_count: 0,
810 overscan: 2,
811 follow_mode: false,
812 scroll_velocity: 0.0,
813 persistence_id: None,
814 }
815 }
816
817 #[must_use]
819 pub fn with_overscan(mut self, overscan: usize) -> Self {
820 self.overscan = overscan;
821 self
822 }
823
824 #[must_use]
826 pub fn with_follow(mut self, follow: bool) -> Self {
827 self.follow_mode = follow;
828 self
829 }
830
831 #[must_use]
833 pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
834 self.persistence_id = Some(id.into());
835 self
836 }
837
838 #[must_use = "use the persistence id (if any)"]
840 pub fn persistence_id(&self) -> Option<&str> {
841 self.persistence_id.as_deref()
842 }
843
844 #[must_use]
846 pub fn scroll_offset(&self) -> usize {
847 self.scroll_offset
848 }
849
850 #[must_use]
852 pub fn visible_count(&self) -> usize {
853 self.visible_count
854 }
855
856 pub fn scroll(&mut self, delta: i32, total_items: usize) {
858 if total_items == 0 {
859 return;
860 }
861 let max_offset = if self.visible_count > 0 {
862 total_items.saturating_sub(self.visible_count)
863 } else {
864 total_items.saturating_sub(1)
865 };
866 let new_offset = (self.scroll_offset as i64 + delta as i64)
867 .max(0)
868 .min(max_offset as i64);
869 self.scroll_offset = new_offset as usize;
870
871 if delta != 0 {
872 self.follow_mode = false;
873 }
874 }
875
876 pub fn scroll_to(&mut self, idx: usize, total_items: usize) {
878 self.scroll_offset = idx.min(total_items.saturating_sub(1));
879 self.follow_mode = false;
880 }
881
882 pub fn scroll_to_top(&mut self) {
884 self.scroll_offset = 0;
885 self.follow_mode = false;
886 }
887
888 pub fn scroll_to_bottom(&mut self, total_items: usize) {
890 if total_items > self.visible_count && self.visible_count > 0 {
891 self.scroll_offset = total_items - self.visible_count;
892 } else {
893 self.scroll_offset = 0;
894 }
895 }
896
897 pub fn page_up(&mut self, total_items: usize) {
899 if self.visible_count > 0 {
900 let delta = i32::try_from(self.visible_count).unwrap_or(i32::MAX);
901 self.scroll(-delta, total_items);
902 }
903 }
904
905 pub fn page_down(&mut self, total_items: usize) {
907 if self.visible_count > 0 {
908 let delta = i32::try_from(self.visible_count).unwrap_or(i32::MAX);
909 self.scroll(delta, total_items);
910 }
911 }
912
913 pub fn select(&mut self, index: Option<usize>) {
915 self.selected = index;
916 }
917
918 pub fn select_previous(&mut self, total_items: usize) {
920 if total_items == 0 {
921 self.selected = None;
922 return;
923 }
924 self.selected = Some(match self.selected {
925 Some(i) if i > 0 => i - 1,
926 Some(_) => 0,
927 None => 0,
928 });
929 }
930
931 pub fn select_next(&mut self, total_items: usize) {
933 if total_items == 0 {
934 self.selected = None;
935 return;
936 }
937 self.selected = Some(match self.selected {
938 Some(i) if i < total_items - 1 => i + 1,
939 Some(i) => i,
940 None => 0,
941 });
942 }
943
944 #[must_use]
946 pub fn is_at_bottom(&self, total_items: usize) -> bool {
947 if total_items <= self.visible_count {
948 true
949 } else {
950 self.scroll_offset >= total_items - self.visible_count
951 }
952 }
953
954 pub fn set_follow(&mut self, follow: bool, total_items: usize) {
956 self.follow_mode = follow;
957 if follow {
958 self.scroll_to_bottom(total_items);
959 }
960 }
961
962 #[must_use]
964 pub fn follow_mode(&self) -> bool {
965 self.follow_mode
966 }
967
968 pub fn fling(&mut self, velocity: f32) {
970 self.scroll_velocity = velocity;
971 }
972
973 pub fn tick(&mut self, dt: Duration, total_items: usize) {
975 if self.scroll_velocity.abs() > 0.1 {
976 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
977 if delta != 0 {
978 self.scroll(delta, total_items);
979 }
980 self.scroll_velocity *= 0.95;
981 } else {
982 self.scroll_velocity = 0.0;
983 }
984 }
985}
986
987#[derive(Clone, Debug, Default, PartialEq)]
996#[cfg_attr(
997 feature = "state-persistence",
998 derive(serde::Serialize, serde::Deserialize)
999)]
1000pub struct VirtualizedListPersistState {
1001 pub selected: Option<usize>,
1003 pub scroll_offset: usize,
1005 pub follow_mode: bool,
1007}
1008
1009impl crate::stateful::Stateful for VirtualizedListState {
1010 type State = VirtualizedListPersistState;
1011
1012 fn state_key(&self) -> crate::stateful::StateKey {
1013 crate::stateful::StateKey::new(
1014 "VirtualizedList",
1015 self.persistence_id.as_deref().unwrap_or("default"),
1016 )
1017 }
1018
1019 fn save_state(&self) -> VirtualizedListPersistState {
1020 VirtualizedListPersistState {
1021 selected: self.selected,
1022 scroll_offset: self.scroll_offset,
1023 follow_mode: self.follow_mode,
1024 }
1025 }
1026
1027 fn restore_state(&mut self, state: VirtualizedListPersistState) {
1028 self.selected = state.selected;
1029 self.scroll_offset = state.scroll_offset;
1030 self.follow_mode = state.follow_mode;
1031 self.scroll_velocity = 0.0;
1033 }
1034}
1035
1036#[derive(Debug)]
1042pub struct VirtualizedList<'a, T> {
1043 items: &'a [T],
1045 style: Style,
1047 highlight_style: Style,
1049 show_scrollbar: bool,
1051 fixed_height: u16,
1053}
1054
1055impl<'a, T> VirtualizedList<'a, T> {
1056 #[must_use]
1058 pub fn new(items: &'a [T]) -> Self {
1059 Self {
1060 items,
1061 style: Style::default(),
1062 highlight_style: Style::default(),
1063 show_scrollbar: true,
1064 fixed_height: 1,
1065 }
1066 }
1067
1068 #[must_use]
1070 pub fn style(mut self, style: Style) -> Self {
1071 self.style = style;
1072 self
1073 }
1074
1075 #[must_use]
1077 pub fn highlight_style(mut self, style: Style) -> Self {
1078 self.highlight_style = style;
1079 self
1080 }
1081
1082 #[must_use]
1084 pub fn show_scrollbar(mut self, show: bool) -> Self {
1085 self.show_scrollbar = show;
1086 self
1087 }
1088
1089 #[must_use]
1091 pub fn fixed_height(mut self, height: u16) -> Self {
1092 self.fixed_height = height;
1093 self
1094 }
1095}
1096
1097impl<T: RenderItem> StatefulWidget for VirtualizedList<'_, T> {
1098 type State = VirtualizedListState;
1099
1100 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1101 #[cfg(feature = "tracing")]
1102 let _span = tracing::debug_span!(
1103 "widget_render",
1104 widget = "VirtualizedList",
1105 x = area.x,
1106 y = area.y,
1107 w = area.width,
1108 h = area.height,
1109 items = self.items.len()
1110 )
1111 .entered();
1112
1113 if area.is_empty() {
1114 return;
1115 }
1116
1117 set_style_area(&mut frame.buffer, area, self.style);
1119
1120 let total_items = self.items.len();
1121 if total_items == 0 {
1122 return;
1123 }
1124
1125 let items_per_viewport = (area.height / self.fixed_height.max(1)) as usize;
1127 let needs_scrollbar = self.show_scrollbar && total_items > items_per_viewport;
1128 let content_width = if needs_scrollbar {
1129 area.width.saturating_sub(1)
1130 } else {
1131 area.width
1132 };
1133
1134 if let Some(selected) = state.selected
1136 && selected >= total_items
1137 {
1138 state.selected = if total_items > 0 {
1140 Some(total_items - 1)
1141 } else {
1142 None
1143 };
1144 }
1145
1146 if let Some(selected) = state.selected {
1148 if selected >= state.scroll_offset + items_per_viewport {
1149 state.scroll_offset = selected.saturating_sub(items_per_viewport.saturating_sub(1));
1150 } else if selected < state.scroll_offset {
1151 state.scroll_offset = selected;
1152 }
1153 }
1154
1155 let max_offset = total_items.saturating_sub(items_per_viewport);
1157 state.scroll_offset = state.scroll_offset.min(max_offset);
1158
1159 state.visible_count = items_per_viewport.min(total_items);
1161
1162 let render_start = state.scroll_offset.saturating_sub(state.overscan);
1164 let render_end = state
1165 .scroll_offset
1166 .saturating_add(items_per_viewport)
1167 .saturating_add(state.overscan)
1168 .min(total_items);
1169
1170 for idx in render_start..render_end {
1172 let idx_i32 = i32::try_from(idx).unwrap_or(i32::MAX);
1175 let offset_i32 = i32::try_from(state.scroll_offset).unwrap_or(i32::MAX);
1176 let relative_idx = idx_i32.saturating_sub(offset_i32);
1177 let height_i32 = i32::from(self.fixed_height);
1178 let y_offset = relative_idx.saturating_mul(height_i32);
1179
1180 if y_offset.saturating_add(height_i32) <= 0 {
1182 continue;
1183 }
1184
1185 if y_offset >= i32::from(area.height) {
1187 break;
1188 }
1189
1190 if i32::from(area.y).saturating_add(y_offset) < 0 {
1194 continue;
1195 }
1196
1197 let y = i32::from(area.y)
1200 .saturating_add(y_offset)
1201 .clamp(0, i32::from(u16::MAX)) as u16;
1202 if y >= area.bottom() {
1203 break;
1204 }
1205
1206 let visible_height = self.fixed_height.min(area.bottom().saturating_sub(y));
1207 if visible_height == 0 {
1208 continue;
1209 }
1210
1211 let row_area = Rect::new(area.x, y, content_width, visible_height);
1212
1213 let is_selected = state.selected == Some(idx);
1214
1215 if is_selected {
1217 set_style_area(&mut frame.buffer, row_area, self.highlight_style);
1218 }
1219
1220 self.items[idx].render(row_area, frame, is_selected);
1222 }
1223
1224 if needs_scrollbar {
1226 let scrollbar_area = Rect::new(area.right().saturating_sub(1), area.y, 1, area.height);
1227
1228 let mut scrollbar_state =
1229 ScrollbarState::new(total_items, state.scroll_offset, items_per_viewport);
1230
1231 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
1232 scrollbar.render(scrollbar_area, frame, &mut scrollbar_state);
1233 }
1234 }
1235}
1236
1237impl RenderItem for String {
1242 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1243 if area.is_empty() {
1244 return;
1245 }
1246 let max_chars = area.width as usize;
1247 for (i, ch) in self.chars().take(max_chars).enumerate() {
1248 frame
1249 .buffer
1250 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1251 }
1252 }
1253}
1254
1255impl RenderItem for &str {
1256 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1257 if area.is_empty() {
1258 return;
1259 }
1260 let max_chars = area.width as usize;
1261 for (i, ch) in self.chars().take(max_chars).enumerate() {
1262 frame
1263 .buffer
1264 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1265 }
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272
1273 #[test]
1274 fn test_new_virtualized() {
1275 let virt: Virtualized<String> = Virtualized::new(100);
1276 assert_eq!(virt.len(), 0);
1277 assert!(virt.is_empty());
1278 }
1279
1280 #[test]
1281 fn test_push_and_len() {
1282 let mut virt: Virtualized<i32> = Virtualized::new(100);
1283 virt.push(1);
1284 virt.push(2);
1285 virt.push(3);
1286 assert_eq!(virt.len(), 3);
1287 assert!(!virt.is_empty());
1288 }
1289
1290 #[test]
1291 fn test_visible_range_fixed_height() {
1292 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(2);
1293 for i in 0..20 {
1294 virt.push(i);
1295 }
1296 let range = virt.visible_range(20);
1298 assert_eq!(range, 0..10);
1299 }
1300
1301 #[test]
1302 fn test_visible_range_variable_height_clamps() {
1303 let mut cache = HeightCache::new(1, 16);
1304 cache.set(0, 3);
1305 cache.set(1, 3);
1306 cache.set(2, 3);
1307 let mut virt: Virtualized<i32> =
1308 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1309 for i in 0..3 {
1310 virt.push(i);
1311 }
1312 let range = virt.visible_range(5);
1313 assert_eq!(range, 0..1);
1314 }
1315
1316 #[test]
1317 fn test_visible_range_variable_height_exact_fit() {
1318 let mut cache = HeightCache::new(1, 16);
1319 cache.set(0, 2);
1320 cache.set(1, 3);
1321 let mut virt: Virtualized<i32> =
1322 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1323 for i in 0..2 {
1324 virt.push(i);
1325 }
1326 let range = virt.visible_range(5);
1327 assert_eq!(range, 0..2);
1328 }
1329
1330 #[test]
1331 fn test_visible_range_with_scroll() {
1332 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(1);
1333 for i in 0..50 {
1334 virt.push(i);
1335 }
1336 virt.scroll(10);
1337 let range = virt.visible_range(10);
1338 assert_eq!(range, 10..20);
1339 }
1340
1341 #[test]
1342 fn test_visible_range_variable_height_excludes_partial() {
1343 let mut cache = HeightCache::new(1, 16);
1344 cache.set(0, 6);
1345 cache.set(1, 6);
1346 let mut virt: Virtualized<i32> =
1347 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1348 virt.push(1);
1349 virt.push(2);
1350 virt.push(3);
1351
1352 let range = virt.visible_range(10);
1353 assert_eq!(range, 0..1);
1354 }
1355
1356 #[test]
1357 fn test_visible_range_variable_height_exact_fit_larger() {
1358 let mut cache = HeightCache::new(1, 16);
1359 cache.set(0, 4);
1360 cache.set(1, 6);
1361 let mut virt: Virtualized<i32> =
1362 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1363 virt.push(1);
1364 virt.push(2);
1365 virt.push(3);
1366
1367 let range = virt.visible_range(10);
1368 assert_eq!(range, 0..2);
1369 }
1370
1371 #[test]
1372 fn test_visible_range_variable_height_default_for_unmeasured() {
1373 let cache = HeightCache::new(2, 16);
1374 let mut virt: Virtualized<i32> =
1375 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1376 for i in 0..3 {
1377 virt.push(i);
1378 }
1379
1380 let range = virt.visible_range(5);
1382 assert_eq!(range, 0..2);
1383 }
1384
1385 #[test]
1386 fn test_render_range_with_overscan() {
1387 let mut virt: Virtualized<i32> =
1388 Virtualized::new(100).with_fixed_height(1).with_overscan(2);
1389 for i in 0..50 {
1390 virt.push(i);
1391 }
1392 virt.scroll(10);
1393 let range = virt.render_range(10);
1394 assert_eq!(range, 8..22);
1397 }
1398
1399 #[test]
1400 fn test_scroll_bounds() {
1401 let mut virt: Virtualized<i32> = Virtualized::new(100);
1402 for i in 0..10 {
1403 virt.push(i);
1404 }
1405
1406 virt.scroll(-100);
1408 assert_eq!(virt.scroll_offset(), 0);
1409
1410 virt.scroll(100);
1412 assert_eq!(virt.scroll_offset(), 9);
1413 }
1414
1415 #[test]
1416 fn test_scroll_to() {
1417 let mut virt: Virtualized<i32> = Virtualized::new(100);
1418 for i in 0..20 {
1419 virt.push(i);
1420 }
1421
1422 virt.scroll_to(15);
1423 assert_eq!(virt.scroll_offset(), 15);
1424
1425 virt.scroll_to(100);
1427 assert_eq!(virt.scroll_offset(), 19);
1428 }
1429
1430 #[test]
1431 fn test_follow_mode() {
1432 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
1433 virt.set_visible_count(5);
1434
1435 for i in 0..10 {
1436 virt.push(i);
1437 }
1438
1439 assert!(virt.is_at_bottom());
1441
1442 virt.scroll(-5);
1444 assert!(!virt.follow_mode());
1445 }
1446
1447 #[test]
1448 fn test_scroll_to_start_and_end() {
1449 let mut virt: Virtualized<i32> = Virtualized::new(100);
1450 virt.set_visible_count(5);
1451 for i in 0..20 {
1452 virt.push(i);
1453 }
1454
1455 virt.scroll_to(10);
1457 virt.set_follow(true);
1458 virt.scroll_to_start();
1459 assert_eq!(virt.scroll_offset(), 0);
1460 assert!(!virt.follow_mode());
1461
1462 virt.scroll_to_end();
1464 assert!(virt.is_at_bottom());
1465 assert!(virt.follow_mode());
1466 }
1467
1468 #[test]
1469 fn test_virtualized_page_navigation() {
1470 let mut virt: Virtualized<i32> = Virtualized::new(100);
1471 virt.set_visible_count(5);
1472 for i in 0..30 {
1473 virt.push(i);
1474 }
1475
1476 virt.scroll_to(15);
1477 virt.page_up();
1478 assert_eq!(virt.scroll_offset(), 10);
1479
1480 virt.page_down();
1481 assert_eq!(virt.scroll_offset(), 15);
1482
1483 virt.scroll_to(2);
1485 virt.page_up();
1486 assert_eq!(virt.scroll_offset(), 0);
1487 }
1488
1489 #[test]
1490 fn test_height_cache() {
1491 let mut cache = HeightCache::new(1, 100);
1492
1493 assert_eq!(cache.get(0), 1);
1495 assert_eq!(cache.get(50), 1);
1496
1497 cache.set(5, 3);
1499 assert_eq!(cache.get(5), 3);
1500
1501 assert_eq!(cache.get(4), 1);
1503 assert_eq!(cache.get(6), 1);
1504 }
1505
1506 #[test]
1507 fn test_height_cache_large_index_window() {
1508 let mut cache = HeightCache::new(1, 8);
1509 cache.set(10_000, 4);
1510 assert_eq!(cache.get(10_000), 4);
1511 assert_eq!(cache.get(0), 1);
1512 assert!(cache.cache.len() <= cache.capacity);
1513 }
1514
1515 #[test]
1516 fn test_clear() {
1517 let mut virt: Virtualized<i32> = Virtualized::new(100);
1518 for i in 0..10 {
1519 virt.push(i);
1520 }
1521 virt.scroll(5);
1522
1523 virt.clear();
1524 assert_eq!(virt.len(), 0);
1525 assert_eq!(virt.scroll_offset(), 0);
1526 }
1527
1528 #[test]
1529 fn test_get_item() {
1530 let mut virt: Virtualized<String> = Virtualized::new(100);
1531 virt.push("hello".to_string());
1532 virt.push("world".to_string());
1533
1534 assert_eq!(virt.get(0), Some(&"hello".to_string()));
1535 assert_eq!(virt.get(1), Some(&"world".to_string()));
1536 assert_eq!(virt.get(2), None);
1537 }
1538
1539 #[test]
1540 fn test_external_storage_len() {
1541 let mut virt: Virtualized<i32> = Virtualized::external(1000, 100);
1542 assert_eq!(virt.len(), 1000);
1543
1544 virt.set_external_len(2000);
1545 assert_eq!(virt.len(), 2000);
1546 }
1547
1548 #[test]
1549 fn test_momentum_scrolling() {
1550 let mut virt: Virtualized<i32> = Virtualized::new(100);
1551 for i in 0..50 {
1552 virt.push(i);
1553 }
1554
1555 virt.fling(10.0);
1556
1557 virt.tick(Duration::from_millis(100));
1559
1560 assert!(virt.scroll_offset() > 0);
1562 }
1563
1564 #[test]
1569 fn test_virtualized_list_state_new() {
1570 let state = VirtualizedListState::new();
1571 assert_eq!(state.selected, None);
1572 assert_eq!(state.scroll_offset(), 0);
1573 assert_eq!(state.visible_count(), 0);
1574 }
1575
1576 #[test]
1577 fn test_virtualized_list_state_select_next() {
1578 let mut state = VirtualizedListState::new();
1579
1580 state.select_next(10);
1581 assert_eq!(state.selected, Some(0));
1582
1583 state.select_next(10);
1584 assert_eq!(state.selected, Some(1));
1585
1586 state.selected = Some(9);
1588 state.select_next(10);
1589 assert_eq!(state.selected, Some(9));
1590 }
1591
1592 #[test]
1593 fn test_virtualized_list_state_select_previous() {
1594 let mut state = VirtualizedListState::new();
1595 state.selected = Some(5);
1596
1597 state.select_previous(10);
1598 assert_eq!(state.selected, Some(4));
1599
1600 state.selected = Some(0);
1601 state.select_previous(10);
1602 assert_eq!(state.selected, Some(0));
1603 }
1604
1605 #[test]
1606 fn test_virtualized_list_state_scroll() {
1607 let mut state = VirtualizedListState::new();
1608
1609 state.scroll(5, 20);
1610 assert_eq!(state.scroll_offset(), 5);
1611
1612 state.scroll(-3, 20);
1613 assert_eq!(state.scroll_offset(), 2);
1614
1615 state.scroll(-100, 20);
1617 assert_eq!(state.scroll_offset(), 0);
1618
1619 state.scroll(100, 20);
1621 assert_eq!(state.scroll_offset(), 19);
1622 }
1623
1624 #[test]
1625 fn test_virtualized_list_state_follow_mode() {
1626 let mut state = VirtualizedListState::new().with_follow(true);
1627 assert!(state.follow_mode());
1628
1629 state.scroll(5, 20);
1631 assert!(!state.follow_mode());
1632 }
1633
1634 #[test]
1635 fn test_render_item_string() {
1636 let s = String::from("hello");
1638 assert_eq!(s.height(), 1);
1639 }
1640
1641 #[test]
1642 fn test_page_up_down() {
1643 let mut virt: Virtualized<i32> = Virtualized::new(100);
1644 for i in 0..50 {
1645 virt.push(i);
1646 }
1647 virt.set_visible_count(10);
1648
1649 assert_eq!(virt.scroll_offset(), 0);
1651
1652 virt.page_down();
1654 assert_eq!(virt.scroll_offset(), 10);
1655
1656 virt.page_down();
1658 assert_eq!(virt.scroll_offset(), 20);
1659
1660 virt.page_up();
1662 assert_eq!(virt.scroll_offset(), 10);
1663
1664 virt.page_up();
1666 assert_eq!(virt.scroll_offset(), 0);
1667
1668 virt.page_up();
1670 assert_eq!(virt.scroll_offset(), 0);
1671 }
1672
1673 #[test]
1678 fn test_render_scales_with_visible_not_total() {
1679 use ftui_render::grapheme_pool::GraphemePool;
1680 use std::time::Instant;
1681
1682 let small_items: Vec<String> = (0..1_000).map(|i| format!("Line {}", i)).collect();
1684 let small_list = VirtualizedList::new(&small_items);
1685 let mut small_state = VirtualizedListState::new();
1686
1687 let area = Rect::new(0, 0, 80, 24);
1688 let mut pool = GraphemePool::new();
1689 let mut frame = Frame::new(80, 24, &mut pool);
1690
1691 small_list.render(area, &mut frame, &mut small_state);
1693
1694 let start = Instant::now();
1695 for _ in 0..100 {
1696 frame.buffer.clear();
1697 small_list.render(area, &mut frame, &mut small_state);
1698 }
1699 let small_time = start.elapsed();
1700
1701 let large_items: Vec<String> = (0..100_000).map(|i| format!("Line {}", i)).collect();
1703 let large_list = VirtualizedList::new(&large_items);
1704 let mut large_state = VirtualizedListState::new();
1705
1706 large_list.render(area, &mut frame, &mut large_state);
1708
1709 let start = Instant::now();
1710 for _ in 0..100 {
1711 frame.buffer.clear();
1712 large_list.render(area, &mut frame, &mut large_state);
1713 }
1714 let large_time = start.elapsed();
1715
1716 assert!(
1718 large_time < small_time * 3,
1719 "Render does not scale O(visible): 1K={:?}, 100K={:?}",
1720 small_time,
1721 large_time
1722 );
1723 }
1724
1725 #[test]
1726 fn test_scroll_is_constant_time() {
1727 use std::time::Instant;
1728
1729 let mut small: Virtualized<i32> = Virtualized::new(1_000);
1730 for i in 0..1_000 {
1731 small.push(i);
1732 }
1733 small.set_visible_count(24);
1734
1735 let mut large: Virtualized<i32> = Virtualized::new(100_000);
1736 for i in 0..100_000 {
1737 large.push(i);
1738 }
1739 large.set_visible_count(24);
1740
1741 let iterations = 10_000;
1742
1743 let start = Instant::now();
1744 for _ in 0..iterations {
1745 small.scroll(1);
1746 small.scroll(-1);
1747 }
1748 let small_time = start.elapsed();
1749
1750 let start = Instant::now();
1751 for _ in 0..iterations {
1752 large.scroll(1);
1753 large.scroll(-1);
1754 }
1755 let large_time = start.elapsed();
1756
1757 assert!(
1759 large_time < small_time * 3,
1760 "Scroll is not O(1): 1K={:?}, 100K={:?}",
1761 small_time,
1762 large_time
1763 );
1764 }
1765
1766 #[test]
1767 fn render_partially_offscreen_top_skips_item() {
1768 use ftui_render::grapheme_pool::GraphemePool;
1769
1770 struct IndexedItem(usize);
1772 impl RenderItem for IndexedItem {
1773 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1774 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1775 for y in area.y..area.bottom() {
1776 frame.buffer.set(area.x, y, Cell::from_char(ch));
1777 }
1778 }
1779 fn height(&self) -> u16 {
1780 2
1781 }
1782 }
1783
1784 let items = vec![
1787 IndexedItem(0),
1788 IndexedItem(1),
1789 IndexedItem(2),
1790 IndexedItem(3),
1791 ];
1792 let list = VirtualizedList::new(&items).fixed_height(2);
1793
1794 let mut state = VirtualizedListState::new().with_overscan(1);
1796 state.scroll_offset = 1; let mut pool = GraphemePool::new();
1799 let mut frame = Frame::new(10, 5, &mut pool);
1800
1801 list.render(Rect::new(0, 0, 10, 5), &mut frame, &mut state);
1803
1804 let cell = frame.buffer.get(0, 0).unwrap();
1812 assert_eq!(cell.content.as_char(), Some('1'));
1813 }
1814
1815 #[test]
1816 fn render_bottom_boundary_clips_partial_item() {
1817 use ftui_render::grapheme_pool::GraphemePool;
1818
1819 struct IndexedItem(u16);
1820 impl RenderItem for IndexedItem {
1821 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1822 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1823 for y in area.y..area.bottom() {
1824 frame.buffer.set(area.x, y, Cell::from_char(ch));
1825 }
1826 }
1827 fn height(&self) -> u16 {
1828 2
1829 }
1830 }
1831
1832 let items = vec![IndexedItem(0), IndexedItem(1), IndexedItem(2)];
1833 let list = VirtualizedList::new(&items)
1834 .fixed_height(2)
1835 .show_scrollbar(false);
1836 let mut state = VirtualizedListState::new();
1837
1838 let mut pool = GraphemePool::new();
1839 let mut frame = Frame::new(4, 4, &mut pool);
1840
1841 list.render(Rect::new(0, 0, 4, 3), &mut frame, &mut state);
1843
1844 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1845 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('0'));
1846 assert_eq!(frame.buffer.get(0, 2).unwrap().content.as_char(), Some('1'));
1847 assert_eq!(frame.buffer.get(0, 3).unwrap().content.as_char(), None);
1849 }
1850
1851 #[test]
1852 fn render_after_fling_advances_visible_rows() {
1853 use ftui_render::grapheme_pool::GraphemePool;
1854
1855 struct IndexedItem(u16);
1856 impl RenderItem for IndexedItem {
1857 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1858 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1859 for y in area.y..area.bottom() {
1860 frame.buffer.set(area.x, y, Cell::from_char(ch));
1861 }
1862 }
1863 }
1864
1865 let items: Vec<IndexedItem> = (0..10).map(IndexedItem).collect();
1866 let list = VirtualizedList::new(&items)
1867 .fixed_height(1)
1868 .show_scrollbar(false);
1869 let mut state = VirtualizedListState::new();
1870
1871 let mut pool = GraphemePool::new();
1872 let mut frame = Frame::new(4, 3, &mut pool);
1873 let area = Rect::new(0, 0, 4, 3);
1874
1875 list.render(area, &mut frame, &mut state);
1877 assert_eq!(state.scroll_offset(), 0);
1878 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1879
1880 state.fling(40.0);
1882 state.tick(Duration::from_millis(100), items.len());
1883 assert_eq!(state.scroll_offset(), 4);
1884
1885 frame.buffer.clear();
1886 list.render(area, &mut frame, &mut state);
1887 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('4'));
1888 }
1889
1890 #[test]
1891 fn test_memory_bounded_by_ring_capacity() {
1892 use crate::log_ring::LogRing;
1893
1894 let mut ring: LogRing<String> = LogRing::new(1_000);
1895
1896 for i in 0..100_000 {
1898 ring.push(format!("Line {}", i));
1899 }
1900
1901 assert_eq!(ring.len(), 1_000);
1903 assert_eq!(ring.total_count(), 100_000);
1904 assert_eq!(ring.first_index(), 99_000);
1905
1906 assert!(ring.get(99_999).is_some());
1908 assert!(ring.get(99_000).is_some());
1909 assert!(ring.get(0).is_none());
1911 assert!(ring.get(98_999).is_none());
1912 }
1913
1914 #[test]
1915 fn test_visible_range_constant_regardless_of_total() {
1916 let mut small: Virtualized<i32> = Virtualized::new(100);
1917 for i in 0..100 {
1918 small.push(i);
1919 }
1920 let small_range = small.visible_range(24);
1921
1922 let mut large: Virtualized<i32> = Virtualized::new(100_000);
1923 for i in 0..100_000 {
1924 large.push(i);
1925 }
1926 let large_range = large.visible_range(24);
1927
1928 assert_eq!(small_range.end - small_range.start, 24);
1930 assert_eq!(large_range.end - large_range.start, 24);
1931 }
1932
1933 #[test]
1934 fn test_virtualized_list_state_page_up_down() {
1935 let mut state = VirtualizedListState::new();
1936 state.visible_count = 10;
1937
1938 state.page_down(50);
1940 assert_eq!(state.scroll_offset(), 10);
1941
1942 state.page_down(50);
1944 assert_eq!(state.scroll_offset(), 20);
1945
1946 state.page_up(50);
1948 assert_eq!(state.scroll_offset(), 10);
1949
1950 state.page_up(50);
1952 assert_eq!(state.scroll_offset(), 0);
1953 }
1954
1955 #[test]
1960 fn test_variable_heights_fenwick_new() {
1961 let tracker = VariableHeightsFenwick::new(2, 10);
1962 assert_eq!(tracker.len(), 10);
1963 assert!(!tracker.is_empty());
1964 assert_eq!(tracker.default_height(), 2);
1965 }
1966
1967 #[test]
1968 fn test_variable_heights_fenwick_empty() {
1969 let tracker = VariableHeightsFenwick::new(1, 0);
1970 assert!(tracker.is_empty());
1971 assert_eq!(tracker.total_height(), 0);
1972 }
1973
1974 #[test]
1975 fn test_variable_heights_fenwick_from_heights() {
1976 let heights = vec![3, 2, 5, 1, 4];
1977 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
1978
1979 assert_eq!(tracker.len(), 5);
1980 assert_eq!(tracker.get(0), 3);
1981 assert_eq!(tracker.get(1), 2);
1982 assert_eq!(tracker.get(2), 5);
1983 assert_eq!(tracker.get(3), 1);
1984 assert_eq!(tracker.get(4), 4);
1985 assert_eq!(tracker.total_height(), 15);
1986 }
1987
1988 #[test]
1989 fn test_variable_heights_fenwick_offset_of_item() {
1990 let heights = vec![3, 2, 5, 1, 4];
1992 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
1993
1994 assert_eq!(tracker.offset_of_item(0), 0);
1995 assert_eq!(tracker.offset_of_item(1), 3);
1996 assert_eq!(tracker.offset_of_item(2), 5);
1997 assert_eq!(tracker.offset_of_item(3), 10);
1998 assert_eq!(tracker.offset_of_item(4), 11);
1999 assert_eq!(tracker.offset_of_item(5), 15); }
2001
2002 #[test]
2003 fn test_variable_heights_fenwick_find_item_at_offset() {
2004 let heights = vec![3, 2, 5, 1, 4];
2006 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2007
2008 assert_eq!(tracker.find_item_at_offset(0), 0);
2010 assert_eq!(tracker.find_item_at_offset(1), 0);
2012 assert_eq!(tracker.find_item_at_offset(3), 1);
2014 assert_eq!(tracker.find_item_at_offset(5), 2);
2016 assert_eq!(tracker.find_item_at_offset(10), 3);
2018 assert_eq!(tracker.find_item_at_offset(11), 4);
2020 assert_eq!(tracker.find_item_at_offset(15), 5);
2022 }
2023
2024 #[test]
2025 fn test_variable_heights_fenwick_visible_count() {
2026 let heights = vec![3, 2, 5, 1, 4];
2028 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2029
2030 assert_eq!(tracker.visible_count(0, 5), 2);
2032
2033 assert_eq!(tracker.visible_count(0, 4), 1);
2035
2036 assert_eq!(tracker.visible_count(0, 10), 3);
2038
2039 assert_eq!(tracker.visible_count(2, 6), 2);
2041 }
2042
2043 #[test]
2044 fn test_variable_heights_fenwick_visible_count_viewport_beyond_total_height() {
2045 let heights = vec![1, 1, 1];
2046 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2047
2048 assert_eq!(tracker.visible_count(0, 10), 3);
2050 assert_eq!(tracker.visible_count(1, 10), 2);
2051 assert_eq!(tracker.visible_count(2, 10), 1);
2052 }
2053
2054 #[test]
2055 fn test_variable_heights_fenwick_set() {
2056 let mut tracker = VariableHeightsFenwick::new(1, 5);
2057
2058 assert_eq!(tracker.get(0), 1);
2060 assert_eq!(tracker.total_height(), 5);
2061
2062 tracker.set(2, 10);
2064 assert_eq!(tracker.get(2), 10);
2065 assert_eq!(tracker.total_height(), 14); }
2067
2068 #[test]
2069 fn test_variable_heights_fenwick_resize() {
2070 let mut tracker = VariableHeightsFenwick::new(2, 3);
2071 assert_eq!(tracker.len(), 3);
2072 assert_eq!(tracker.total_height(), 6);
2073
2074 tracker.resize(5);
2076 assert_eq!(tracker.len(), 5);
2077 assert_eq!(tracker.total_height(), 10);
2078 assert_eq!(tracker.get(4), 2);
2079
2080 tracker.resize(2);
2082 assert_eq!(tracker.len(), 2);
2083 assert_eq!(tracker.total_height(), 4);
2084 }
2085
2086 #[test]
2087 fn test_virtualized_with_variable_heights_fenwick() {
2088 let mut virt: Virtualized<i32> = Virtualized::new(100).with_variable_heights_fenwick(2, 10);
2089
2090 for i in 0..10 {
2091 virt.push(i);
2092 }
2093
2094 let range = virt.visible_range(6);
2096 assert_eq!(range.end - range.start, 3);
2097 }
2098
2099 #[test]
2100 fn test_variable_heights_fenwick_performance() {
2101 use std::time::Instant;
2102
2103 let n = 100_000;
2105 let heights: Vec<u16> = (0..n).map(|i| (i % 10 + 1) as u16).collect();
2106 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2107
2108 let _ = tracker.find_item_at_offset(500_000);
2110 let _ = tracker.offset_of_item(50_000);
2111
2112 let start = Instant::now();
2114 let mut _sink = 0usize;
2115 for i in 0..10_000 {
2116 _sink = _sink.wrapping_add(tracker.find_item_at_offset((i * 50) as u32));
2117 }
2118 let find_time = start.elapsed();
2119
2120 let start = Instant::now();
2122 let mut _sink2 = 0u32;
2123 for i in 0..10_000 {
2124 _sink2 = _sink2.wrapping_add(tracker.offset_of_item((i * 10) % n));
2125 }
2126 let offset_time = start.elapsed();
2127
2128 eprintln!("=== VariableHeightsFenwick Performance (n={n}) ===");
2129 eprintln!("10k find_item_at_offset: {:?}", find_time);
2130 eprintln!("10k offset_of_item: {:?}", offset_time);
2131
2132 assert!(
2134 find_time < std::time::Duration::from_millis(50),
2135 "find_item_at_offset too slow: {:?}",
2136 find_time
2137 );
2138 assert!(
2139 offset_time < std::time::Duration::from_millis(50),
2140 "offset_of_item too slow: {:?}",
2141 offset_time
2142 );
2143 }
2144
2145 #[test]
2146 fn test_variable_heights_fenwick_scales_logarithmically() {
2147 use std::time::Instant;
2148
2149 let small_n = 1_000;
2151 let small_heights: Vec<u16> = (0..small_n).map(|i| (i % 5 + 1) as u16).collect();
2152 let small_tracker = VariableHeightsFenwick::from_heights(&small_heights, 1);
2153
2154 let large_n = 100_000;
2156 let large_heights: Vec<u16> = (0..large_n).map(|i| (i % 5 + 1) as u16).collect();
2157 let large_tracker = VariableHeightsFenwick::from_heights(&large_heights, 1);
2158
2159 let iterations = 5_000;
2160
2161 let start = Instant::now();
2163 for i in 0..iterations {
2164 let _ = small_tracker.find_item_at_offset((i * 2) as u32);
2165 }
2166 let small_time = start.elapsed();
2167
2168 let start = Instant::now();
2170 for i in 0..iterations {
2171 let _ = large_tracker.find_item_at_offset((i * 200) as u32);
2172 }
2173 let large_time = start.elapsed();
2174
2175 assert!(
2177 large_time < small_time * 5,
2178 "Not O(log n): small={:?}, large={:?}",
2179 small_time,
2180 large_time
2181 );
2182 }
2183
2184 #[test]
2191 fn new_zero_capacity() {
2192 let virt: Virtualized<i32> = Virtualized::new(0);
2193 assert_eq!(virt.len(), 0);
2194 assert!(virt.is_empty());
2195 assert_eq!(virt.scroll_offset(), 0);
2196 assert_eq!(virt.visible_count(), 0);
2197 assert!(!virt.follow_mode());
2198 }
2199
2200 #[test]
2201 fn external_zero_len_zero_cache() {
2202 let virt: Virtualized<i32> = Virtualized::external(0, 0);
2203 assert_eq!(virt.len(), 0);
2204 assert!(virt.is_empty());
2205 }
2206
2207 #[test]
2208 fn external_storage_returns_none_for_get() {
2209 let virt: Virtualized<i32> = Virtualized::external(100, 10);
2210 assert_eq!(virt.get(0), None);
2211 assert_eq!(virt.get(50), None);
2212 }
2213
2214 #[test]
2215 fn external_storage_returns_none_for_get_mut() {
2216 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2217 assert!(virt.get_mut(0).is_none());
2218 }
2219
2220 #[test]
2221 fn push_on_external_is_noop() {
2222 let mut virt: Virtualized<i32> = Virtualized::external(5, 10);
2223 virt.push(42);
2224 assert_eq!(virt.len(), 5);
2226 }
2227
2228 #[test]
2229 fn iter_on_external_is_empty() {
2230 let virt: Virtualized<i32> = Virtualized::external(100, 10);
2231 assert_eq!(virt.iter().count(), 0);
2232 }
2233
2234 #[test]
2235 fn set_external_len_on_owned_is_noop() {
2236 let mut virt: Virtualized<i32> = Virtualized::new(100);
2237 virt.push(1);
2238 virt.set_external_len(999);
2239 assert_eq!(virt.len(), 1); }
2241
2242 #[test]
2245 fn visible_range_zero_viewport() {
2246 let mut virt: Virtualized<i32> = Virtualized::new(100);
2247 virt.push(1);
2248 let range = virt.visible_range(0);
2249 assert_eq!(range, 0..0);
2250 assert_eq!(virt.visible_count(), 0);
2251 }
2252
2253 #[test]
2254 fn visible_range_empty_container() {
2255 let virt: Virtualized<i32> = Virtualized::new(100);
2256 let range = virt.visible_range(24);
2257 assert_eq!(range, 0..0);
2258 }
2259
2260 #[test]
2261 fn visible_range_fixed_height_zero() {
2262 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(0);
2264 for i in 0..10 {
2265 virt.push(i);
2266 }
2267 let range = virt.visible_range(5);
2268 assert_eq!(range, 0..5);
2270 }
2271
2272 #[test]
2273 fn visible_range_fewer_items_than_viewport() {
2274 let mut virt: Virtualized<i32> = Virtualized::new(100);
2275 for i in 0..3 {
2276 virt.push(i);
2277 }
2278 let range = virt.visible_range(24);
2279 assert_eq!(range, 0..3);
2281 }
2282
2283 #[test]
2284 fn visible_range_single_item() {
2285 let mut virt: Virtualized<i32> = Virtualized::new(100);
2286 virt.push(42);
2287 let range = virt.visible_range(1);
2288 assert_eq!(range, 0..1);
2289 }
2290
2291 #[test]
2294 fn render_range_at_start_clamps_overscan() {
2295 let mut virt: Virtualized<i32> =
2296 Virtualized::new(100).with_fixed_height(1).with_overscan(5);
2297 for i in 0..20 {
2298 virt.push(i);
2299 }
2300 let range = virt.render_range(10);
2302 assert_eq!(range.start, 0);
2303 }
2304
2305 #[test]
2306 fn render_range_at_end_clamps_overscan() {
2307 let mut virt: Virtualized<i32> =
2308 Virtualized::new(100).with_fixed_height(1).with_overscan(5);
2309 for i in 0..20 {
2310 virt.push(i);
2311 }
2312 virt.set_visible_count(10);
2313 virt.scroll_to(10); let range = virt.render_range(10);
2315 assert_eq!(range.end, 20);
2317 }
2318
2319 #[test]
2320 fn render_range_zero_overscan() {
2321 let mut virt: Virtualized<i32> =
2322 Virtualized::new(100).with_fixed_height(1).with_overscan(0);
2323 for i in 0..20 {
2324 virt.push(i);
2325 }
2326 virt.set_visible_count(10);
2327 virt.scroll_to(5);
2328 let range = virt.render_range(10);
2329 let visible = virt.visible_range(10);
2331 assert_eq!(range, visible);
2332 }
2333
2334 #[test]
2337 fn scroll_on_empty_is_noop() {
2338 let mut virt: Virtualized<i32> = Virtualized::new(100);
2339 virt.scroll(10);
2340 assert_eq!(virt.scroll_offset(), 0);
2341 }
2342
2343 #[test]
2344 fn scroll_delta_zero_does_not_disable_follow() {
2345 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2346 virt.push(1);
2347 virt.scroll(0);
2348 assert!(virt.follow_mode());
2350 }
2351
2352 #[test]
2353 fn scroll_negative_beyond_start() {
2354 let mut virt: Virtualized<i32> = Virtualized::new(100);
2355 for i in 0..10 {
2356 virt.push(i);
2357 }
2358 virt.scroll(-1);
2359 assert_eq!(virt.scroll_offset(), 0);
2360 }
2361
2362 #[test]
2363 fn scroll_to_on_empty() {
2364 let mut virt: Virtualized<i32> = Virtualized::new(100);
2365 virt.scroll_to(100);
2367 assert_eq!(virt.scroll_offset(), 0);
2368 }
2369
2370 #[test]
2371 fn scroll_to_top_already_at_top() {
2372 let mut virt: Virtualized<i32> = Virtualized::new(100);
2373 virt.push(1);
2374 virt.scroll_to_top();
2375 assert_eq!(virt.scroll_offset(), 0);
2376 }
2377
2378 #[test]
2379 fn scroll_to_bottom_fewer_items_than_visible() {
2380 let mut virt: Virtualized<i32> = Virtualized::new(100);
2381 virt.set_visible_count(10);
2382 for i in 0..3 {
2383 virt.push(i);
2384 }
2385 virt.scroll_to_bottom();
2386 assert_eq!(virt.scroll_offset(), 0);
2388 }
2389
2390 #[test]
2391 fn scroll_to_bottom_visible_count_zero() {
2392 let mut virt: Virtualized<i32> = Virtualized::new(100);
2393 for i in 0..20 {
2394 virt.push(i);
2395 }
2396 virt.scroll_to_bottom();
2398 assert_eq!(virt.scroll_offset(), 0);
2399 }
2400
2401 #[test]
2404 fn page_up_visible_count_zero_is_noop() {
2405 let mut virt: Virtualized<i32> = Virtualized::new(100);
2406 for i in 0..20 {
2407 virt.push(i);
2408 }
2409 virt.scroll_to(10);
2410 virt.page_up();
2412 assert_eq!(virt.scroll_offset(), 10);
2413 }
2414
2415 #[test]
2416 fn page_down_visible_count_zero_is_noop() {
2417 let mut virt: Virtualized<i32> = Virtualized::new(100);
2418 for i in 0..20 {
2419 virt.push(i);
2420 }
2421 virt.page_down();
2423 assert_eq!(virt.scroll_offset(), 0);
2424 }
2425
2426 #[test]
2429 fn is_at_bottom_fewer_items_than_visible() {
2430 let mut virt: Virtualized<i32> = Virtualized::new(100);
2431 virt.set_visible_count(10);
2432 for i in 0..3 {
2433 virt.push(i);
2434 }
2435 assert!(virt.is_at_bottom());
2436 }
2437
2438 #[test]
2439 fn is_at_bottom_empty() {
2440 let virt: Virtualized<i32> = Virtualized::new(100);
2441 assert!(virt.is_at_bottom());
2443 }
2444
2445 #[test]
2448 fn trim_front_under_max_returns_zero() {
2449 let mut virt: Virtualized<i32> = Virtualized::new(100);
2450 for i in 0..5 {
2451 virt.push(i);
2452 }
2453 let removed = virt.trim_front(10);
2454 assert_eq!(removed, 0);
2455 assert_eq!(virt.len(), 5);
2456 }
2457
2458 #[test]
2459 fn trim_front_adjusts_scroll_offset() {
2460 let mut virt: Virtualized<i32> = Virtualized::new(100);
2461 for i in 0..20 {
2462 virt.push(i);
2463 }
2464 virt.scroll_to(10);
2465 let removed = virt.trim_front(15);
2466 assert_eq!(removed, 5);
2467 assert_eq!(virt.len(), 15);
2468 assert_eq!(virt.scroll_offset(), 5);
2470 }
2471
2472 #[test]
2473 fn trim_front_scroll_offset_saturates_to_zero() {
2474 let mut virt: Virtualized<i32> = Virtualized::new(100);
2475 for i in 0..20 {
2476 virt.push(i);
2477 }
2478 virt.scroll_to(2);
2479 let removed = virt.trim_front(10);
2480 assert_eq!(removed, 10);
2481 assert_eq!(virt.scroll_offset(), 0);
2483 }
2484
2485 #[test]
2486 fn trim_front_on_external_returns_zero() {
2487 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2488 let removed = virt.trim_front(5);
2489 assert_eq!(removed, 0);
2490 }
2491
2492 #[test]
2495 fn clear_on_external_resets_scroll() {
2496 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2497 virt.scroll_to(50);
2498 virt.clear();
2499 assert_eq!(virt.scroll_offset(), 0);
2500 assert_eq!(virt.len(), 100);
2502 }
2503
2504 #[test]
2507 fn tick_zero_velocity_is_noop() {
2508 let mut virt: Virtualized<i32> = Virtualized::new(100);
2509 for i in 0..20 {
2510 virt.push(i);
2511 }
2512 virt.tick(Duration::from_millis(100));
2513 assert_eq!(virt.scroll_offset(), 0);
2514 }
2515
2516 #[test]
2517 fn tick_below_threshold_stops_momentum() {
2518 let mut virt: Virtualized<i32> = Virtualized::new(100);
2519 for i in 0..20 {
2520 virt.push(i);
2521 }
2522 virt.fling(0.05); virt.tick(Duration::from_millis(100));
2524 assert_eq!(virt.scroll_offset(), 0);
2526 }
2527
2528 #[test]
2529 fn tick_zero_duration_no_scroll() {
2530 let mut virt: Virtualized<i32> = Virtualized::new(100);
2531 for i in 0..50 {
2532 virt.push(i);
2533 }
2534 virt.fling(100.0);
2535 virt.tick(Duration::ZERO);
2536 assert_eq!(virt.scroll_offset(), 0);
2538 }
2539
2540 #[test]
2541 fn fling_negative_scrolls_up() {
2542 let mut virt: Virtualized<i32> = Virtualized::new(100);
2543 for i in 0..50 {
2544 virt.push(i);
2545 }
2546 virt.scroll(20);
2547 let before = virt.scroll_offset();
2548 virt.fling(-50.0);
2549 virt.tick(Duration::from_millis(100));
2550 assert!(virt.scroll_offset() < before);
2551 }
2552
2553 #[test]
2556 fn follow_mode_auto_scrolls_on_push() {
2557 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2558 virt.set_visible_count(5);
2559 for i in 0..20 {
2560 virt.push(i);
2561 }
2562 assert!(virt.is_at_bottom());
2564 assert_eq!(virt.scroll_offset(), 15); }
2566
2567 #[test]
2568 fn set_follow_false_does_not_scroll() {
2569 let mut virt: Virtualized<i32> = Virtualized::new(100);
2570 virt.set_visible_count(5);
2571 for i in 0..20 {
2572 virt.push(i);
2573 }
2574 virt.scroll_to(5);
2575 virt.set_follow(false);
2576 assert_eq!(virt.scroll_offset(), 5); }
2578
2579 #[test]
2580 fn scroll_to_start_disables_follow() {
2581 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2582 virt.set_visible_count(5);
2583 for i in 0..20 {
2584 virt.push(i);
2585 }
2586 virt.scroll_to_start();
2587 assert!(!virt.follow_mode());
2588 assert_eq!(virt.scroll_offset(), 0);
2589 }
2590
2591 #[test]
2592 fn scroll_to_end_enables_follow() {
2593 let mut virt: Virtualized<i32> = Virtualized::new(100);
2594 virt.set_visible_count(5);
2595 for i in 0..20 {
2596 virt.push(i);
2597 }
2598 assert!(!virt.follow_mode());
2599 virt.scroll_to_end();
2600 assert!(virt.follow_mode());
2601 assert!(virt.is_at_bottom());
2602 }
2603
2604 #[test]
2605 fn external_follow_mode_scrolls_on_set_external_len() {
2606 let mut virt: Virtualized<i32> = Virtualized::external(10, 100).with_follow(true);
2607 virt.set_visible_count(5);
2608 virt.set_external_len(20);
2609 assert_eq!(virt.len(), 20);
2610 assert!(virt.is_at_bottom());
2611 }
2612
2613 #[test]
2616 fn builder_chain_all_options() {
2617 let virt: Virtualized<i32> = Virtualized::new(100)
2618 .with_fixed_height(3)
2619 .with_overscan(5)
2620 .with_follow(true);
2621 assert!(virt.follow_mode());
2622 let range = virt.visible_range(9);
2625 assert_eq!(range, 0..0);
2626 }
2627
2628 #[test]
2631 fn height_cache_default() {
2632 let cache = HeightCache::default();
2633 assert_eq!(cache.get(0), 1); assert_eq!(cache.capacity, 1000);
2635 }
2636
2637 #[test]
2638 fn height_cache_get_before_base_offset() {
2639 let mut cache = HeightCache::new(5, 100);
2640 cache.set(200, 10); assert_eq!(cache.get(0), 5);
2644 }
2645
2646 #[test]
2647 fn height_cache_set_before_base_offset_ignored() {
2648 let mut cache = HeightCache::new(5, 100);
2649 cache.set(200, 10);
2650 let base = cache.base_offset;
2651 cache.set(0, 99); assert_eq!(cache.get(0), 5); assert_eq!(cache.base_offset, base); }
2655
2656 #[test]
2657 fn height_cache_capacity_zero_ignores_all_sets() {
2658 let mut cache = HeightCache::new(3, 0);
2659 cache.set(0, 10);
2660 cache.set(5, 20);
2661 assert_eq!(cache.get(0), 3);
2663 assert_eq!(cache.get(5), 3);
2664 }
2665
2666 #[test]
2667 fn height_cache_clear_resets_base() {
2668 let mut cache = HeightCache::new(1, 100);
2669 cache.set(50, 10);
2670 cache.clear();
2671 assert_eq!(cache.base_offset, 0);
2672 assert_eq!(cache.get(50), 1); }
2674
2675 #[test]
2676 fn height_cache_eviction_trims_oldest() {
2677 let mut cache = HeightCache::new(1, 4);
2678 for i in 0..6 {
2680 cache.set(i, (i + 10) as u16);
2681 }
2682 assert!(cache.cache.len() <= cache.capacity);
2684 assert_eq!(cache.get(5), 15);
2686 assert_eq!(cache.get(0), 1);
2688 }
2689
2690 #[test]
2693 fn fenwick_default_is_empty() {
2694 let tracker = VariableHeightsFenwick::default();
2695 assert!(tracker.is_empty());
2696 assert_eq!(tracker.len(), 0);
2697 assert_eq!(tracker.total_height(), 0);
2698 assert_eq!(tracker.default_height(), 1);
2699 }
2700
2701 #[test]
2702 fn fenwick_get_beyond_len_returns_default() {
2703 let tracker = VariableHeightsFenwick::new(3, 5);
2704 assert_eq!(tracker.get(5), 3); assert_eq!(tracker.get(100), 3);
2706 }
2707
2708 #[test]
2709 fn fenwick_set_beyond_len_resizes() {
2710 let mut tracker = VariableHeightsFenwick::new(2, 3);
2711 assert_eq!(tracker.len(), 3);
2712 tracker.set(10, 7);
2713 assert!(tracker.len() > 10);
2714 assert_eq!(tracker.get(10), 7);
2715 }
2716
2717 #[test]
2718 fn fenwick_offset_of_item_zero_always_zero() {
2719 let tracker = VariableHeightsFenwick::new(5, 10);
2720 assert_eq!(tracker.offset_of_item(0), 0);
2721
2722 let empty = VariableHeightsFenwick::new(5, 0);
2723 assert_eq!(empty.offset_of_item(0), 0);
2724 }
2725
2726 #[test]
2727 fn fenwick_find_item_at_offset_empty() {
2728 let tracker = VariableHeightsFenwick::new(1, 0);
2729 assert_eq!(tracker.find_item_at_offset(0), 0);
2730 assert_eq!(tracker.find_item_at_offset(100), 0);
2731 }
2732
2733 #[test]
2734 fn fenwick_visible_count_zero_viewport() {
2735 let tracker = VariableHeightsFenwick::new(2, 10);
2736 assert_eq!(tracker.visible_count(0, 0), 0);
2737 }
2738
2739 #[test]
2740 fn fenwick_visible_count_start_beyond_len() {
2741 let tracker = VariableHeightsFenwick::new(2, 5);
2742 let count = tracker.visible_count(100, 10);
2744 assert_eq!(count, 0);
2746 }
2747
2748 #[test]
2749 fn fenwick_clear_then_operations() {
2750 let mut tracker = VariableHeightsFenwick::new(3, 5);
2751 assert_eq!(tracker.total_height(), 15);
2752 tracker.clear();
2753 assert_eq!(tracker.len(), 0);
2754 assert_eq!(tracker.total_height(), 0);
2755 assert_eq!(tracker.find_item_at_offset(0), 0);
2756 }
2757
2758 #[test]
2759 fn fenwick_rebuild_replaces_data() {
2760 let mut tracker = VariableHeightsFenwick::new(1, 10);
2761 assert_eq!(tracker.total_height(), 10);
2762 tracker.rebuild(&[5, 3, 2]);
2763 assert_eq!(tracker.len(), 3);
2764 assert_eq!(tracker.total_height(), 10);
2765 assert_eq!(tracker.get(0), 5);
2766 assert_eq!(tracker.get(1), 3);
2767 assert_eq!(tracker.get(2), 2);
2768 }
2769
2770 #[test]
2771 fn fenwick_resize_same_size_is_noop() {
2772 let mut tracker = VariableHeightsFenwick::new(2, 5);
2773 tracker.set(2, 10);
2774 tracker.resize(5);
2775 assert_eq!(tracker.get(2), 10);
2777 assert_eq!(tracker.len(), 5);
2778 }
2779
2780 #[test]
2783 fn list_state_default_matches_new() {
2784 let d = VirtualizedListState::default();
2785 let n = VirtualizedListState::new();
2786 assert_eq!(d.selected, n.selected);
2787 assert_eq!(d.scroll_offset(), n.scroll_offset());
2788 assert_eq!(d.visible_count(), n.visible_count());
2789 assert_eq!(d.follow_mode(), n.follow_mode());
2790 }
2791
2792 #[test]
2793 fn list_state_select_next_on_empty() {
2794 let mut state = VirtualizedListState::new();
2795 state.select_next(0);
2796 assert_eq!(state.selected, None);
2797 }
2798
2799 #[test]
2800 fn list_state_select_previous_on_empty() {
2801 let mut state = VirtualizedListState::new();
2802 state.select_previous(0);
2803 assert_eq!(state.selected, None);
2804 }
2805
2806 #[test]
2807 fn list_state_select_previous_from_none() {
2808 let mut state = VirtualizedListState::new();
2809 state.select_previous(10);
2810 assert_eq!(state.selected, Some(0));
2811 }
2812
2813 #[test]
2814 fn list_state_select_next_from_none() {
2815 let mut state = VirtualizedListState::new();
2816 state.select_next(10);
2817 assert_eq!(state.selected, Some(0));
2818 }
2819
2820 #[test]
2821 fn list_state_scroll_zero_items() {
2822 let mut state = VirtualizedListState::new();
2823 state.scroll(10, 0);
2824 assert_eq!(state.scroll_offset(), 0);
2825 }
2826
2827 #[test]
2828 fn list_state_scroll_to_clamps() {
2829 let mut state = VirtualizedListState::new();
2830 state.scroll_to(100, 10);
2831 assert_eq!(state.scroll_offset(), 9);
2832 }
2833
2834 #[test]
2835 fn list_state_scroll_to_bottom_zero_items() {
2836 let mut state = VirtualizedListState::new();
2837 state.scroll_to_bottom(0);
2838 assert_eq!(state.scroll_offset(), 0);
2839 }
2840
2841 #[test]
2842 fn list_state_is_at_bottom_zero_items() {
2843 let state = VirtualizedListState::new();
2844 assert!(state.is_at_bottom(0));
2845 }
2846
2847 #[test]
2848 fn list_state_page_up_visible_count_zero() {
2849 let mut state = VirtualizedListState::new();
2850 state.scroll_offset = 5;
2851 state.page_up(20);
2852 assert_eq!(state.scroll_offset(), 5);
2854 }
2855
2856 #[test]
2857 fn list_state_page_down_visible_count_zero() {
2858 let mut state = VirtualizedListState::new();
2859 state.page_down(20);
2860 assert_eq!(state.scroll_offset(), 0);
2862 }
2863
2864 #[test]
2865 fn list_state_set_follow_false_no_scroll() {
2866 let mut state = VirtualizedListState::new();
2867 state.scroll_offset = 5;
2868 state.set_follow(false, 20);
2869 assert_eq!(state.scroll_offset(), 5); assert!(!state.follow_mode());
2871 }
2872
2873 #[test]
2874 fn list_state_persistence_id() {
2875 let state = VirtualizedListState::new().with_persistence_id("my-list");
2876 assert_eq!(state.persistence_id(), Some("my-list"));
2877 }
2878
2879 #[test]
2880 fn list_state_persistence_id_none() {
2881 let state = VirtualizedListState::new();
2882 assert_eq!(state.persistence_id(), None);
2883 }
2884
2885 #[test]
2886 fn list_state_momentum_tick_zero_items() {
2887 let mut state = VirtualizedListState::new();
2888 state.fling(50.0);
2889 state.tick(Duration::from_millis(100), 0);
2890 assert_eq!(state.scroll_offset(), 0);
2892 }
2893
2894 #[test]
2897 fn persist_state_default() {
2898 let ps = VirtualizedListPersistState::default();
2899 assert_eq!(ps.selected, None);
2900 assert_eq!(ps.scroll_offset, 0);
2901 assert!(!ps.follow_mode);
2902 }
2903
2904 #[test]
2905 fn persist_state_eq() {
2906 let a = VirtualizedListPersistState {
2907 selected: Some(5),
2908 scroll_offset: 10,
2909 follow_mode: true,
2910 };
2911 let b = a.clone();
2912 assert_eq!(a, b);
2913 }
2914
2915 #[test]
2918 fn stateful_state_key_with_persistence_id() {
2919 use crate::stateful::Stateful;
2920 let state = VirtualizedListState::new().with_persistence_id("logs");
2921 let key = state.state_key();
2922 assert_eq!(key.widget_type, "VirtualizedList");
2923 assert_eq!(key.instance_id, "logs");
2924 }
2925
2926 #[test]
2927 fn stateful_state_key_default_instance() {
2928 use crate::stateful::Stateful;
2929 let state = VirtualizedListState::new();
2930 let key = state.state_key();
2931 assert_eq!(key.instance_id, "default");
2932 }
2933
2934 #[test]
2935 fn stateful_save_restore_roundtrip() {
2936 use crate::stateful::Stateful;
2937 let mut state = VirtualizedListState::new();
2938 state.selected = Some(7);
2939 state.scroll_offset = 15;
2940 state.follow_mode = true;
2941 state.scroll_velocity = 42.0; let saved = state.save_state();
2944 assert_eq!(saved.selected, Some(7));
2945 assert_eq!(saved.scroll_offset, 15);
2946 assert!(saved.follow_mode);
2947
2948 let mut restored = VirtualizedListState::new();
2949 restored.scroll_velocity = 99.0;
2950 restored.restore_state(saved);
2951 assert_eq!(restored.selected, Some(7));
2952 assert_eq!(restored.scroll_offset, 15);
2953 assert!(restored.follow_mode);
2954 assert_eq!(restored.scroll_velocity, 0.0);
2956 }
2957
2958 #[test]
2961 fn virtualized_list_builder() {
2962 let items: Vec<String> = vec!["a".into()];
2963 let list = VirtualizedList::new(&items)
2964 .style(Style::default())
2965 .highlight_style(Style::default())
2966 .show_scrollbar(false)
2967 .fixed_height(3);
2968 assert_eq!(list.fixed_height, 3);
2969 assert!(!list.show_scrollbar);
2970 }
2971
2972 #[test]
2975 fn virtualized_storage_debug() {
2976 let storage: VirtualizedStorage<i32> = VirtualizedStorage::Owned(VecDeque::new());
2977 let dbg = format!("{:?}", storage);
2978 assert!(dbg.contains("Owned"));
2979
2980 let ext: VirtualizedStorage<i32> = VirtualizedStorage::External {
2981 len: 100,
2982 cache_capacity: 10,
2983 };
2984 let dbg = format!("{:?}", ext);
2985 assert!(dbg.contains("External"));
2986 }
2987
2988 #[test]
2989 fn virtualized_clone() {
2990 let mut virt: Virtualized<i32> = Virtualized::new(100);
2991 virt.push(1);
2992 virt.push(2);
2993 let cloned = virt.clone();
2994 assert_eq!(cloned.len(), 2);
2995 assert_eq!(cloned.get(0), Some(&1));
2996 }
2997}