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.div_ceil(*h) as usize,
228 ItemHeight::Fixed(_) => viewport_height as usize,
229 ItemHeight::Variable(cache) => {
230 let mut count = 0;
232 let mut total_height = 0u16;
233 let start = self.scroll_offset;
234 while start + count < self.len() {
235 let next = cache.get(start + count);
236 let proposed = total_height.saturating_add(next);
237
238 total_height = proposed;
240 count += 1;
241
242 if total_height >= viewport_height {
244 break;
245 }
246 }
247 count
248 }
249 ItemHeight::VariableFenwick(tracker) => {
250 tracker.visible_count(self.scroll_offset, viewport_height)
252 }
253 };
254
255 let start = self.scroll_offset;
256 let end = (start + items_visible).min(self.len());
257 self.visible_count.set(items_visible);
258 start..end
259 }
260
261 #[must_use]
263 pub fn render_range(&self, viewport_height: u16) -> Range<usize> {
264 let visible = self.visible_range(viewport_height);
265 let start = visible.start.saturating_sub(self.overscan);
266 let end = visible.end.saturating_add(self.overscan).min(self.len());
267 start..end
268 }
269
270 pub fn scroll(&mut self, delta: i32) {
272 if self.is_empty() {
273 return;
274 }
275 let visible_count = self.visible_count.get();
276 let max_offset = if visible_count > 0 {
277 self.len().saturating_sub(visible_count)
278 } else {
279 self.len().saturating_sub(1)
280 };
281 let new_offset = (self.scroll_offset as i64 + delta as i64)
282 .max(0)
283 .min(max_offset as i64);
284 self.scroll_offset = new_offset as usize;
285
286 if delta != 0 {
288 self.follow_mode = false;
289 }
290 }
291
292 pub fn scroll_to(&mut self, idx: usize) {
294 self.scroll_offset = idx.min(self.len().saturating_sub(1));
295 self.follow_mode = false;
296 }
297
298 pub fn scroll_to_bottom(&mut self) {
300 let visible_count = self.visible_count.get();
301 if self.len() > visible_count && visible_count > 0 {
302 self.scroll_offset = self.len().saturating_sub(visible_count);
303 } else {
304 self.scroll_offset = 0;
305 }
306 }
307
308 pub fn scroll_to_top(&mut self) {
310 self.scroll_offset = 0;
311 self.follow_mode = false;
312 }
313
314 pub fn scroll_to_start(&mut self) {
316 self.scroll_to_top();
317 }
318
319 pub fn scroll_to_end(&mut self) {
321 self.scroll_to_bottom();
322 self.follow_mode = true;
323 }
324
325 pub fn page_up(&mut self) {
327 let visible_count = self.visible_count.get();
328 if visible_count > 0 {
329 let step = if visible_count > 1 {
330 visible_count - 1
331 } else {
332 1
333 };
334 let delta = i32::try_from(step).unwrap_or(i32::MAX);
335 self.scroll(-delta);
336 }
337 }
338
339 pub fn page_down(&mut self) {
341 let visible_count = self.visible_count.get();
342 if visible_count > 0 {
343 let step = if visible_count > 1 {
344 visible_count - 1
345 } else {
346 1
347 };
348 let delta = i32::try_from(step).unwrap_or(i32::MAX);
349 self.scroll(delta);
350 }
351 }
352
353 pub fn set_follow(&mut self, follow: bool) {
355 self.follow_mode = follow;
356 if follow {
357 self.scroll_to_bottom();
358 }
359 }
360
361 #[must_use]
363 pub fn is_at_bottom(&self) -> bool {
364 let visible_count = self.visible_count.get();
365 if self.len() <= visible_count {
366 true
367 } else {
368 self.scroll_offset >= self.len().saturating_sub(visible_count)
369 }
370 }
371
372 pub fn fling(&mut self, velocity: f32) {
374 self.scroll_velocity = velocity;
375 }
376
377 pub fn tick(&mut self, dt: Duration) {
379 if self.scroll_velocity.abs() > 0.1 {
380 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
381 if delta != 0 {
382 self.scroll(delta);
383 }
384 self.scroll_velocity *= 0.95;
386 } else {
387 self.scroll_velocity = 0.0;
388 }
389 }
390
391 pub fn set_visible_count(&mut self, count: usize) {
393 self.visible_count.set(count);
394 }
395}
396
397impl<T> Virtualized<T> {
398 pub fn push(&mut self, item: T) {
400 if let VirtualizedStorage::Owned(items) = &mut self.storage {
401 items.push_back(item);
402 if self.follow_mode {
403 self.scroll_to_bottom();
404 }
405 }
406 }
407
408 #[must_use = "use the returned item (if any)"]
410 pub fn get(&self, idx: usize) -> Option<&T> {
411 if let VirtualizedStorage::Owned(items) = &self.storage {
412 items.get(idx)
413 } else {
414 None
415 }
416 }
417
418 #[must_use = "use the returned item (if any)"]
420 pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> {
421 if let VirtualizedStorage::Owned(items) = &mut self.storage {
422 items.get_mut(idx)
423 } else {
424 None
425 }
426 }
427
428 pub fn clear(&mut self) {
430 if let VirtualizedStorage::Owned(items) = &mut self.storage {
431 items.clear();
432 }
433 self.scroll_offset = 0;
434 }
435
436 pub fn trim_front(&mut self, max: usize) -> usize {
440 if let VirtualizedStorage::Owned(items) = &mut self.storage
441 && items.len() > max
442 {
443 let to_remove = items.len() - max;
444 items.drain(..to_remove);
445 self.scroll_offset = self.scroll_offset.saturating_sub(to_remove);
447 return to_remove;
448 }
449 0
450 }
451
452 pub fn iter(&self) -> Box<dyn Iterator<Item = &T> + '_> {
455 match &self.storage {
456 VirtualizedStorage::Owned(items) => Box::new(items.iter()),
457 VirtualizedStorage::External { .. } => Box::new(std::iter::empty()),
458 }
459 }
460
461 pub fn set_external_len(&mut self, len: usize) {
463 if let VirtualizedStorage::External { len: l, .. } = &mut self.storage {
464 *l = len;
465 if self.follow_mode {
466 self.scroll_to_bottom();
467 }
468 }
469 }
470}
471
472impl Default for HeightCache {
473 fn default() -> Self {
474 Self::new(1, 1000)
475 }
476}
477
478impl HeightCache {
479 #[must_use]
481 pub fn new(default_height: u16, capacity: usize) -> Self {
482 Self {
483 cache: Vec::new(),
484 base_offset: 0,
485 default_height,
486 capacity,
487 }
488 }
489
490 #[must_use]
492 pub fn get(&self, idx: usize) -> u16 {
493 if idx < self.base_offset {
494 return self.default_height;
495 }
496 let local = idx - self.base_offset;
497 self.cache
498 .get(local)
499 .and_then(|h| *h)
500 .unwrap_or(self.default_height)
501 }
502
503 pub fn set(&mut self, idx: usize, height: u16) {
505 if self.capacity == 0 {
506 return;
507 }
508 if idx < self.base_offset {
509 return;
511 }
512 let mut local = idx - self.base_offset;
513 if local >= self.capacity {
514 self.base_offset = idx.saturating_add(1).saturating_sub(self.capacity);
516 self.cache.clear();
517 local = idx - self.base_offset;
518 }
519 if local >= self.cache.len() {
520 self.cache.resize(local + 1, None);
521 }
522 self.cache[local] = Some(height);
523
524 if self.cache.len() > self.capacity {
526 let to_remove = self.cache.len() - self.capacity;
527 self.cache.drain(0..to_remove);
528 self.base_offset += to_remove;
529 }
530 }
531
532 pub fn clear(&mut self) {
534 self.cache.clear();
535 self.base_offset = 0;
536 }
537}
538
539use crate::fenwick::FenwickTree;
544
545#[derive(Debug, Clone)]
565pub struct VariableHeightsFenwick {
566 tree: FenwickTree,
568 default_height: u16,
570 len: usize,
572}
573
574impl Default for VariableHeightsFenwick {
575 fn default() -> Self {
576 Self::new(1, 0)
577 }
578}
579
580impl VariableHeightsFenwick {
581 #[must_use]
583 pub fn new(default_height: u16, capacity: usize) -> Self {
584 let tree = if capacity > 0 {
585 let heights: Vec<u32> = vec![u32::from(default_height); capacity];
587 FenwickTree::from_values(&heights)
588 } else {
589 FenwickTree::new(0)
590 };
591 Self {
592 tree,
593 default_height,
594 len: capacity,
595 }
596 }
597
598 #[must_use]
600 pub fn from_heights(heights: &[u16], default_height: u16) -> Self {
601 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
602 Self {
603 tree: FenwickTree::from_values(&heights_u32),
604 default_height,
605 len: heights.len(),
606 }
607 }
608
609 #[must_use]
611 pub fn len(&self) -> usize {
612 self.len
613 }
614
615 #[must_use]
617 pub fn is_empty(&self) -> bool {
618 self.len == 0
619 }
620
621 #[must_use]
623 pub fn default_height(&self) -> u16 {
624 self.default_height
625 }
626
627 #[must_use]
629 pub fn get(&self, idx: usize) -> u16 {
630 if idx >= self.len {
631 return self.default_height;
632 }
633 self.tree.get(idx).min(u32::from(u16::MAX)) as u16
635 }
636
637 pub fn set(&mut self, idx: usize, height: u16) {
639 if idx >= self.len {
640 self.resize(idx + 1);
642 }
643 self.tree.set(idx, u32::from(height));
644 }
645
646 #[must_use]
650 pub fn offset_of_item(&self, idx: usize) -> u32 {
651 if idx == 0 || self.len == 0 {
652 return 0;
653 }
654 let clamped = idx.min(self.len);
655 if clamped > 0 {
656 self.tree.prefix(clamped - 1)
657 } else {
658 0
659 }
660 }
661
662 #[must_use]
669 pub fn find_item_at_offset(&self, offset: u32) -> usize {
670 if self.len == 0 {
671 return 0;
672 }
673 if offset == 0 {
674 return 0;
675 }
676 match self.tree.find_prefix(offset) {
684 Some(i) => {
685 (i + 1).min(self.len)
689 }
690 None => {
691 0
693 }
694 }
695 }
696
697 #[must_use]
705 pub fn visible_count(&self, start_idx: usize, viewport_height: u16) -> usize {
706 if self.len == 0 || viewport_height == 0 {
707 return 0;
708 }
709 let start = start_idx.min(self.len);
710 let start_offset = self.offset_of_item(start);
711 let end_offset = start_offset.saturating_add(u32::from(viewport_height));
712
713 let end_idx = self.find_item_at_offset(end_offset);
715
716 if end_idx > start {
718 if end_idx >= self.len {
721 return self.len.saturating_sub(start);
722 }
723 let end_item_start = self.offset_of_item(end_idx);
725 if end_offset > end_item_start {
726 end_idx - start + 1
727 } else {
728 end_idx - start
729 }
730 } else {
731 if viewport_height > 0 && start < self.len {
733 1
734 } else {
735 0
736 }
737 }
738 }
739
740 #[must_use]
742 pub fn total_height(&self) -> u32 {
743 self.tree.total()
744 }
745
746 pub fn resize(&mut self, new_len: usize) {
750 if new_len == self.len {
751 return;
752 }
753 self.tree.resize(new_len);
754 if new_len > self.len {
756 for i in self.len..new_len {
757 self.tree.set(i, u32::from(self.default_height));
758 }
759 }
760 self.len = new_len;
761 }
762
763 pub fn clear(&mut self) {
765 self.tree = FenwickTree::new(0);
766 self.len = 0;
767 }
768
769 pub fn rebuild(&mut self, heights: &[u16]) {
771 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
772 self.tree = FenwickTree::from_values(&heights_u32);
773 self.len = heights.len();
774 }
775}
776
777pub trait RenderItem {
785 fn render(&self, area: Rect, frame: &mut Frame, selected: bool);
787
788 fn height(&self) -> u16 {
790 1
791 }
792}
793
794#[derive(Debug, Clone)]
796pub struct VirtualizedListState {
797 pub selected: Option<usize>,
799 scroll_offset: usize,
801 visible_count: usize,
803 overscan: usize,
805 follow_mode: bool,
807 scroll_velocity: f32,
809 scrollbar_drag_anchor: Option<usize>,
811 persistence_id: Option<String>,
813}
814
815impl Default for VirtualizedListState {
816 fn default() -> Self {
817 Self::new()
818 }
819}
820
821impl VirtualizedListState {
822 #[must_use]
824 pub fn new() -> Self {
825 Self {
826 selected: None,
827 scroll_offset: 0,
828 visible_count: 0,
829 overscan: 2,
830 follow_mode: false,
831 scroll_velocity: 0.0,
832 scrollbar_drag_anchor: None,
833 persistence_id: None,
834 }
835 }
836
837 #[must_use]
839 pub fn with_overscan(mut self, overscan: usize) -> Self {
840 self.overscan = overscan;
841 self
842 }
843
844 #[must_use]
846 pub fn with_follow(mut self, follow: bool) -> Self {
847 self.follow_mode = follow;
848 self
849 }
850
851 #[must_use]
853 pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
854 self.persistence_id = Some(id.into());
855 self
856 }
857
858 #[must_use = "use the persistence id (if any)"]
860 pub fn persistence_id(&self) -> Option<&str> {
861 self.persistence_id.as_deref()
862 }
863
864 #[must_use]
866 pub fn scroll_offset(&self) -> usize {
867 self.scroll_offset
868 }
869
870 #[must_use]
872 pub fn visible_count(&self) -> usize {
873 self.visible_count
874 }
875
876 pub fn scroll(&mut self, delta: i32, total_items: usize) {
878 if total_items == 0 {
879 return;
880 }
881 let max_offset = if self.visible_count > 0 {
882 total_items.saturating_sub(self.visible_count)
883 } else {
884 total_items.saturating_sub(1)
885 };
886 let new_offset = (self.scroll_offset as i64 + delta as i64)
887 .max(0)
888 .min(max_offset as i64);
889 self.scroll_offset = new_offset as usize;
890
891 if delta != 0 {
892 self.follow_mode = false;
893 }
894 }
895
896 pub fn scroll_to(&mut self, idx: usize, total_items: usize) {
898 self.scroll_offset = idx.min(total_items.saturating_sub(1));
899 self.follow_mode = false;
900 }
901
902 pub fn scroll_to_top(&mut self) {
904 self.scroll_offset = 0;
905 self.follow_mode = false;
906 }
907
908 pub fn scroll_to_bottom(&mut self, total_items: usize) {
910 if total_items > self.visible_count && self.visible_count > 0 {
911 self.scroll_offset = total_items - self.visible_count;
912 } else {
913 self.scroll_offset = 0;
914 }
915 }
916
917 pub fn page_up(&mut self, total_items: usize) {
919 if self.visible_count > 0 {
920 let step = if self.visible_count > 1 {
921 self.visible_count - 1
922 } else {
923 1
924 };
925 let delta = i32::try_from(step).unwrap_or(i32::MAX);
926 self.scroll(-delta, total_items);
927 }
928 }
929
930 pub fn page_down(&mut self, total_items: usize) {
932 if self.visible_count > 0 {
933 let step = if self.visible_count > 1 {
934 self.visible_count - 1
935 } else {
936 1
937 };
938 let delta = i32::try_from(step).unwrap_or(i32::MAX);
939 self.scroll(delta, total_items);
940 }
941 }
942
943 pub fn select(&mut self, index: Option<usize>) {
945 self.selected = index;
946 }
947
948 pub fn select_previous(&mut self, total_items: usize) {
950 if total_items == 0 {
951 self.selected = None;
952 return;
953 }
954 self.selected = Some(match self.selected {
955 Some(i) if i > 0 => i - 1,
956 Some(_) => 0,
957 None => 0,
958 });
959 }
960
961 pub fn select_next(&mut self, total_items: usize) {
963 if total_items == 0 {
964 self.selected = None;
965 return;
966 }
967 self.selected = Some(match self.selected {
968 Some(i) if i < total_items - 1 => i + 1,
969 Some(i) => i,
970 None => 0,
971 });
972 }
973
974 #[must_use]
976 pub fn is_at_bottom(&self, total_items: usize) -> bool {
977 if total_items <= self.visible_count {
978 true
979 } else {
980 self.scroll_offset >= total_items - self.visible_count
981 }
982 }
983
984 pub fn set_follow(&mut self, follow: bool, total_items: usize) {
986 self.follow_mode = follow;
987 if follow {
988 self.scroll_to_bottom(total_items);
989 }
990 }
991
992 #[must_use]
994 pub fn follow_mode(&self) -> bool {
995 self.follow_mode
996 }
997
998 pub fn fling(&mut self, velocity: f32) {
1000 self.scroll_velocity = velocity;
1001 }
1002
1003 pub fn tick(&mut self, dt: Duration, total_items: usize) {
1005 if self.scroll_velocity.abs() > 0.1 {
1006 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
1007 if delta != 0 {
1008 self.scroll(delta, total_items);
1009 }
1010 self.scroll_velocity *= 0.95;
1011 } else {
1012 self.scroll_velocity = 0.0;
1013 }
1014 }
1015
1016 pub fn handle_mouse(
1021 &mut self,
1022 event: &ftui_core::event::MouseEvent,
1023 hit: Option<(
1024 ftui_render::frame::HitId,
1025 ftui_render::frame::HitRegion,
1026 u64,
1027 )>,
1028 scrollbar_hit_id: ftui_render::frame::HitId,
1029 total_items: usize,
1030 viewport_height: u16,
1031 fixed_item_height: u16,
1032 ) -> crate::mouse::MouseResult {
1033 let items_per_viewport = viewport_height.div_ceil(fixed_item_height.max(1)) as usize;
1035 let mut scrollbar_state =
1036 ScrollbarState::new(total_items, self.scroll_offset, items_per_viewport);
1037
1038 scrollbar_state.drag_anchor = self.scrollbar_drag_anchor;
1040
1041 let result = scrollbar_state.handle_mouse(event, hit, scrollbar_hit_id);
1042
1043 self.scroll_offset = scrollbar_state.position;
1045 self.scrollbar_drag_anchor = scrollbar_state.drag_anchor;
1046
1047 if result == crate::mouse::MouseResult::Scrolled {
1048 self.follow_mode = false;
1049 }
1050
1051 result
1052 }
1053}
1054
1055#[derive(Clone, Debug, Default, PartialEq)]
1064#[cfg_attr(
1065 feature = "state-persistence",
1066 derive(serde::Serialize, serde::Deserialize)
1067)]
1068pub struct VirtualizedListPersistState {
1069 pub selected: Option<usize>,
1071 pub scroll_offset: usize,
1073 pub follow_mode: bool,
1075}
1076
1077impl crate::stateful::Stateful for VirtualizedListState {
1078 type State = VirtualizedListPersistState;
1079
1080 fn state_key(&self) -> crate::stateful::StateKey {
1081 crate::stateful::StateKey::new(
1082 "VirtualizedList",
1083 self.persistence_id.as_deref().unwrap_or("default"),
1084 )
1085 }
1086
1087 fn save_state(&self) -> VirtualizedListPersistState {
1088 VirtualizedListPersistState {
1089 selected: self.selected,
1090 scroll_offset: self.scroll_offset,
1091 follow_mode: self.follow_mode,
1092 }
1093 }
1094
1095 fn restore_state(&mut self, state: VirtualizedListPersistState) {
1096 self.selected = state.selected;
1097 self.scroll_offset = state.scroll_offset;
1098 self.follow_mode = state.follow_mode;
1099 self.scroll_velocity = 0.0;
1101 self.scrollbar_drag_anchor = None;
1102 }
1103}
1104
1105#[derive(Debug)]
1111pub struct VirtualizedList<'a, T> {
1112 items: &'a [T],
1114 style: Style,
1116 highlight_style: Style,
1118 show_scrollbar: bool,
1120 fixed_height: u16,
1122 hit_id: Option<ftui_render::frame::HitId>,
1124}
1125
1126impl<'a, T> VirtualizedList<'a, T> {
1127 #[must_use]
1129 pub fn new(items: &'a [T]) -> Self {
1130 Self {
1131 items,
1132 style: Style::default(),
1133 highlight_style: Style::default(),
1134 show_scrollbar: true,
1135 fixed_height: 1,
1136 hit_id: None,
1137 }
1138 }
1139
1140 #[must_use]
1142 pub fn style(mut self, style: Style) -> Self {
1143 self.style = style;
1144 self
1145 }
1146
1147 #[must_use]
1149 pub fn highlight_style(mut self, style: Style) -> Self {
1150 self.highlight_style = style;
1151 self
1152 }
1153
1154 #[must_use]
1156 pub fn show_scrollbar(mut self, show: bool) -> Self {
1157 self.show_scrollbar = show;
1158 self
1159 }
1160
1161 #[must_use]
1163 pub fn fixed_height(mut self, height: u16) -> Self {
1164 self.fixed_height = height;
1165 self
1166 }
1167
1168 #[must_use]
1170 pub fn hit_id(mut self, id: ftui_render::frame::HitId) -> Self {
1171 self.hit_id = Some(id);
1172 self
1173 }
1174}
1175
1176impl<T: RenderItem> StatefulWidget for VirtualizedList<'_, T> {
1177 type State = VirtualizedListState;
1178
1179 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1180 #[cfg(feature = "tracing")]
1181 let _span = tracing::debug_span!(
1182 "widget_render",
1183 widget = "VirtualizedList",
1184 x = area.x,
1185 y = area.y,
1186 w = area.width,
1187 h = area.height,
1188 items = self.items.len()
1189 )
1190 .entered();
1191
1192 if area.is_empty() {
1193 return;
1194 }
1195
1196 set_style_area(&mut frame.buffer, area, self.style);
1198
1199 let total_items = self.items.len();
1200 if total_items == 0 {
1201 return;
1202 }
1203
1204 let fixed_h = self.fixed_height.max(1);
1206 let items_per_viewport = area.height.div_ceil(fixed_h) as usize;
1208 let needs_scrollbar = self.show_scrollbar && total_items > items_per_viewport;
1209 let content_width = if needs_scrollbar {
1210 area.width.saturating_sub(1)
1211 } else {
1212 area.width
1213 };
1214
1215 if let Some(selected) = state.selected
1217 && selected >= total_items
1218 {
1219 state.selected = if total_items > 0 {
1221 Some(total_items - 1)
1222 } else {
1223 None
1224 };
1225 }
1226
1227 if let Some(selected) = state.selected {
1229 if selected >= state.scroll_offset + items_per_viewport {
1230 state.scroll_offset = selected.saturating_sub(items_per_viewport.saturating_sub(1));
1231 } else if selected < state.scroll_offset {
1232 state.scroll_offset = selected;
1233 }
1234 }
1235
1236 let max_offset = total_items.saturating_sub(items_per_viewport);
1238 state.scroll_offset = state.scroll_offset.min(max_offset);
1239
1240 state.visible_count = items_per_viewport.min(total_items);
1242
1243 let render_start = state.scroll_offset.saturating_sub(state.overscan);
1245 let render_end = state
1246 .scroll_offset
1247 .saturating_add(items_per_viewport)
1248 .saturating_add(state.overscan)
1249 .min(total_items);
1250
1251 for idx in render_start..render_end {
1253 let idx_i32 = i32::try_from(idx).unwrap_or(i32::MAX);
1256 let offset_i32 = i32::try_from(state.scroll_offset).unwrap_or(i32::MAX);
1257 let relative_idx = idx_i32.saturating_sub(offset_i32);
1258 let height_i32 = i32::from(self.fixed_height);
1259 let y_offset = relative_idx.saturating_mul(height_i32);
1260
1261 if y_offset.saturating_add(height_i32) <= 0 {
1263 continue;
1264 }
1265
1266 if y_offset >= i32::from(area.height) {
1268 break;
1269 }
1270
1271 if i32::from(area.y).saturating_add(y_offset) < 0 {
1275 continue;
1276 }
1277
1278 let y = i32::from(area.y)
1281 .saturating_add(y_offset)
1282 .clamp(0, i32::from(u16::MAX)) as u16;
1283 if y >= area.bottom() {
1284 break;
1285 }
1286
1287 let visible_height = self.fixed_height.min(area.bottom().saturating_sub(y));
1288 if visible_height == 0 {
1289 continue;
1290 }
1291
1292 let row_area = Rect::new(area.x, y, content_width, visible_height);
1293
1294 let is_selected = state.selected == Some(idx);
1295
1296 if is_selected {
1298 set_style_area(&mut frame.buffer, row_area, self.highlight_style);
1299 }
1300
1301 self.items[idx].render(row_area, frame, is_selected);
1303 }
1304
1305 if needs_scrollbar {
1307 let scrollbar_area = Rect::new(area.right().saturating_sub(1), area.y, 1, area.height);
1308
1309 let mut scrollbar_state =
1310 ScrollbarState::new(total_items, state.scroll_offset, items_per_viewport);
1311
1312 scrollbar_state.drag_anchor = state.scrollbar_drag_anchor;
1314
1315 let mut scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
1316 if let Some(id) = self.hit_id {
1317 scrollbar = scrollbar.hit_id(id);
1318 }
1319 scrollbar.render(scrollbar_area, frame, &mut scrollbar_state);
1320 }
1321 }
1322}
1323
1324impl RenderItem for String {
1329 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1330 if area.is_empty() {
1331 return;
1332 }
1333 let max_chars = area.width as usize;
1334 for (i, ch) in self.chars().take(max_chars).enumerate() {
1335 frame
1336 .buffer
1337 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1338 }
1339 }
1340}
1341
1342impl RenderItem for &str {
1343 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1344 if area.is_empty() {
1345 return;
1346 }
1347 let max_chars = area.width as usize;
1348 for (i, ch) in self.chars().take(max_chars).enumerate() {
1349 frame
1350 .buffer
1351 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1352 }
1353 }
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359
1360 #[test]
1361 fn test_new_virtualized() {
1362 let virt: Virtualized<String> = Virtualized::new(100);
1363 assert_eq!(virt.len(), 0);
1364 assert!(virt.is_empty());
1365 }
1366
1367 #[test]
1368 fn test_push_and_len() {
1369 let mut virt: Virtualized<i32> = Virtualized::new(100);
1370 virt.push(1);
1371 virt.push(2);
1372 virt.push(3);
1373 assert_eq!(virt.len(), 3);
1374 assert!(!virt.is_empty());
1375 }
1376
1377 #[test]
1378 fn test_visible_range_fixed_height() {
1379 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(2);
1380 for i in 0..20 {
1381 virt.push(i);
1382 }
1383 let range = virt.visible_range(20);
1385 assert_eq!(range, 0..10);
1386 }
1387
1388 #[test]
1389 fn test_visible_range_variable_height_clamps() {
1390 let mut cache = HeightCache::new(1, 16);
1391 cache.set(0, 3);
1392 cache.set(1, 3);
1393 cache.set(2, 3);
1394 let mut virt: Virtualized<i32> =
1395 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1396 for i in 0..3 {
1397 virt.push(i);
1398 }
1399 let range = virt.visible_range(5);
1400 assert_eq!(range, 0..1);
1401 }
1402
1403 #[test]
1404 fn test_visible_range_variable_height_exact_fit() {
1405 let mut cache = HeightCache::new(1, 16);
1406 cache.set(0, 2);
1407 cache.set(1, 3);
1408 let mut virt: Virtualized<i32> =
1409 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1410 for i in 0..2 {
1411 virt.push(i);
1412 }
1413 let range = virt.visible_range(5);
1414 assert_eq!(range, 0..2);
1415 }
1416
1417 #[test]
1418 fn test_visible_range_with_scroll() {
1419 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(1);
1420 for i in 0..50 {
1421 virt.push(i);
1422 }
1423 virt.scroll(10);
1424 let range = virt.visible_range(10);
1425 assert_eq!(range, 10..20);
1426 }
1427
1428 #[test]
1429 fn test_visible_range_variable_height_excludes_partial() {
1430 let mut cache = HeightCache::new(1, 16);
1431 cache.set(0, 6);
1432 cache.set(1, 6);
1433 let mut virt: Virtualized<i32> =
1434 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1435 virt.push(1);
1436 virt.push(2);
1437 virt.push(3);
1438
1439 let range = virt.visible_range(10);
1440 assert_eq!(range, 0..1);
1441 }
1442
1443 #[test]
1444 fn test_visible_range_variable_height_exact_fit_larger() {
1445 let mut cache = HeightCache::new(1, 16);
1446 cache.set(0, 4);
1447 cache.set(1, 6);
1448 let mut virt: Virtualized<i32> =
1449 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1450 virt.push(1);
1451 virt.push(2);
1452 virt.push(3);
1453
1454 let range = virt.visible_range(10);
1455 assert_eq!(range, 0..2);
1456 }
1457
1458 #[test]
1459 fn test_visible_range_variable_height_default_for_unmeasured() {
1460 let cache = HeightCache::new(2, 16);
1461 let mut virt: Virtualized<i32> =
1462 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1463 for i in 0..3 {
1464 virt.push(i);
1465 }
1466
1467 let range = virt.visible_range(5);
1469 assert_eq!(range, 0..2);
1470 }
1471
1472 #[test]
1473 fn test_render_range_with_overscan() {
1474 let mut virt: Virtualized<i32> =
1475 Virtualized::new(100).with_fixed_height(1).with_overscan(2);
1476 for i in 0..50 {
1477 virt.push(i);
1478 }
1479 virt.scroll(10);
1480 let range = virt.render_range(10);
1481 assert_eq!(range, 8..22);
1484 }
1485
1486 #[test]
1487 fn test_scroll_bounds() {
1488 let mut virt: Virtualized<i32> = Virtualized::new(100);
1489 for i in 0..10 {
1490 virt.push(i);
1491 }
1492
1493 virt.scroll(-100);
1495 assert_eq!(virt.scroll_offset(), 0);
1496
1497 virt.scroll(100);
1499 assert_eq!(virt.scroll_offset(), 9);
1500 }
1501
1502 #[test]
1503 fn test_scroll_to() {
1504 let mut virt: Virtualized<i32> = Virtualized::new(100);
1505 for i in 0..20 {
1506 virt.push(i);
1507 }
1508
1509 virt.scroll_to(15);
1510 assert_eq!(virt.scroll_offset(), 15);
1511
1512 virt.scroll_to(100);
1514 assert_eq!(virt.scroll_offset(), 19);
1515 }
1516
1517 #[test]
1518 fn test_follow_mode() {
1519 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
1520 virt.set_visible_count(5);
1521
1522 for i in 0..10 {
1523 virt.push(i);
1524 }
1525
1526 assert!(virt.is_at_bottom());
1528
1529 virt.scroll(-5);
1531 assert!(!virt.follow_mode());
1532 }
1533
1534 #[test]
1535 fn test_scroll_to_start_and_end() {
1536 let mut virt: Virtualized<i32> = Virtualized::new(100);
1537 virt.set_visible_count(5);
1538 for i in 0..20 {
1539 virt.push(i);
1540 }
1541
1542 virt.scroll_to(10);
1544 virt.set_follow(true);
1545 virt.scroll_to_start();
1546 assert_eq!(virt.scroll_offset(), 0);
1547 assert!(!virt.follow_mode());
1548
1549 virt.scroll_to_end();
1551 assert!(virt.is_at_bottom());
1552 assert!(virt.follow_mode());
1553 }
1554
1555 #[test]
1556 fn test_virtualized_page_navigation() {
1557 let mut virt: Virtualized<i32> = Virtualized::new(100);
1558 virt.set_visible_count(5);
1559 for i in 0..30 {
1560 virt.push(i);
1561 }
1562
1563 virt.scroll_to(15);
1564 virt.page_up();
1565 assert_eq!(virt.scroll_offset(), 10);
1566
1567 virt.page_down();
1568 assert_eq!(virt.scroll_offset(), 15);
1569
1570 virt.scroll_to(2);
1572 virt.page_up();
1573 assert_eq!(virt.scroll_offset(), 0);
1574 }
1575
1576 #[test]
1577 fn test_height_cache() {
1578 let mut cache = HeightCache::new(1, 100);
1579
1580 assert_eq!(cache.get(0), 1);
1582 assert_eq!(cache.get(50), 1);
1583
1584 cache.set(5, 3);
1586 assert_eq!(cache.get(5), 3);
1587
1588 assert_eq!(cache.get(4), 1);
1590 assert_eq!(cache.get(6), 1);
1591 }
1592
1593 #[test]
1594 fn test_height_cache_large_index_window() {
1595 let mut cache = HeightCache::new(1, 8);
1596 cache.set(10_000, 4);
1597 assert_eq!(cache.get(10_000), 4);
1598 assert_eq!(cache.get(0), 1);
1599 assert!(cache.cache.len() <= cache.capacity);
1600 }
1601
1602 #[test]
1603 fn test_clear() {
1604 let mut virt: Virtualized<i32> = Virtualized::new(100);
1605 for i in 0..10 {
1606 virt.push(i);
1607 }
1608 virt.scroll(5);
1609
1610 virt.clear();
1611 assert_eq!(virt.len(), 0);
1612 assert_eq!(virt.scroll_offset(), 0);
1613 }
1614
1615 #[test]
1616 fn test_get_item() {
1617 let mut virt: Virtualized<String> = Virtualized::new(100);
1618 virt.push("hello".to_string());
1619 virt.push("world".to_string());
1620
1621 assert_eq!(virt.get(0), Some(&"hello".to_string()));
1622 assert_eq!(virt.get(1), Some(&"world".to_string()));
1623 assert_eq!(virt.get(2), None);
1624 }
1625
1626 #[test]
1627 fn test_external_storage_len() {
1628 let mut virt: Virtualized<i32> = Virtualized::external(1000, 100);
1629 assert_eq!(virt.len(), 1000);
1630
1631 virt.set_external_len(2000);
1632 assert_eq!(virt.len(), 2000);
1633 }
1634
1635 #[test]
1636 fn test_momentum_scrolling() {
1637 let mut virt: Virtualized<i32> = Virtualized::new(100);
1638 for i in 0..50 {
1639 virt.push(i);
1640 }
1641
1642 virt.fling(10.0);
1643
1644 virt.tick(Duration::from_millis(100));
1646
1647 assert!(virt.scroll_offset() > 0);
1649 }
1650
1651 #[test]
1656 fn test_virtualized_list_state_new() {
1657 let state = VirtualizedListState::new();
1658 assert_eq!(state.selected, None);
1659 assert_eq!(state.scroll_offset(), 0);
1660 assert_eq!(state.visible_count(), 0);
1661 }
1662
1663 #[test]
1664 fn test_virtualized_list_state_select_next() {
1665 let mut state = VirtualizedListState::new();
1666
1667 state.select_next(10);
1668 assert_eq!(state.selected, Some(0));
1669
1670 state.select_next(10);
1671 assert_eq!(state.selected, Some(1));
1672
1673 state.selected = Some(9);
1675 state.select_next(10);
1676 assert_eq!(state.selected, Some(9));
1677 }
1678
1679 #[test]
1680 fn test_virtualized_list_state_select_previous() {
1681 let mut state = VirtualizedListState::new();
1682 state.selected = Some(5);
1683
1684 state.select_previous(10);
1685 assert_eq!(state.selected, Some(4));
1686
1687 state.selected = Some(0);
1688 state.select_previous(10);
1689 assert_eq!(state.selected, Some(0));
1690 }
1691
1692 #[test]
1693 fn test_virtualized_list_state_scroll() {
1694 let mut state = VirtualizedListState::new();
1695
1696 state.scroll(5, 20);
1697 assert_eq!(state.scroll_offset(), 5);
1698
1699 state.scroll(-3, 20);
1700 assert_eq!(state.scroll_offset(), 2);
1701
1702 state.scroll(-100, 20);
1704 assert_eq!(state.scroll_offset(), 0);
1705
1706 state.scroll(100, 20);
1708 assert_eq!(state.scroll_offset(), 19);
1709 }
1710
1711 #[test]
1712 fn test_virtualized_list_state_follow_mode() {
1713 let mut state = VirtualizedListState::new().with_follow(true);
1714 assert!(state.follow_mode());
1715
1716 state.scroll(5, 20);
1718 assert!(!state.follow_mode());
1719 }
1720
1721 #[test]
1722 fn test_render_item_string() {
1723 let s = String::from("hello");
1725 assert_eq!(s.height(), 1);
1726 }
1727
1728 #[test]
1729 fn test_page_up_down() {
1730 let mut virt: Virtualized<i32> = Virtualized::new(100);
1731 for i in 0..50 {
1732 virt.push(i);
1733 }
1734 virt.set_visible_count(10);
1735
1736 assert_eq!(virt.scroll_offset(), 0);
1738
1739 virt.page_down();
1741 assert_eq!(virt.scroll_offset(), 10);
1742
1743 virt.page_down();
1745 assert_eq!(virt.scroll_offset(), 20);
1746
1747 virt.page_up();
1749 assert_eq!(virt.scroll_offset(), 10);
1750
1751 virt.page_up();
1753 assert_eq!(virt.scroll_offset(), 0);
1754
1755 virt.page_up();
1757 assert_eq!(virt.scroll_offset(), 0);
1758 }
1759
1760 #[test]
1765 fn test_render_scales_with_visible_not_total() {
1766 use ftui_render::grapheme_pool::GraphemePool;
1767 use std::time::Instant;
1768
1769 let small_items: Vec<String> = (0..1_000).map(|i| format!("Line {}", i)).collect();
1771 let small_list = VirtualizedList::new(&small_items);
1772 let mut small_state = VirtualizedListState::new();
1773
1774 let area = Rect::new(0, 0, 80, 24);
1775 let mut pool = GraphemePool::new();
1776 let mut frame = Frame::new(80, 24, &mut pool);
1777
1778 small_list.render(area, &mut frame, &mut small_state);
1780
1781 let start = Instant::now();
1782 for _ in 0..100 {
1783 frame.buffer.clear();
1784 small_list.render(area, &mut frame, &mut small_state);
1785 }
1786 let small_time = start.elapsed();
1787
1788 let large_items: Vec<String> = (0..100_000).map(|i| format!("Line {}", i)).collect();
1790 let large_list = VirtualizedList::new(&large_items);
1791 let mut large_state = VirtualizedListState::new();
1792
1793 large_list.render(area, &mut frame, &mut large_state);
1795
1796 let start = Instant::now();
1797 for _ in 0..100 {
1798 frame.buffer.clear();
1799 large_list.render(area, &mut frame, &mut large_state);
1800 }
1801 let large_time = start.elapsed();
1802
1803 assert!(
1805 large_time < small_time * 3,
1806 "Render does not scale O(visible): 1K={:?}, 100K={:?}",
1807 small_time,
1808 large_time
1809 );
1810 }
1811
1812 #[test]
1813 fn test_scroll_is_constant_time() {
1814 use std::time::Instant;
1815
1816 let mut small: Virtualized<i32> = Virtualized::new(1_000);
1817 for i in 0..1_000 {
1818 small.push(i);
1819 }
1820 small.set_visible_count(24);
1821
1822 let mut large: Virtualized<i32> = Virtualized::new(100_000);
1823 for i in 0..100_000 {
1824 large.push(i);
1825 }
1826 large.set_visible_count(24);
1827
1828 let iterations = 10_000;
1829
1830 let start = Instant::now();
1831 for _ in 0..iterations {
1832 small.scroll(1);
1833 small.scroll(-1);
1834 }
1835 let small_time = start.elapsed();
1836
1837 let start = Instant::now();
1838 for _ in 0..iterations {
1839 large.scroll(1);
1840 large.scroll(-1);
1841 }
1842 let large_time = start.elapsed();
1843
1844 assert!(
1846 large_time < small_time * 3,
1847 "Scroll is not O(1): 1K={:?}, 100K={:?}",
1848 small_time,
1849 large_time
1850 );
1851 }
1852
1853 #[test]
1854 fn render_partially_offscreen_top_skips_item() {
1855 use ftui_render::grapheme_pool::GraphemePool;
1856
1857 struct IndexedItem(usize);
1859 impl RenderItem for IndexedItem {
1860 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1861 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1862 for y in area.y..area.bottom() {
1863 frame.buffer.set(area.x, y, Cell::from_char(ch));
1864 }
1865 }
1866 fn height(&self) -> u16 {
1867 2
1868 }
1869 }
1870
1871 let items = vec![
1874 IndexedItem(0),
1875 IndexedItem(1),
1876 IndexedItem(2),
1877 IndexedItem(3),
1878 ];
1879 let list = VirtualizedList::new(&items).fixed_height(2);
1880
1881 let mut state = VirtualizedListState::new().with_overscan(1);
1883 state.scroll_offset = 1; let mut pool = GraphemePool::new();
1886 let mut frame = Frame::new(10, 5, &mut pool);
1887
1888 list.render(Rect::new(0, 0, 10, 5), &mut frame, &mut state);
1890
1891 let cell = frame.buffer.get(0, 0).unwrap();
1899 assert_eq!(cell.content.as_char(), Some('1'));
1900 }
1901
1902 #[test]
1903 fn render_bottom_boundary_clips_partial_item() {
1904 use ftui_render::grapheme_pool::GraphemePool;
1905
1906 struct IndexedItem(u16);
1907 impl RenderItem for IndexedItem {
1908 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1909 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1910 for y in area.y..area.bottom() {
1911 frame.buffer.set(area.x, y, Cell::from_char(ch));
1912 }
1913 }
1914 fn height(&self) -> u16 {
1915 2
1916 }
1917 }
1918
1919 let items = vec![IndexedItem(0), IndexedItem(1), IndexedItem(2)];
1920 let list = VirtualizedList::new(&items)
1921 .fixed_height(2)
1922 .show_scrollbar(false);
1923 let mut state = VirtualizedListState::new();
1924
1925 let mut pool = GraphemePool::new();
1926 let mut frame = Frame::new(4, 4, &mut pool);
1927
1928 list.render(Rect::new(0, 0, 4, 3), &mut frame, &mut state);
1930
1931 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1932 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('0'));
1933 assert_eq!(frame.buffer.get(0, 2).unwrap().content.as_char(), Some('1'));
1934 assert_eq!(frame.buffer.get(0, 3).unwrap().content.as_char(), None);
1936 }
1937
1938 #[test]
1939 fn render_after_fling_advances_visible_rows() {
1940 use ftui_render::grapheme_pool::GraphemePool;
1941
1942 struct IndexedItem(u16);
1943 impl RenderItem for IndexedItem {
1944 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1945 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1946 for y in area.y..area.bottom() {
1947 frame.buffer.set(area.x, y, Cell::from_char(ch));
1948 }
1949 }
1950 }
1951
1952 let items: Vec<IndexedItem> = (0..10).map(IndexedItem).collect();
1953 let list = VirtualizedList::new(&items)
1954 .fixed_height(1)
1955 .show_scrollbar(false);
1956 let mut state = VirtualizedListState::new();
1957
1958 let mut pool = GraphemePool::new();
1959 let mut frame = Frame::new(4, 3, &mut pool);
1960 let area = Rect::new(0, 0, 4, 3);
1961
1962 list.render(area, &mut frame, &mut state);
1964 assert_eq!(state.scroll_offset(), 0);
1965 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1966
1967 state.fling(40.0);
1969 state.tick(Duration::from_millis(100), items.len());
1970 assert_eq!(state.scroll_offset(), 4);
1971
1972 frame.buffer.clear();
1973 list.render(area, &mut frame, &mut state);
1974 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('4'));
1975 }
1976
1977 #[test]
1978 fn test_memory_bounded_by_ring_capacity() {
1979 use crate::log_ring::LogRing;
1980
1981 let mut ring: LogRing<String> = LogRing::new(1_000);
1982
1983 for i in 0..100_000 {
1985 ring.push(format!("Line {}", i));
1986 }
1987
1988 assert_eq!(ring.len(), 1_000);
1990 assert_eq!(ring.total_count(), 100_000);
1991 assert_eq!(ring.first_index(), 99_000);
1992
1993 assert!(ring.get(99_999).is_some());
1995 assert!(ring.get(99_000).is_some());
1996 assert!(ring.get(0).is_none());
1998 assert!(ring.get(98_999).is_none());
1999 }
2000
2001 #[test]
2002 fn test_visible_range_constant_regardless_of_total() {
2003 let mut small: Virtualized<i32> = Virtualized::new(100);
2004 for i in 0..100 {
2005 small.push(i);
2006 }
2007 let small_range = small.visible_range(24);
2008
2009 let mut large: Virtualized<i32> = Virtualized::new(100_000);
2010 for i in 0..100_000 {
2011 large.push(i);
2012 }
2013 let large_range = large.visible_range(24);
2014
2015 assert_eq!(small_range.end - small_range.start, 24);
2017 assert_eq!(large_range.end - large_range.start, 24);
2018 }
2019
2020 #[test]
2021 fn test_virtualized_list_state_page_up_down() {
2022 let mut state = VirtualizedListState::new();
2023 state.visible_count = 10;
2024
2025 state.page_down(50);
2027 assert_eq!(state.scroll_offset(), 10);
2028
2029 state.page_down(50);
2031 assert_eq!(state.scroll_offset(), 20);
2032
2033 state.page_up(50);
2035 assert_eq!(state.scroll_offset(), 10);
2036
2037 state.page_up(50);
2039 assert_eq!(state.scroll_offset(), 0);
2040 }
2041
2042 #[test]
2047 fn test_variable_heights_fenwick_new() {
2048 let tracker = VariableHeightsFenwick::new(2, 10);
2049 assert_eq!(tracker.len(), 10);
2050 assert!(!tracker.is_empty());
2051 assert_eq!(tracker.default_height(), 2);
2052 }
2053
2054 #[test]
2055 fn test_variable_heights_fenwick_empty() {
2056 let tracker = VariableHeightsFenwick::new(1, 0);
2057 assert!(tracker.is_empty());
2058 assert_eq!(tracker.total_height(), 0);
2059 }
2060
2061 #[test]
2062 fn test_variable_heights_fenwick_from_heights() {
2063 let heights = vec![3, 2, 5, 1, 4];
2064 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2065
2066 assert_eq!(tracker.len(), 5);
2067 assert_eq!(tracker.get(0), 3);
2068 assert_eq!(tracker.get(1), 2);
2069 assert_eq!(tracker.get(2), 5);
2070 assert_eq!(tracker.get(3), 1);
2071 assert_eq!(tracker.get(4), 4);
2072 assert_eq!(tracker.total_height(), 15);
2073 }
2074
2075 #[test]
2076 fn test_variable_heights_fenwick_offset_of_item() {
2077 let heights = vec![3, 2, 5, 1, 4];
2079 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2080
2081 assert_eq!(tracker.offset_of_item(0), 0);
2082 assert_eq!(tracker.offset_of_item(1), 3);
2083 assert_eq!(tracker.offset_of_item(2), 5);
2084 assert_eq!(tracker.offset_of_item(3), 10);
2085 assert_eq!(tracker.offset_of_item(4), 11);
2086 assert_eq!(tracker.offset_of_item(5), 15); }
2088
2089 #[test]
2090 fn test_variable_heights_fenwick_find_item_at_offset() {
2091 let heights = vec![3, 2, 5, 1, 4];
2093 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2094
2095 assert_eq!(tracker.find_item_at_offset(0), 0);
2097 assert_eq!(tracker.find_item_at_offset(1), 0);
2099 assert_eq!(tracker.find_item_at_offset(3), 1);
2101 assert_eq!(tracker.find_item_at_offset(5), 2);
2103 assert_eq!(tracker.find_item_at_offset(10), 3);
2105 assert_eq!(tracker.find_item_at_offset(11), 4);
2107 assert_eq!(tracker.find_item_at_offset(15), 5);
2109 }
2110
2111 #[test]
2112 fn test_variable_heights_fenwick_visible_count() {
2113 let heights = vec![3, 2, 5, 1, 4];
2115 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2116
2117 assert_eq!(tracker.visible_count(0, 5), 2);
2119
2120 assert_eq!(tracker.visible_count(0, 4), 2);
2122
2123 assert_eq!(tracker.visible_count(0, 10), 3);
2125
2126 assert_eq!(tracker.visible_count(2, 6), 2);
2128 }
2129
2130 #[test]
2131 fn test_variable_heights_fenwick_visible_count_viewport_beyond_total_height() {
2132 let heights = vec![1, 1, 1];
2133 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2134
2135 assert_eq!(tracker.visible_count(0, 10), 3);
2137 assert_eq!(tracker.visible_count(1, 10), 2);
2138 assert_eq!(tracker.visible_count(2, 10), 1);
2139 }
2140
2141 #[test]
2142 fn test_variable_heights_fenwick_set() {
2143 let mut tracker = VariableHeightsFenwick::new(1, 5);
2144
2145 assert_eq!(tracker.get(0), 1);
2147 assert_eq!(tracker.total_height(), 5);
2148
2149 tracker.set(2, 10);
2151 assert_eq!(tracker.get(2), 10);
2152 assert_eq!(tracker.total_height(), 14); }
2154
2155 #[test]
2156 fn test_variable_heights_fenwick_resize() {
2157 let mut tracker = VariableHeightsFenwick::new(2, 3);
2158 assert_eq!(tracker.len(), 3);
2159 assert_eq!(tracker.total_height(), 6);
2160
2161 tracker.resize(5);
2163 assert_eq!(tracker.len(), 5);
2164 assert_eq!(tracker.total_height(), 10);
2165 assert_eq!(tracker.get(4), 2);
2166
2167 tracker.resize(2);
2169 assert_eq!(tracker.len(), 2);
2170 assert_eq!(tracker.total_height(), 4);
2171 }
2172
2173 #[test]
2174 fn test_virtualized_with_variable_heights_fenwick() {
2175 let mut virt: Virtualized<i32> = Virtualized::new(100).with_variable_heights_fenwick(2, 10);
2176
2177 for i in 0..10 {
2178 virt.push(i);
2179 }
2180
2181 let range = virt.visible_range(6);
2183 assert_eq!(range.end - range.start, 3);
2184 }
2185
2186 #[test]
2187 fn test_variable_heights_fenwick_performance() {
2188 use std::time::Instant;
2189
2190 let n = 100_000;
2192 let heights: Vec<u16> = (0..n).map(|i| (i % 10 + 1) as u16).collect();
2193 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2194
2195 let _ = tracker.find_item_at_offset(500_000);
2197 let _ = tracker.offset_of_item(50_000);
2198
2199 let start = Instant::now();
2201 let mut _sink = 0usize;
2202 for i in 0..10_000 {
2203 _sink = _sink.wrapping_add(tracker.find_item_at_offset((i * 50) as u32));
2204 }
2205 let find_time = start.elapsed();
2206
2207 let start = Instant::now();
2209 let mut _sink2 = 0u32;
2210 for i in 0..10_000 {
2211 _sink2 = _sink2.wrapping_add(tracker.offset_of_item((i * 10) % n));
2212 }
2213 let offset_time = start.elapsed();
2214
2215 eprintln!("=== VariableHeightsFenwick Performance (n={n}) ===");
2216 eprintln!("10k find_item_at_offset: {:?}", find_time);
2217 eprintln!("10k offset_of_item: {:?}", offset_time);
2218
2219 assert!(
2221 find_time < std::time::Duration::from_millis(50),
2222 "find_item_at_offset too slow: {:?}",
2223 find_time
2224 );
2225 assert!(
2226 offset_time < std::time::Duration::from_millis(50),
2227 "offset_of_item too slow: {:?}",
2228 offset_time
2229 );
2230 }
2231
2232 #[test]
2233 fn test_variable_heights_fenwick_scales_logarithmically() {
2234 use std::time::Instant;
2235
2236 let small_n = 1_000;
2238 let small_heights: Vec<u16> = (0..small_n).map(|i| (i % 5 + 1) as u16).collect();
2239 let small_tracker = VariableHeightsFenwick::from_heights(&small_heights, 1);
2240
2241 let large_n = 100_000;
2243 let large_heights: Vec<u16> = (0..large_n).map(|i| (i % 5 + 1) as u16).collect();
2244 let large_tracker = VariableHeightsFenwick::from_heights(&large_heights, 1);
2245
2246 let iterations = 5_000;
2247
2248 let start = Instant::now();
2250 for i in 0..iterations {
2251 let _ = small_tracker.find_item_at_offset((i * 2) as u32);
2252 }
2253 let small_time = start.elapsed();
2254
2255 let start = Instant::now();
2257 for i in 0..iterations {
2258 let _ = large_tracker.find_item_at_offset((i * 200) as u32);
2259 }
2260 let large_time = start.elapsed();
2261
2262 assert!(
2264 large_time < small_time * 5,
2265 "Not O(log n): small={:?}, large={:?}",
2266 small_time,
2267 large_time
2268 );
2269 }
2270
2271 #[test]
2278 fn new_zero_capacity() {
2279 let virt: Virtualized<i32> = Virtualized::new(0);
2280 assert_eq!(virt.len(), 0);
2281 assert!(virt.is_empty());
2282 assert_eq!(virt.scroll_offset(), 0);
2283 assert_eq!(virt.visible_count(), 0);
2284 assert!(!virt.follow_mode());
2285 }
2286
2287 #[test]
2288 fn external_zero_len_zero_cache() {
2289 let virt: Virtualized<i32> = Virtualized::external(0, 0);
2290 assert_eq!(virt.len(), 0);
2291 assert!(virt.is_empty());
2292 }
2293
2294 #[test]
2295 fn external_storage_returns_none_for_get() {
2296 let virt: Virtualized<i32> = Virtualized::external(100, 10);
2297 assert_eq!(virt.get(0), None);
2298 assert_eq!(virt.get(50), None);
2299 }
2300
2301 #[test]
2302 fn external_storage_returns_none_for_get_mut() {
2303 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2304 assert!(virt.get_mut(0).is_none());
2305 }
2306
2307 #[test]
2308 fn push_on_external_is_noop() {
2309 let mut virt: Virtualized<i32> = Virtualized::external(5, 10);
2310 virt.push(42);
2311 assert_eq!(virt.len(), 5);
2313 }
2314
2315 #[test]
2316 fn iter_on_external_is_empty() {
2317 let virt: Virtualized<i32> = Virtualized::external(100, 10);
2318 assert_eq!(virt.iter().count(), 0);
2319 }
2320
2321 #[test]
2322 fn set_external_len_on_owned_is_noop() {
2323 let mut virt: Virtualized<i32> = Virtualized::new(100);
2324 virt.push(1);
2325 virt.set_external_len(999);
2326 assert_eq!(virt.len(), 1); }
2328
2329 #[test]
2332 fn visible_range_zero_viewport() {
2333 let mut virt: Virtualized<i32> = Virtualized::new(100);
2334 virt.push(1);
2335 let range = virt.visible_range(0);
2336 assert_eq!(range, 0..0);
2337 assert_eq!(virt.visible_count(), 0);
2338 }
2339
2340 #[test]
2341 fn visible_range_empty_container() {
2342 let virt: Virtualized<i32> = Virtualized::new(100);
2343 let range = virt.visible_range(24);
2344 assert_eq!(range, 0..0);
2345 }
2346
2347 #[test]
2348 fn visible_range_fixed_height_zero() {
2349 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(0);
2351 for i in 0..10 {
2352 virt.push(i);
2353 }
2354 let range = virt.visible_range(5);
2355 assert_eq!(range, 0..5);
2357 }
2358
2359 #[test]
2360 fn visible_range_fewer_items_than_viewport() {
2361 let mut virt: Virtualized<i32> = Virtualized::new(100);
2362 for i in 0..3 {
2363 virt.push(i);
2364 }
2365 let range = virt.visible_range(24);
2366 assert_eq!(range, 0..3);
2368 }
2369
2370 #[test]
2371 fn visible_range_single_item() {
2372 let mut virt: Virtualized<i32> = Virtualized::new(100);
2373 virt.push(42);
2374 let range = virt.visible_range(1);
2375 assert_eq!(range, 0..1);
2376 }
2377
2378 #[test]
2381 fn render_range_at_start_clamps_overscan() {
2382 let mut virt: Virtualized<i32> =
2383 Virtualized::new(100).with_fixed_height(1).with_overscan(5);
2384 for i in 0..20 {
2385 virt.push(i);
2386 }
2387 let range = virt.render_range(10);
2389 assert_eq!(range.start, 0);
2390 }
2391
2392 #[test]
2393 fn render_range_at_end_clamps_overscan() {
2394 let mut virt: Virtualized<i32> =
2395 Virtualized::new(100).with_fixed_height(1).with_overscan(5);
2396 for i in 0..20 {
2397 virt.push(i);
2398 }
2399 virt.set_visible_count(10);
2400 virt.scroll_to(10); let range = virt.render_range(10);
2402 assert_eq!(range.end, 20);
2404 }
2405
2406 #[test]
2407 fn render_range_zero_overscan() {
2408 let mut virt: Virtualized<i32> =
2409 Virtualized::new(100).with_fixed_height(1).with_overscan(0);
2410 for i in 0..20 {
2411 virt.push(i);
2412 }
2413 virt.set_visible_count(10);
2414 virt.scroll_to(5);
2415 let range = virt.render_range(10);
2416 let visible = virt.visible_range(10);
2418 assert_eq!(range, visible);
2419 }
2420
2421 #[test]
2424 fn scroll_on_empty_is_noop() {
2425 let mut virt: Virtualized<i32> = Virtualized::new(100);
2426 virt.scroll(10);
2427 assert_eq!(virt.scroll_offset(), 0);
2428 }
2429
2430 #[test]
2431 fn scroll_delta_zero_does_not_disable_follow() {
2432 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2433 virt.push(1);
2434 virt.scroll(0);
2435 assert!(virt.follow_mode());
2437 }
2438
2439 #[test]
2440 fn scroll_negative_beyond_start() {
2441 let mut virt: Virtualized<i32> = Virtualized::new(100);
2442 for i in 0..10 {
2443 virt.push(i);
2444 }
2445 virt.scroll(-1);
2446 assert_eq!(virt.scroll_offset(), 0);
2447 }
2448
2449 #[test]
2450 fn scroll_to_on_empty() {
2451 let mut virt: Virtualized<i32> = Virtualized::new(100);
2452 virt.scroll_to(100);
2454 assert_eq!(virt.scroll_offset(), 0);
2455 }
2456
2457 #[test]
2458 fn scroll_to_top_already_at_top() {
2459 let mut virt: Virtualized<i32> = Virtualized::new(100);
2460 virt.push(1);
2461 virt.scroll_to_top();
2462 assert_eq!(virt.scroll_offset(), 0);
2463 }
2464
2465 #[test]
2466 fn scroll_to_bottom_fewer_items_than_visible() {
2467 let mut virt: Virtualized<i32> = Virtualized::new(100);
2468 virt.set_visible_count(10);
2469 for i in 0..3 {
2470 virt.push(i);
2471 }
2472 virt.scroll_to_bottom();
2473 assert_eq!(virt.scroll_offset(), 0);
2475 }
2476
2477 #[test]
2478 fn scroll_to_bottom_visible_count_zero() {
2479 let mut virt: Virtualized<i32> = Virtualized::new(100);
2480 for i in 0..20 {
2481 virt.push(i);
2482 }
2483 virt.scroll_to_bottom();
2485 assert_eq!(virt.scroll_offset(), 0);
2486 }
2487
2488 #[test]
2491 fn page_up_visible_count_zero_is_noop() {
2492 let mut virt: Virtualized<i32> = Virtualized::new(100);
2493 for i in 0..20 {
2494 virt.push(i);
2495 }
2496 virt.scroll_to(10);
2497 virt.page_up();
2499 assert_eq!(virt.scroll_offset(), 10);
2500 }
2501
2502 #[test]
2503 fn page_down_visible_count_zero_is_noop() {
2504 let mut virt: Virtualized<i32> = Virtualized::new(100);
2505 for i in 0..20 {
2506 virt.push(i);
2507 }
2508 virt.page_down();
2510 assert_eq!(virt.scroll_offset(), 0);
2511 }
2512
2513 #[test]
2516 fn is_at_bottom_fewer_items_than_visible() {
2517 let mut virt: Virtualized<i32> = Virtualized::new(100);
2518 virt.set_visible_count(10);
2519 for i in 0..3 {
2520 virt.push(i);
2521 }
2522 assert!(virt.is_at_bottom());
2523 }
2524
2525 #[test]
2526 fn is_at_bottom_empty() {
2527 let virt: Virtualized<i32> = Virtualized::new(100);
2528 assert!(virt.is_at_bottom());
2530 }
2531
2532 #[test]
2535 fn trim_front_under_max_returns_zero() {
2536 let mut virt: Virtualized<i32> = Virtualized::new(100);
2537 for i in 0..5 {
2538 virt.push(i);
2539 }
2540 let removed = virt.trim_front(10);
2541 assert_eq!(removed, 0);
2542 assert_eq!(virt.len(), 5);
2543 }
2544
2545 #[test]
2546 fn trim_front_adjusts_scroll_offset() {
2547 let mut virt: Virtualized<i32> = Virtualized::new(100);
2548 for i in 0..20 {
2549 virt.push(i);
2550 }
2551 virt.scroll_to(10);
2552 let removed = virt.trim_front(15);
2553 assert_eq!(removed, 5);
2554 assert_eq!(virt.len(), 15);
2555 assert_eq!(virt.scroll_offset(), 5);
2557 }
2558
2559 #[test]
2560 fn trim_front_scroll_offset_saturates_to_zero() {
2561 let mut virt: Virtualized<i32> = Virtualized::new(100);
2562 for i in 0..20 {
2563 virt.push(i);
2564 }
2565 virt.scroll_to(2);
2566 let removed = virt.trim_front(10);
2567 assert_eq!(removed, 10);
2568 assert_eq!(virt.scroll_offset(), 0);
2570 }
2571
2572 #[test]
2573 fn trim_front_on_external_returns_zero() {
2574 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2575 let removed = virt.trim_front(5);
2576 assert_eq!(removed, 0);
2577 }
2578
2579 #[test]
2582 fn clear_on_external_resets_scroll() {
2583 let mut virt: Virtualized<i32> = Virtualized::external(100, 10);
2584 virt.scroll_to(50);
2585 virt.clear();
2586 assert_eq!(virt.scroll_offset(), 0);
2587 assert_eq!(virt.len(), 100);
2589 }
2590
2591 #[test]
2594 fn tick_zero_velocity_is_noop() {
2595 let mut virt: Virtualized<i32> = Virtualized::new(100);
2596 for i in 0..20 {
2597 virt.push(i);
2598 }
2599 virt.tick(Duration::from_millis(100));
2600 assert_eq!(virt.scroll_offset(), 0);
2601 }
2602
2603 #[test]
2604 fn tick_below_threshold_stops_momentum() {
2605 let mut virt: Virtualized<i32> = Virtualized::new(100);
2606 for i in 0..20 {
2607 virt.push(i);
2608 }
2609 virt.fling(0.05); virt.tick(Duration::from_millis(100));
2611 assert_eq!(virt.scroll_offset(), 0);
2613 }
2614
2615 #[test]
2616 fn tick_zero_duration_no_scroll() {
2617 let mut virt: Virtualized<i32> = Virtualized::new(100);
2618 for i in 0..50 {
2619 virt.push(i);
2620 }
2621 virt.fling(100.0);
2622 virt.tick(Duration::ZERO);
2623 assert_eq!(virt.scroll_offset(), 0);
2625 }
2626
2627 #[test]
2628 fn fling_negative_scrolls_up() {
2629 let mut virt: Virtualized<i32> = Virtualized::new(100);
2630 for i in 0..50 {
2631 virt.push(i);
2632 }
2633 virt.scroll(20);
2634 let before = virt.scroll_offset();
2635 virt.fling(-50.0);
2636 virt.tick(Duration::from_millis(100));
2637 assert!(virt.scroll_offset() < before);
2638 }
2639
2640 #[test]
2643 fn follow_mode_auto_scrolls_on_push() {
2644 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2645 virt.set_visible_count(5);
2646 for i in 0..20 {
2647 virt.push(i);
2648 }
2649 assert!(virt.is_at_bottom());
2651 assert_eq!(virt.scroll_offset(), 15); }
2653
2654 #[test]
2655 fn set_follow_false_does_not_scroll() {
2656 let mut virt: Virtualized<i32> = Virtualized::new(100);
2657 virt.set_visible_count(5);
2658 for i in 0..20 {
2659 virt.push(i);
2660 }
2661 virt.scroll_to(5);
2662 virt.set_follow(false);
2663 assert_eq!(virt.scroll_offset(), 5); }
2665
2666 #[test]
2667 fn scroll_to_start_disables_follow() {
2668 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
2669 virt.set_visible_count(5);
2670 for i in 0..20 {
2671 virt.push(i);
2672 }
2673 virt.scroll_to_start();
2674 assert!(!virt.follow_mode());
2675 assert_eq!(virt.scroll_offset(), 0);
2676 }
2677
2678 #[test]
2679 fn scroll_to_end_enables_follow() {
2680 let mut virt: Virtualized<i32> = Virtualized::new(100);
2681 virt.set_visible_count(5);
2682 for i in 0..20 {
2683 virt.push(i);
2684 }
2685 assert!(!virt.follow_mode());
2686 virt.scroll_to_end();
2687 assert!(virt.follow_mode());
2688 assert!(virt.is_at_bottom());
2689 }
2690
2691 #[test]
2692 fn external_follow_mode_scrolls_on_set_external_len() {
2693 let mut virt: Virtualized<i32> = Virtualized::external(10, 100).with_follow(true);
2694 virt.set_visible_count(5);
2695 virt.set_external_len(20);
2696 assert_eq!(virt.len(), 20);
2697 assert!(virt.is_at_bottom());
2698 }
2699
2700 #[test]
2703 fn builder_chain_all_options() {
2704 let virt: Virtualized<i32> = Virtualized::new(100)
2705 .with_fixed_height(3)
2706 .with_overscan(5)
2707 .with_follow(true);
2708 assert!(virt.follow_mode());
2709 let range = virt.visible_range(9);
2712 assert_eq!(range, 0..0);
2713 }
2714
2715 #[test]
2718 fn height_cache_default() {
2719 let cache = HeightCache::default();
2720 assert_eq!(cache.get(0), 1); assert_eq!(cache.capacity, 1000);
2722 }
2723
2724 #[test]
2725 fn height_cache_get_before_base_offset() {
2726 let mut cache = HeightCache::new(5, 100);
2727 cache.set(200, 10); assert_eq!(cache.get(0), 5);
2731 }
2732
2733 #[test]
2734 fn height_cache_set_before_base_offset_ignored() {
2735 let mut cache = HeightCache::new(5, 100);
2736 cache.set(200, 10);
2737 let base = cache.base_offset;
2738 cache.set(0, 99); assert_eq!(cache.get(0), 5); assert_eq!(cache.base_offset, base); }
2742
2743 #[test]
2744 fn height_cache_capacity_zero_ignores_all_sets() {
2745 let mut cache = HeightCache::new(3, 0);
2746 cache.set(0, 10);
2747 cache.set(5, 20);
2748 assert_eq!(cache.get(0), 3);
2750 assert_eq!(cache.get(5), 3);
2751 }
2752
2753 #[test]
2754 fn height_cache_clear_resets_base() {
2755 let mut cache = HeightCache::new(1, 100);
2756 cache.set(50, 10);
2757 cache.clear();
2758 assert_eq!(cache.base_offset, 0);
2759 assert_eq!(cache.get(50), 1); }
2761
2762 #[test]
2763 fn height_cache_eviction_trims_oldest() {
2764 let mut cache = HeightCache::new(1, 4);
2765 for i in 0..6 {
2767 cache.set(i, (i + 10) as u16);
2768 }
2769 assert!(cache.cache.len() <= cache.capacity);
2771 assert_eq!(cache.get(5), 15);
2773 assert_eq!(cache.get(0), 1);
2775 }
2776
2777 #[test]
2780 fn fenwick_default_is_empty() {
2781 let tracker = VariableHeightsFenwick::default();
2782 assert!(tracker.is_empty());
2783 assert_eq!(tracker.len(), 0);
2784 assert_eq!(tracker.total_height(), 0);
2785 assert_eq!(tracker.default_height(), 1);
2786 }
2787
2788 #[test]
2789 fn fenwick_get_beyond_len_returns_default() {
2790 let tracker = VariableHeightsFenwick::new(3, 5);
2791 assert_eq!(tracker.get(5), 3); assert_eq!(tracker.get(100), 3);
2793 }
2794
2795 #[test]
2796 fn fenwick_set_beyond_len_resizes() {
2797 let mut tracker = VariableHeightsFenwick::new(2, 3);
2798 assert_eq!(tracker.len(), 3);
2799 tracker.set(10, 7);
2800 assert!(tracker.len() > 10);
2801 assert_eq!(tracker.get(10), 7);
2802 }
2803
2804 #[test]
2805 fn fenwick_offset_of_item_zero_always_zero() {
2806 let tracker = VariableHeightsFenwick::new(5, 10);
2807 assert_eq!(tracker.offset_of_item(0), 0);
2808
2809 let empty = VariableHeightsFenwick::new(5, 0);
2810 assert_eq!(empty.offset_of_item(0), 0);
2811 }
2812
2813 #[test]
2814 fn fenwick_find_item_at_offset_empty() {
2815 let tracker = VariableHeightsFenwick::new(1, 0);
2816 assert_eq!(tracker.find_item_at_offset(0), 0);
2817 assert_eq!(tracker.find_item_at_offset(100), 0);
2818 }
2819
2820 #[test]
2821 fn fenwick_visible_count_zero_viewport() {
2822 let tracker = VariableHeightsFenwick::new(2, 10);
2823 assert_eq!(tracker.visible_count(0, 0), 0);
2824 }
2825
2826 #[test]
2827 fn fenwick_visible_count_start_beyond_len() {
2828 let tracker = VariableHeightsFenwick::new(2, 5);
2829 let count = tracker.visible_count(100, 10);
2831 assert_eq!(count, 0);
2833 }
2834
2835 #[test]
2836 fn fenwick_clear_then_operations() {
2837 let mut tracker = VariableHeightsFenwick::new(3, 5);
2838 assert_eq!(tracker.total_height(), 15);
2839 tracker.clear();
2840 assert_eq!(tracker.len(), 0);
2841 assert_eq!(tracker.total_height(), 0);
2842 assert_eq!(tracker.find_item_at_offset(0), 0);
2843 }
2844
2845 #[test]
2846 fn fenwick_rebuild_replaces_data() {
2847 let mut tracker = VariableHeightsFenwick::new(1, 10);
2848 assert_eq!(tracker.total_height(), 10);
2849 tracker.rebuild(&[5, 3, 2]);
2850 assert_eq!(tracker.len(), 3);
2851 assert_eq!(tracker.total_height(), 10);
2852 assert_eq!(tracker.get(0), 5);
2853 assert_eq!(tracker.get(1), 3);
2854 assert_eq!(tracker.get(2), 2);
2855 }
2856
2857 #[test]
2858 fn fenwick_resize_same_size_is_noop() {
2859 let mut tracker = VariableHeightsFenwick::new(2, 5);
2860 tracker.set(2, 10);
2861 tracker.resize(5);
2862 assert_eq!(tracker.get(2), 10);
2864 assert_eq!(tracker.len(), 5);
2865 }
2866
2867 #[test]
2870 fn list_state_default_matches_new() {
2871 let d = VirtualizedListState::default();
2872 let n = VirtualizedListState::new();
2873 assert_eq!(d.selected, n.selected);
2874 assert_eq!(d.scroll_offset(), n.scroll_offset());
2875 assert_eq!(d.visible_count(), n.visible_count());
2876 assert_eq!(d.follow_mode(), n.follow_mode());
2877 }
2878
2879 #[test]
2880 fn list_state_select_next_on_empty() {
2881 let mut state = VirtualizedListState::new();
2882 state.select_next(0);
2883 assert_eq!(state.selected, None);
2884 }
2885
2886 #[test]
2887 fn list_state_select_previous_on_empty() {
2888 let mut state = VirtualizedListState::new();
2889 state.select_previous(0);
2890 assert_eq!(state.selected, None);
2891 }
2892
2893 #[test]
2894 fn list_state_select_previous_from_none() {
2895 let mut state = VirtualizedListState::new();
2896 state.select_previous(10);
2897 assert_eq!(state.selected, Some(0));
2898 }
2899
2900 #[test]
2901 fn list_state_select_next_from_none() {
2902 let mut state = VirtualizedListState::new();
2903 state.select_next(10);
2904 assert_eq!(state.selected, Some(0));
2905 }
2906
2907 #[test]
2908 fn list_state_scroll_zero_items() {
2909 let mut state = VirtualizedListState::new();
2910 state.scroll(10, 0);
2911 assert_eq!(state.scroll_offset(), 0);
2912 }
2913
2914 #[test]
2915 fn list_state_scroll_to_clamps() {
2916 let mut state = VirtualizedListState::new();
2917 state.scroll_to(100, 10);
2918 assert_eq!(state.scroll_offset(), 9);
2919 }
2920
2921 #[test]
2922 fn list_state_scroll_to_bottom_zero_items() {
2923 let mut state = VirtualizedListState::new();
2924 state.scroll_to_bottom(0);
2925 assert_eq!(state.scroll_offset(), 0);
2926 }
2927
2928 #[test]
2929 fn list_state_is_at_bottom_zero_items() {
2930 let state = VirtualizedListState::new();
2931 assert!(state.is_at_bottom(0));
2932 }
2933
2934 #[test]
2935 fn list_state_page_up_visible_count_zero() {
2936 let mut state = VirtualizedListState::new();
2937 state.scroll_offset = 5;
2938 state.page_up(20);
2939 assert_eq!(state.scroll_offset(), 5);
2941 }
2942
2943 #[test]
2944 fn list_state_page_down_visible_count_zero() {
2945 let mut state = VirtualizedListState::new();
2946 state.page_down(20);
2947 assert_eq!(state.scroll_offset(), 0);
2949 }
2950
2951 #[test]
2952 fn list_state_set_follow_false_no_scroll() {
2953 let mut state = VirtualizedListState::new();
2954 state.scroll_offset = 5;
2955 state.set_follow(false, 20);
2956 assert_eq!(state.scroll_offset(), 5); assert!(!state.follow_mode());
2958 }
2959
2960 #[test]
2961 fn list_state_persistence_id() {
2962 let state = VirtualizedListState::new().with_persistence_id("my-list");
2963 assert_eq!(state.persistence_id(), Some("my-list"));
2964 }
2965
2966 #[test]
2967 fn list_state_persistence_id_none() {
2968 let state = VirtualizedListState::new();
2969 assert_eq!(state.persistence_id(), None);
2970 }
2971
2972 #[test]
2973 fn list_state_momentum_tick_zero_items() {
2974 let mut state = VirtualizedListState::new();
2975 state.fling(50.0);
2976 state.tick(Duration::from_millis(100), 0);
2977 assert_eq!(state.scroll_offset(), 0);
2979 }
2980
2981 #[test]
2984 fn persist_state_default() {
2985 let ps = VirtualizedListPersistState::default();
2986 assert_eq!(ps.selected, None);
2987 assert_eq!(ps.scroll_offset, 0);
2988 assert!(!ps.follow_mode);
2989 }
2990
2991 #[test]
2992 fn persist_state_eq() {
2993 let a = VirtualizedListPersistState {
2994 selected: Some(5),
2995 scroll_offset: 10,
2996 follow_mode: true,
2997 };
2998 let b = a.clone();
2999 assert_eq!(a, b);
3000 }
3001
3002 #[test]
3005 fn stateful_state_key_with_persistence_id() {
3006 use crate::stateful::Stateful;
3007 let state = VirtualizedListState::new().with_persistence_id("logs");
3008 let key = state.state_key();
3009 assert_eq!(key.widget_type, "VirtualizedList");
3010 assert_eq!(key.instance_id, "logs");
3011 }
3012
3013 #[test]
3014 fn stateful_state_key_default_instance() {
3015 use crate::stateful::Stateful;
3016 let state = VirtualizedListState::new();
3017 let key = state.state_key();
3018 assert_eq!(key.instance_id, "default");
3019 }
3020
3021 #[test]
3022 fn stateful_save_restore_roundtrip() {
3023 use crate::stateful::Stateful;
3024 let mut state = VirtualizedListState::new();
3025 state.selected = Some(7);
3026 state.scroll_offset = 15;
3027 state.follow_mode = true;
3028 state.scroll_velocity = 42.0; let saved = state.save_state();
3031 assert_eq!(saved.selected, Some(7));
3032 assert_eq!(saved.scroll_offset, 15);
3033 assert!(saved.follow_mode);
3034
3035 let mut restored = VirtualizedListState::new();
3036 restored.scroll_velocity = 99.0;
3037 restored.restore_state(saved);
3038 assert_eq!(restored.selected, Some(7));
3039 assert_eq!(restored.scroll_offset, 15);
3040 assert!(restored.follow_mode);
3041 assert_eq!(restored.scroll_velocity, 0.0);
3043 }
3044
3045 #[test]
3048 fn virtualized_list_builder() {
3049 let items: Vec<String> = vec!["a".into()];
3050 let list = VirtualizedList::new(&items)
3051 .style(Style::default())
3052 .highlight_style(Style::default())
3053 .show_scrollbar(false)
3054 .fixed_height(3);
3055 assert_eq!(list.fixed_height, 3);
3056 assert!(!list.show_scrollbar);
3057 }
3058
3059 #[test]
3062 fn virtualized_storage_debug() {
3063 let storage: VirtualizedStorage<i32> = VirtualizedStorage::Owned(VecDeque::new());
3064 let dbg = format!("{:?}", storage);
3065 assert!(dbg.contains("Owned"));
3066
3067 let ext: VirtualizedStorage<i32> = VirtualizedStorage::External {
3068 len: 100,
3069 cache_capacity: 10,
3070 };
3071 let dbg = format!("{:?}", ext);
3072 assert!(dbg.contains("External"));
3073 }
3074
3075 #[test]
3076 fn test_virtualized_list_handle_mouse_drag_smooth() {
3077 use crate::scrollbar::SCROLLBAR_PART_THUMB;
3078 use ftui_core::event::{MouseButton, MouseEvent, MouseEventKind};
3079 use ftui_render::frame::{HitId, HitRegion};
3080
3081 let mut state = VirtualizedListState::new();
3082 let scrollbar_hit_id = HitId::new(1);
3083 let total_items = 100;
3084 let viewport_height = 10;
3085 let fixed_height = 1;
3086
3087 let track_len = 10u64;
3093 let track_pos = 0u64;
3094 let hit_data = (SCROLLBAR_PART_THUMB << 56)
3095 | ((track_len & 0x0FFF_FFFF) << 28)
3096 | (track_pos & 0x0FFF_FFFF);
3097
3098 let down_event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
3099 let hit = Some((scrollbar_hit_id, HitRegion::Scrollbar, hit_data));
3100
3101 state.handle_mouse(
3102 &down_event,
3103 hit,
3104 scrollbar_hit_id,
3105 total_items,
3106 viewport_height,
3107 fixed_height,
3108 );
3109
3110 assert!(
3111 state.scrollbar_drag_anchor.is_some(),
3112 "Drag anchor should be set on down"
3113 );
3114 assert_eq!(
3115 state.scrollbar_drag_anchor.unwrap(),
3116 0,
3117 "Anchor should be 0 (clicked top of thumb)"
3118 );
3119
3120 let drag_pos = 1u64;
3125 let drag_data = (SCROLLBAR_PART_THUMB << 56)
3126 | ((track_len & 0x0FFF_FFFF) << 28)
3127 | (drag_pos & 0x0FFF_FFFF);
3128 let drag_event = MouseEvent::new(MouseEventKind::Drag(MouseButton::Left), 0, 1);
3129 let drag_hit = Some((scrollbar_hit_id, HitRegion::Scrollbar, drag_data));
3130
3131 state.handle_mouse(
3132 &drag_event,
3133 drag_hit,
3134 scrollbar_hit_id,
3135 total_items,
3136 viewport_height,
3137 fixed_height,
3138 );
3139
3140 assert_eq!(
3141 state.scroll_offset, 10,
3142 "Scroll offset should update smoothly"
3143 );
3144 }
3145}