1#![forbid(unsafe_code)]
2
3use std::cell::Cell as StdCell;
34use std::collections::VecDeque;
35use std::ops::Range;
36use std::time::Duration;
37
38#[allow(unused_imports)]
40use crate::scrollbar::{Scrollbar, ScrollbarOrientation, ScrollbarState};
41#[allow(unused_imports)]
42use crate::{StatefulWidget, set_style_area};
43#[allow(unused_imports)]
44use ftui_core::geometry::Rect;
45#[allow(unused_imports)]
46use ftui_render::cell::Cell;
47#[allow(unused_imports)]
48use ftui_render::frame::Frame;
49#[allow(unused_imports)]
50use ftui_style::Style;
51
52#[derive(Debug, Clone)]
61pub struct Virtualized<T> {
62 storage: VirtualizedStorage<T>,
64 scroll_offset: usize,
66 visible_count: StdCell<usize>,
68 overscan: usize,
70 item_height: ItemHeight,
72 follow_mode: bool,
74 scroll_velocity: f32,
76}
77
78#[derive(Debug, Clone)]
80pub enum VirtualizedStorage<T> {
81 Owned(VecDeque<T>),
83 External {
86 len: usize,
88 cache_capacity: usize,
90 },
91}
92
93#[derive(Debug, Clone)]
95pub enum ItemHeight {
96 Fixed(u16),
98 Variable(HeightCache),
100 VariableFenwick(VariableHeightsFenwick),
102}
103
104#[derive(Debug, Clone)]
106pub struct HeightCache {
107 cache: Vec<Option<u16>>,
109 base_offset: usize,
111 default_height: u16,
113 capacity: usize,
115}
116
117impl<T> Virtualized<T> {
118 #[must_use]
123 pub fn new(capacity: usize) -> Self {
124 Self {
125 storage: VirtualizedStorage::Owned(VecDeque::with_capacity(capacity.min(1024))),
126 scroll_offset: 0,
127 visible_count: StdCell::new(0),
128 overscan: 2,
129 item_height: ItemHeight::Fixed(1),
130 follow_mode: false,
131 scroll_velocity: 0.0,
132 }
133 }
134
135 #[must_use]
137 pub fn external(len: usize, cache_capacity: usize) -> Self {
138 Self {
139 storage: VirtualizedStorage::External {
140 len,
141 cache_capacity,
142 },
143 scroll_offset: 0,
144 visible_count: StdCell::new(0),
145 overscan: 2,
146 item_height: ItemHeight::Fixed(1),
147 follow_mode: false,
148 scroll_velocity: 0.0,
149 }
150 }
151
152 #[must_use]
154 pub fn with_item_height(mut self, height: ItemHeight) -> Self {
155 self.item_height = height;
156 self
157 }
158
159 #[must_use]
161 pub fn with_fixed_height(mut self, height: u16) -> Self {
162 self.item_height = ItemHeight::Fixed(height);
163 self
164 }
165
166 #[must_use]
171 pub fn with_variable_heights_fenwick(mut self, default_height: u16, capacity: usize) -> Self {
172 self.item_height =
173 ItemHeight::VariableFenwick(VariableHeightsFenwick::new(default_height, capacity));
174 self
175 }
176
177 #[must_use]
179 pub fn with_overscan(mut self, overscan: usize) -> Self {
180 self.overscan = overscan;
181 self
182 }
183
184 #[must_use]
186 pub fn with_follow(mut self, follow: bool) -> Self {
187 self.follow_mode = follow;
188 self
189 }
190
191 #[must_use]
193 pub fn len(&self) -> usize {
194 match &self.storage {
195 VirtualizedStorage::Owned(items) => items.len(),
196 VirtualizedStorage::External { len, .. } => *len,
197 }
198 }
199
200 #[must_use]
202 pub fn is_empty(&self) -> bool {
203 self.len() == 0
204 }
205
206 #[must_use]
208 pub fn scroll_offset(&self) -> usize {
209 self.scroll_offset
210 }
211
212 #[must_use]
214 pub fn visible_count(&self) -> usize {
215 self.visible_count.get()
216 }
217
218 #[must_use]
220 pub fn follow_mode(&self) -> bool {
221 self.follow_mode
222 }
223
224 #[must_use]
226 pub fn visible_range(&self, viewport_height: u16) -> Range<usize> {
227 if self.is_empty() || viewport_height == 0 {
228 self.visible_count.set(0);
229 return 0..0;
230 }
231
232 let items_visible = match &self.item_height {
233 ItemHeight::Fixed(h) if *h > 0 => (viewport_height / h) as usize,
234 ItemHeight::Fixed(_) => viewport_height as usize,
235 ItemHeight::Variable(cache) => {
236 let mut count = 0;
238 let mut total_height = 0u16;
239 let start = self.scroll_offset;
240 while start + count < self.len() {
241 let next = cache.get(start + count);
242 let proposed = total_height.saturating_add(next);
243 if proposed > viewport_height {
244 break;
245 }
246 total_height = proposed;
247 count += 1;
248 }
249 count
250 }
251 ItemHeight::VariableFenwick(tracker) => {
252 tracker.visible_count(self.scroll_offset, viewport_height)
254 }
255 };
256
257 let start = self.scroll_offset;
258 let end = (start + items_visible).min(self.len());
259 self.visible_count.set(items_visible);
260 start..end
261 }
262
263 #[must_use]
265 pub fn render_range(&self, viewport_height: u16) -> Range<usize> {
266 let visible = self.visible_range(viewport_height);
267 let start = visible.start.saturating_sub(self.overscan);
268 let end = visible.end.saturating_add(self.overscan).min(self.len());
269 start..end
270 }
271
272 pub fn scroll(&mut self, delta: i32) {
274 if self.is_empty() {
275 return;
276 }
277 let visible_count = self.visible_count.get();
278 let max_offset = if visible_count > 0 {
279 self.len().saturating_sub(visible_count)
280 } else {
281 self.len().saturating_sub(1)
282 };
283 let new_offset = (self.scroll_offset as i64 + delta as i64)
284 .max(0)
285 .min(max_offset as i64);
286 self.scroll_offset = new_offset as usize;
287
288 if delta != 0 {
290 self.follow_mode = false;
291 }
292 }
293
294 pub fn scroll_to(&mut self, idx: usize) {
296 self.scroll_offset = idx.min(self.len().saturating_sub(1));
297 self.follow_mode = false;
298 }
299
300 pub fn scroll_to_bottom(&mut self) {
302 let visible_count = self.visible_count.get();
303 if self.len() > visible_count && visible_count > 0 {
304 self.scroll_offset = self.len() - visible_count;
305 } else {
306 self.scroll_offset = 0;
307 }
308 }
309
310 pub fn scroll_to_top(&mut self) {
312 self.scroll_offset = 0;
313 self.follow_mode = false;
314 }
315
316 pub fn scroll_to_start(&mut self) {
318 self.scroll_to_top();
319 }
320
321 pub fn scroll_to_end(&mut self) {
323 self.scroll_to_bottom();
324 self.follow_mode = true;
325 }
326
327 pub fn page_up(&mut self) {
329 let visible_count = self.visible_count.get();
330 if visible_count > 0 {
331 self.scroll(-(visible_count as i32));
332 }
333 }
334
335 pub fn page_down(&mut self) {
337 let visible_count = self.visible_count.get();
338 if visible_count > 0 {
339 self.scroll(visible_count as i32);
340 }
341 }
342
343 pub fn set_follow(&mut self, follow: bool) {
345 self.follow_mode = follow;
346 if follow {
347 self.scroll_to_bottom();
348 }
349 }
350
351 #[must_use]
353 pub fn is_at_bottom(&self) -> bool {
354 let visible_count = self.visible_count.get();
355 if self.len() <= visible_count {
356 true
357 } else {
358 self.scroll_offset >= self.len() - visible_count
359 }
360 }
361
362 pub fn fling(&mut self, velocity: f32) {
364 self.scroll_velocity = velocity;
365 }
366
367 pub fn tick(&mut self, dt: Duration) {
369 if self.scroll_velocity.abs() > 0.1 {
370 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
371 if delta != 0 {
372 self.scroll(delta);
373 }
374 self.scroll_velocity *= 0.95;
376 } else {
377 self.scroll_velocity = 0.0;
378 }
379 }
380
381 pub fn set_visible_count(&mut self, count: usize) {
383 self.visible_count.set(count);
384 }
385}
386
387impl<T> Virtualized<T> {
388 pub fn push(&mut self, item: T) {
390 if let VirtualizedStorage::Owned(items) = &mut self.storage {
391 items.push_back(item);
392 if self.follow_mode {
393 self.scroll_to_bottom();
394 }
395 }
396 }
397
398 #[must_use]
400 pub fn get(&self, idx: usize) -> Option<&T> {
401 if let VirtualizedStorage::Owned(items) = &self.storage {
402 items.get(idx)
403 } else {
404 None
405 }
406 }
407
408 pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> {
410 if let VirtualizedStorage::Owned(items) = &mut self.storage {
411 items.get_mut(idx)
412 } else {
413 None
414 }
415 }
416
417 pub fn clear(&mut self) {
419 if let VirtualizedStorage::Owned(items) = &mut self.storage {
420 items.clear();
421 }
422 self.scroll_offset = 0;
423 }
424
425 pub fn trim_front(&mut self, max: usize) -> usize {
429 if let VirtualizedStorage::Owned(items) = &mut self.storage
430 && items.len() > max
431 {
432 let to_remove = items.len() - max;
433 items.drain(..to_remove);
434 self.scroll_offset = self.scroll_offset.saturating_sub(to_remove);
436 return to_remove;
437 }
438 0
439 }
440
441 pub fn iter(&self) -> Box<dyn Iterator<Item = &T> + '_> {
444 match &self.storage {
445 VirtualizedStorage::Owned(items) => Box::new(items.iter()),
446 VirtualizedStorage::External { .. } => Box::new(std::iter::empty()),
447 }
448 }
449
450 pub fn set_external_len(&mut self, len: usize) {
452 if let VirtualizedStorage::External { len: l, .. } = &mut self.storage {
453 *l = len;
454 if self.follow_mode {
455 self.scroll_to_bottom();
456 }
457 }
458 }
459}
460
461impl Default for HeightCache {
462 fn default() -> Self {
463 Self::new(1, 1000)
464 }
465}
466
467impl HeightCache {
468 #[must_use]
470 pub fn new(default_height: u16, capacity: usize) -> Self {
471 Self {
472 cache: Vec::new(),
473 base_offset: 0,
474 default_height,
475 capacity,
476 }
477 }
478
479 #[must_use]
481 pub fn get(&self, idx: usize) -> u16 {
482 if idx < self.base_offset {
483 return self.default_height;
484 }
485 let local = idx - self.base_offset;
486 self.cache
487 .get(local)
488 .and_then(|h| *h)
489 .unwrap_or(self.default_height)
490 }
491
492 pub fn set(&mut self, idx: usize, height: u16) {
494 if self.capacity == 0 {
495 return;
496 }
497 if idx < self.base_offset {
498 return;
500 }
501 let mut local = idx - self.base_offset;
502 if local >= self.capacity {
503 self.base_offset = idx.saturating_add(1).saturating_sub(self.capacity);
505 self.cache.clear();
506 local = idx - self.base_offset;
507 }
508 if local >= self.cache.len() {
509 self.cache.resize(local + 1, None);
510 }
511 self.cache[local] = Some(height);
512
513 if self.cache.len() > self.capacity {
515 let to_remove = self.cache.len() - self.capacity;
516 self.cache.drain(0..to_remove);
517 self.base_offset += to_remove;
518 }
519 }
520
521 pub fn clear(&mut self) {
523 self.cache.clear();
524 self.base_offset = 0;
525 }
526}
527
528use crate::fenwick::FenwickTree;
533
534#[derive(Debug, Clone)]
554pub struct VariableHeightsFenwick {
555 tree: FenwickTree,
557 default_height: u16,
559 len: usize,
561}
562
563impl Default for VariableHeightsFenwick {
564 fn default() -> Self {
565 Self::new(1, 0)
566 }
567}
568
569impl VariableHeightsFenwick {
570 #[must_use]
572 pub fn new(default_height: u16, capacity: usize) -> Self {
573 let tree = if capacity > 0 {
574 let heights: Vec<u32> = vec![u32::from(default_height); capacity];
576 FenwickTree::from_values(&heights)
577 } else {
578 FenwickTree::new(0)
579 };
580 Self {
581 tree,
582 default_height,
583 len: capacity,
584 }
585 }
586
587 #[must_use]
589 pub fn from_heights(heights: &[u16], default_height: u16) -> Self {
590 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
591 Self {
592 tree: FenwickTree::from_values(&heights_u32),
593 default_height,
594 len: heights.len(),
595 }
596 }
597
598 #[must_use]
600 pub fn len(&self) -> usize {
601 self.len
602 }
603
604 #[must_use]
606 pub fn is_empty(&self) -> bool {
607 self.len == 0
608 }
609
610 #[must_use]
612 pub fn default_height(&self) -> u16 {
613 self.default_height
614 }
615
616 #[must_use]
618 pub fn get(&self, idx: usize) -> u16 {
619 if idx >= self.len {
620 return self.default_height;
621 }
622 self.tree.get(idx).min(u32::from(u16::MAX)) as u16
624 }
625
626 pub fn set(&mut self, idx: usize, height: u16) {
628 if idx >= self.len {
629 self.resize(idx + 1);
631 }
632 self.tree.set(idx, u32::from(height));
633 }
634
635 #[must_use]
639 pub fn offset_of_item(&self, idx: usize) -> u32 {
640 if idx == 0 || self.len == 0 {
641 return 0;
642 }
643 let clamped = idx.min(self.len);
644 if clamped > 0 {
645 self.tree.prefix(clamped - 1)
646 } else {
647 0
648 }
649 }
650
651 #[must_use]
658 pub fn find_item_at_offset(&self, offset: u32) -> usize {
659 if self.len == 0 {
660 return 0;
661 }
662 if offset == 0 {
663 return 0;
664 }
665 match self.tree.find_prefix(offset) {
675 Some(i) => {
676 (i + 1).min(self.len)
680 }
681 None => {
682 0
684 }
685 }
686 }
687
688 #[must_use]
692 pub fn visible_count(&self, start_idx: usize, viewport_height: u16) -> usize {
693 if self.len == 0 || viewport_height == 0 {
694 return 0;
695 }
696 let start = start_idx.min(self.len);
697 let start_offset = self.offset_of_item(start);
698 let end_offset = start_offset + u32::from(viewport_height);
699
700 let end_idx = self.find_item_at_offset(end_offset);
702
703 if end_idx > start {
705 let end_item_start = self.offset_of_item(end_idx);
707 if end_item_start + u32::from(self.get(end_idx)) <= end_offset {
708 end_idx - start + 1
709 } else {
710 end_idx - start
711 }
712 } else {
713 if viewport_height > 0 && start < self.len {
715 1
716 } else {
717 0
718 }
719 }
720 }
721
722 #[must_use]
724 pub fn total_height(&self) -> u32 {
725 self.tree.total()
726 }
727
728 pub fn resize(&mut self, new_len: usize) {
732 if new_len == self.len {
733 return;
734 }
735 self.tree.resize(new_len);
736 if new_len > self.len {
738 for i in self.len..new_len {
739 self.tree.set(i, u32::from(self.default_height));
740 }
741 }
742 self.len = new_len;
743 }
744
745 pub fn clear(&mut self) {
747 self.tree = FenwickTree::new(0);
748 self.len = 0;
749 }
750
751 pub fn rebuild(&mut self, heights: &[u16]) {
753 let heights_u32: Vec<u32> = heights.iter().map(|&h| u32::from(h)).collect();
754 self.tree = FenwickTree::from_values(&heights_u32);
755 self.len = heights.len();
756 }
757}
758
759pub trait RenderItem {
767 fn render(&self, area: Rect, frame: &mut Frame, selected: bool);
769
770 fn height(&self) -> u16 {
772 1
773 }
774}
775
776#[derive(Debug, Clone)]
778pub struct VirtualizedListState {
779 pub selected: Option<usize>,
781 scroll_offset: usize,
783 visible_count: usize,
785 overscan: usize,
787 follow_mode: bool,
789 scroll_velocity: f32,
791 persistence_id: Option<String>,
793}
794
795impl Default for VirtualizedListState {
796 fn default() -> Self {
797 Self::new()
798 }
799}
800
801impl VirtualizedListState {
802 #[must_use]
804 pub fn new() -> Self {
805 Self {
806 selected: None,
807 scroll_offset: 0,
808 visible_count: 0,
809 overscan: 2,
810 follow_mode: false,
811 scroll_velocity: 0.0,
812 persistence_id: None,
813 }
814 }
815
816 #[must_use]
818 pub fn with_overscan(mut self, overscan: usize) -> Self {
819 self.overscan = overscan;
820 self
821 }
822
823 #[must_use]
825 pub fn with_follow(mut self, follow: bool) -> Self {
826 self.follow_mode = follow;
827 self
828 }
829
830 #[must_use]
832 pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
833 self.persistence_id = Some(id.into());
834 self
835 }
836
837 #[must_use]
839 pub fn persistence_id(&self) -> Option<&str> {
840 self.persistence_id.as_deref()
841 }
842
843 #[must_use]
845 pub fn scroll_offset(&self) -> usize {
846 self.scroll_offset
847 }
848
849 #[must_use]
851 pub fn visible_count(&self) -> usize {
852 self.visible_count
853 }
854
855 pub fn scroll(&mut self, delta: i32, total_items: usize) {
857 if total_items == 0 {
858 return;
859 }
860 let max_offset = if self.visible_count > 0 {
861 total_items.saturating_sub(self.visible_count)
862 } else {
863 total_items.saturating_sub(1)
864 };
865 let new_offset = (self.scroll_offset as i64 + delta as i64)
866 .max(0)
867 .min(max_offset as i64);
868 self.scroll_offset = new_offset as usize;
869
870 if delta != 0 {
871 self.follow_mode = false;
872 }
873 }
874
875 pub fn scroll_to(&mut self, idx: usize, total_items: usize) {
877 self.scroll_offset = idx.min(total_items.saturating_sub(1));
878 self.follow_mode = false;
879 }
880
881 pub fn scroll_to_top(&mut self) {
883 self.scroll_offset = 0;
884 self.follow_mode = false;
885 }
886
887 pub fn scroll_to_bottom(&mut self, total_items: usize) {
889 if total_items > self.visible_count && self.visible_count > 0 {
890 self.scroll_offset = total_items - self.visible_count;
891 } else {
892 self.scroll_offset = 0;
893 }
894 }
895
896 pub fn page_up(&mut self, total_items: usize) {
898 if self.visible_count > 0 {
899 self.scroll(-(self.visible_count as i32), total_items);
900 }
901 }
902
903 pub fn page_down(&mut self, total_items: usize) {
905 if self.visible_count > 0 {
906 self.scroll(self.visible_count as i32, total_items);
907 }
908 }
909
910 pub fn select(&mut self, index: Option<usize>) {
912 self.selected = index;
913 }
914
915 pub fn select_previous(&mut self, total_items: usize) {
917 if total_items == 0 {
918 self.selected = None;
919 return;
920 }
921 self.selected = Some(match self.selected {
922 Some(i) if i > 0 => i - 1,
923 Some(_) => 0,
924 None => 0,
925 });
926 }
927
928 pub fn select_next(&mut self, total_items: usize) {
930 if total_items == 0 {
931 self.selected = None;
932 return;
933 }
934 self.selected = Some(match self.selected {
935 Some(i) if i < total_items - 1 => i + 1,
936 Some(i) => i,
937 None => 0,
938 });
939 }
940
941 #[must_use]
943 pub fn is_at_bottom(&self, total_items: usize) -> bool {
944 if total_items <= self.visible_count {
945 true
946 } else {
947 self.scroll_offset >= total_items - self.visible_count
948 }
949 }
950
951 pub fn set_follow(&mut self, follow: bool, total_items: usize) {
953 self.follow_mode = follow;
954 if follow {
955 self.scroll_to_bottom(total_items);
956 }
957 }
958
959 #[must_use]
961 pub fn follow_mode(&self) -> bool {
962 self.follow_mode
963 }
964
965 pub fn fling(&mut self, velocity: f32) {
967 self.scroll_velocity = velocity;
968 }
969
970 pub fn tick(&mut self, dt: Duration, total_items: usize) {
972 if self.scroll_velocity.abs() > 0.1 {
973 let delta = (self.scroll_velocity * dt.as_secs_f32()) as i32;
974 if delta != 0 {
975 self.scroll(delta, total_items);
976 }
977 self.scroll_velocity *= 0.95;
978 } else {
979 self.scroll_velocity = 0.0;
980 }
981 }
982}
983
984#[derive(Clone, Debug, Default, PartialEq)]
993#[cfg_attr(
994 feature = "state-persistence",
995 derive(serde::Serialize, serde::Deserialize)
996)]
997pub struct VirtualizedListPersistState {
998 pub selected: Option<usize>,
1000 pub scroll_offset: usize,
1002 pub follow_mode: bool,
1004}
1005
1006impl crate::stateful::Stateful for VirtualizedListState {
1007 type State = VirtualizedListPersistState;
1008
1009 fn state_key(&self) -> crate::stateful::StateKey {
1010 crate::stateful::StateKey::new(
1011 "VirtualizedList",
1012 self.persistence_id.as_deref().unwrap_or("default"),
1013 )
1014 }
1015
1016 fn save_state(&self) -> VirtualizedListPersistState {
1017 VirtualizedListPersistState {
1018 selected: self.selected,
1019 scroll_offset: self.scroll_offset,
1020 follow_mode: self.follow_mode,
1021 }
1022 }
1023
1024 fn restore_state(&mut self, state: VirtualizedListPersistState) {
1025 self.selected = state.selected;
1026 self.scroll_offset = state.scroll_offset;
1027 self.follow_mode = state.follow_mode;
1028 self.scroll_velocity = 0.0;
1030 }
1031}
1032
1033#[derive(Debug)]
1039pub struct VirtualizedList<'a, T> {
1040 items: &'a [T],
1042 style: Style,
1044 highlight_style: Style,
1046 show_scrollbar: bool,
1048 fixed_height: u16,
1050}
1051
1052impl<'a, T> VirtualizedList<'a, T> {
1053 #[must_use]
1055 pub fn new(items: &'a [T]) -> Self {
1056 Self {
1057 items,
1058 style: Style::default(),
1059 highlight_style: Style::default(),
1060 show_scrollbar: true,
1061 fixed_height: 1,
1062 }
1063 }
1064
1065 #[must_use]
1067 pub fn style(mut self, style: Style) -> Self {
1068 self.style = style;
1069 self
1070 }
1071
1072 #[must_use]
1074 pub fn highlight_style(mut self, style: Style) -> Self {
1075 self.highlight_style = style;
1076 self
1077 }
1078
1079 #[must_use]
1081 pub fn show_scrollbar(mut self, show: bool) -> Self {
1082 self.show_scrollbar = show;
1083 self
1084 }
1085
1086 #[must_use]
1088 pub fn fixed_height(mut self, height: u16) -> Self {
1089 self.fixed_height = height;
1090 self
1091 }
1092}
1093
1094impl<T: RenderItem> StatefulWidget for VirtualizedList<'_, T> {
1095 type State = VirtualizedListState;
1096
1097 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1098 #[cfg(feature = "tracing")]
1099 let _span = tracing::debug_span!(
1100 "widget_render",
1101 widget = "VirtualizedList",
1102 x = area.x,
1103 y = area.y,
1104 w = area.width,
1105 h = area.height,
1106 items = self.items.len()
1107 )
1108 .entered();
1109
1110 if area.is_empty() {
1111 return;
1112 }
1113
1114 set_style_area(&mut frame.buffer, area, self.style);
1116
1117 let total_items = self.items.len();
1118 if total_items == 0 {
1119 return;
1120 }
1121
1122 let items_per_viewport = (area.height / self.fixed_height.max(1)) as usize;
1124 let needs_scrollbar = self.show_scrollbar && total_items > items_per_viewport;
1125 let content_width = if needs_scrollbar {
1126 area.width.saturating_sub(1)
1127 } else {
1128 area.width
1129 };
1130
1131 if let Some(selected) = state.selected
1133 && selected >= total_items
1134 {
1135 state.selected = if total_items > 0 {
1137 Some(total_items - 1)
1138 } else {
1139 None
1140 };
1141 }
1142
1143 if let Some(selected) = state.selected {
1145 if selected >= state.scroll_offset + items_per_viewport {
1146 state.scroll_offset = selected.saturating_sub(items_per_viewport.saturating_sub(1));
1147 } else if selected < state.scroll_offset {
1148 state.scroll_offset = selected;
1149 }
1150 }
1151
1152 let max_offset = total_items.saturating_sub(items_per_viewport);
1154 state.scroll_offset = state.scroll_offset.min(max_offset);
1155
1156 state.visible_count = items_per_viewport.min(total_items);
1158
1159 let render_start = state.scroll_offset.saturating_sub(state.overscan);
1161 let render_end = state
1162 .scroll_offset
1163 .saturating_add(items_per_viewport)
1164 .saturating_add(state.overscan)
1165 .min(total_items);
1166
1167 for idx in render_start..render_end {
1169 let relative_idx = idx as i32 - state.scroll_offset as i32;
1171 let y_offset = relative_idx * self.fixed_height as i32;
1172
1173 if y_offset + self.fixed_height as i32 <= 0 {
1175 continue;
1176 }
1177
1178 if y_offset >= area.height as i32 {
1180 break;
1181 }
1182
1183 if i32::from(area.y) + y_offset < 0 {
1187 continue;
1188 }
1189
1190 let y = i32::from(area.y)
1193 .saturating_add(y_offset)
1194 .clamp(0, i32::from(u16::MAX)) as u16;
1195 if y >= area.bottom() {
1196 break;
1197 }
1198
1199 let visible_height = self.fixed_height.min(area.bottom().saturating_sub(y));
1200 if visible_height == 0 {
1201 continue;
1202 }
1203
1204 let row_area = Rect::new(area.x, y, content_width, visible_height);
1205
1206 let is_selected = state.selected == Some(idx);
1207
1208 if is_selected {
1210 set_style_area(&mut frame.buffer, row_area, self.highlight_style);
1211 }
1212
1213 self.items[idx].render(row_area, frame, is_selected);
1215 }
1216
1217 if needs_scrollbar {
1219 let scrollbar_area = Rect::new(area.right().saturating_sub(1), area.y, 1, area.height);
1220
1221 let mut scrollbar_state =
1222 ScrollbarState::new(total_items, state.scroll_offset, items_per_viewport);
1223
1224 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
1225 scrollbar.render(scrollbar_area, frame, &mut scrollbar_state);
1226 }
1227 }
1228}
1229
1230impl RenderItem for String {
1235 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1236 if area.is_empty() {
1237 return;
1238 }
1239 let max_chars = area.width as usize;
1240 for (i, ch) in self.chars().take(max_chars).enumerate() {
1241 frame
1242 .buffer
1243 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1244 }
1245 }
1246}
1247
1248impl RenderItem for &str {
1249 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1250 if area.is_empty() {
1251 return;
1252 }
1253 let max_chars = area.width as usize;
1254 for (i, ch) in self.chars().take(max_chars).enumerate() {
1255 frame
1256 .buffer
1257 .set(area.x.saturating_add(i as u16), area.y, Cell::from_char(ch));
1258 }
1259 }
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use super::*;
1265
1266 #[test]
1267 fn test_new_virtualized() {
1268 let virt: Virtualized<String> = Virtualized::new(100);
1269 assert_eq!(virt.len(), 0);
1270 assert!(virt.is_empty());
1271 }
1272
1273 #[test]
1274 fn test_push_and_len() {
1275 let mut virt: Virtualized<i32> = Virtualized::new(100);
1276 virt.push(1);
1277 virt.push(2);
1278 virt.push(3);
1279 assert_eq!(virt.len(), 3);
1280 assert!(!virt.is_empty());
1281 }
1282
1283 #[test]
1284 fn test_visible_range_fixed_height() {
1285 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(2);
1286 for i in 0..20 {
1287 virt.push(i);
1288 }
1289 let range = virt.visible_range(20);
1291 assert_eq!(range, 0..10);
1292 }
1293
1294 #[test]
1295 fn test_visible_range_variable_height_clamps() {
1296 let mut cache = HeightCache::new(1, 16);
1297 cache.set(0, 3);
1298 cache.set(1, 3);
1299 cache.set(2, 3);
1300 let mut virt: Virtualized<i32> =
1301 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1302 for i in 0..3 {
1303 virt.push(i);
1304 }
1305 let range = virt.visible_range(5);
1306 assert_eq!(range, 0..1);
1307 }
1308
1309 #[test]
1310 fn test_visible_range_variable_height_exact_fit() {
1311 let mut cache = HeightCache::new(1, 16);
1312 cache.set(0, 2);
1313 cache.set(1, 3);
1314 let mut virt: Virtualized<i32> =
1315 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1316 for i in 0..2 {
1317 virt.push(i);
1318 }
1319 let range = virt.visible_range(5);
1320 assert_eq!(range, 0..2);
1321 }
1322
1323 #[test]
1324 fn test_visible_range_with_scroll() {
1325 let mut virt: Virtualized<i32> = Virtualized::new(100).with_fixed_height(1);
1326 for i in 0..50 {
1327 virt.push(i);
1328 }
1329 virt.scroll(10);
1330 let range = virt.visible_range(10);
1331 assert_eq!(range, 10..20);
1332 }
1333
1334 #[test]
1335 fn test_visible_range_variable_height_excludes_partial() {
1336 let mut cache = HeightCache::new(1, 16);
1337 cache.set(0, 6);
1338 cache.set(1, 6);
1339 let mut virt: Virtualized<i32> =
1340 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1341 virt.push(1);
1342 virt.push(2);
1343 virt.push(3);
1344
1345 let range = virt.visible_range(10);
1346 assert_eq!(range, 0..1);
1347 }
1348
1349 #[test]
1350 fn test_visible_range_variable_height_exact_fit_larger() {
1351 let mut cache = HeightCache::new(1, 16);
1352 cache.set(0, 4);
1353 cache.set(1, 6);
1354 let mut virt: Virtualized<i32> =
1355 Virtualized::new(100).with_item_height(ItemHeight::Variable(cache));
1356 virt.push(1);
1357 virt.push(2);
1358 virt.push(3);
1359
1360 let range = virt.visible_range(10);
1361 assert_eq!(range, 0..2);
1362 }
1363
1364 #[test]
1365 fn test_visible_range_variable_height_default_for_unmeasured() {
1366 let cache = HeightCache::new(2, 16);
1367 let mut virt: Virtualized<i32> =
1368 Virtualized::new(10).with_item_height(ItemHeight::Variable(cache));
1369 for i in 0..3 {
1370 virt.push(i);
1371 }
1372
1373 let range = virt.visible_range(5);
1375 assert_eq!(range, 0..2);
1376 }
1377
1378 #[test]
1379 fn test_render_range_with_overscan() {
1380 let mut virt: Virtualized<i32> =
1381 Virtualized::new(100).with_fixed_height(1).with_overscan(2);
1382 for i in 0..50 {
1383 virt.push(i);
1384 }
1385 virt.scroll(10);
1386 let range = virt.render_range(10);
1387 assert_eq!(range, 8..22);
1390 }
1391
1392 #[test]
1393 fn test_scroll_bounds() {
1394 let mut virt: Virtualized<i32> = Virtualized::new(100);
1395 for i in 0..10 {
1396 virt.push(i);
1397 }
1398
1399 virt.scroll(-100);
1401 assert_eq!(virt.scroll_offset(), 0);
1402
1403 virt.scroll(100);
1405 assert_eq!(virt.scroll_offset(), 9);
1406 }
1407
1408 #[test]
1409 fn test_scroll_to() {
1410 let mut virt: Virtualized<i32> = Virtualized::new(100);
1411 for i in 0..20 {
1412 virt.push(i);
1413 }
1414
1415 virt.scroll_to(15);
1416 assert_eq!(virt.scroll_offset(), 15);
1417
1418 virt.scroll_to(100);
1420 assert_eq!(virt.scroll_offset(), 19);
1421 }
1422
1423 #[test]
1424 fn test_follow_mode() {
1425 let mut virt: Virtualized<i32> = Virtualized::new(100).with_follow(true);
1426 virt.set_visible_count(5);
1427
1428 for i in 0..10 {
1429 virt.push(i);
1430 }
1431
1432 assert!(virt.is_at_bottom());
1434
1435 virt.scroll(-5);
1437 assert!(!virt.follow_mode());
1438 }
1439
1440 #[test]
1441 fn test_scroll_to_start_and_end() {
1442 let mut virt: Virtualized<i32> = Virtualized::new(100);
1443 virt.set_visible_count(5);
1444 for i in 0..20 {
1445 virt.push(i);
1446 }
1447
1448 virt.scroll_to(10);
1450 virt.set_follow(true);
1451 virt.scroll_to_start();
1452 assert_eq!(virt.scroll_offset(), 0);
1453 assert!(!virt.follow_mode());
1454
1455 virt.scroll_to_end();
1457 assert!(virt.is_at_bottom());
1458 assert!(virt.follow_mode());
1459 }
1460
1461 #[test]
1462 fn test_virtualized_page_navigation() {
1463 let mut virt: Virtualized<i32> = Virtualized::new(100);
1464 virt.set_visible_count(5);
1465 for i in 0..30 {
1466 virt.push(i);
1467 }
1468
1469 virt.scroll_to(15);
1470 virt.page_up();
1471 assert_eq!(virt.scroll_offset(), 10);
1472
1473 virt.page_down();
1474 assert_eq!(virt.scroll_offset(), 15);
1475
1476 virt.scroll_to(2);
1478 virt.page_up();
1479 assert_eq!(virt.scroll_offset(), 0);
1480 }
1481
1482 #[test]
1483 fn test_height_cache() {
1484 let mut cache = HeightCache::new(1, 100);
1485
1486 assert_eq!(cache.get(0), 1);
1488 assert_eq!(cache.get(50), 1);
1489
1490 cache.set(5, 3);
1492 assert_eq!(cache.get(5), 3);
1493
1494 assert_eq!(cache.get(4), 1);
1496 assert_eq!(cache.get(6), 1);
1497 }
1498
1499 #[test]
1500 fn test_height_cache_large_index_window() {
1501 let mut cache = HeightCache::new(1, 8);
1502 cache.set(10_000, 4);
1503 assert_eq!(cache.get(10_000), 4);
1504 assert_eq!(cache.get(0), 1);
1505 assert!(cache.cache.len() <= cache.capacity);
1506 }
1507
1508 #[test]
1509 fn test_clear() {
1510 let mut virt: Virtualized<i32> = Virtualized::new(100);
1511 for i in 0..10 {
1512 virt.push(i);
1513 }
1514 virt.scroll(5);
1515
1516 virt.clear();
1517 assert_eq!(virt.len(), 0);
1518 assert_eq!(virt.scroll_offset(), 0);
1519 }
1520
1521 #[test]
1522 fn test_get_item() {
1523 let mut virt: Virtualized<String> = Virtualized::new(100);
1524 virt.push("hello".to_string());
1525 virt.push("world".to_string());
1526
1527 assert_eq!(virt.get(0), Some(&"hello".to_string()));
1528 assert_eq!(virt.get(1), Some(&"world".to_string()));
1529 assert_eq!(virt.get(2), None);
1530 }
1531
1532 #[test]
1533 fn test_external_storage_len() {
1534 let mut virt: Virtualized<i32> = Virtualized::external(1000, 100);
1535 assert_eq!(virt.len(), 1000);
1536
1537 virt.set_external_len(2000);
1538 assert_eq!(virt.len(), 2000);
1539 }
1540
1541 #[test]
1542 fn test_momentum_scrolling() {
1543 let mut virt: Virtualized<i32> = Virtualized::new(100);
1544 for i in 0..50 {
1545 virt.push(i);
1546 }
1547
1548 virt.fling(10.0);
1549
1550 virt.tick(Duration::from_millis(100));
1552
1553 assert!(virt.scroll_offset() > 0);
1555 }
1556
1557 #[test]
1562 fn test_virtualized_list_state_new() {
1563 let state = VirtualizedListState::new();
1564 assert_eq!(state.selected, None);
1565 assert_eq!(state.scroll_offset(), 0);
1566 assert_eq!(state.visible_count(), 0);
1567 }
1568
1569 #[test]
1570 fn test_virtualized_list_state_select_next() {
1571 let mut state = VirtualizedListState::new();
1572
1573 state.select_next(10);
1574 assert_eq!(state.selected, Some(0));
1575
1576 state.select_next(10);
1577 assert_eq!(state.selected, Some(1));
1578
1579 state.selected = Some(9);
1581 state.select_next(10);
1582 assert_eq!(state.selected, Some(9));
1583 }
1584
1585 #[test]
1586 fn test_virtualized_list_state_select_previous() {
1587 let mut state = VirtualizedListState::new();
1588 state.selected = Some(5);
1589
1590 state.select_previous(10);
1591 assert_eq!(state.selected, Some(4));
1592
1593 state.selected = Some(0);
1594 state.select_previous(10);
1595 assert_eq!(state.selected, Some(0));
1596 }
1597
1598 #[test]
1599 fn test_virtualized_list_state_scroll() {
1600 let mut state = VirtualizedListState::new();
1601
1602 state.scroll(5, 20);
1603 assert_eq!(state.scroll_offset(), 5);
1604
1605 state.scroll(-3, 20);
1606 assert_eq!(state.scroll_offset(), 2);
1607
1608 state.scroll(-100, 20);
1610 assert_eq!(state.scroll_offset(), 0);
1611
1612 state.scroll(100, 20);
1614 assert_eq!(state.scroll_offset(), 19);
1615 }
1616
1617 #[test]
1618 fn test_virtualized_list_state_follow_mode() {
1619 let mut state = VirtualizedListState::new().with_follow(true);
1620 assert!(state.follow_mode());
1621
1622 state.scroll(5, 20);
1624 assert!(!state.follow_mode());
1625 }
1626
1627 #[test]
1628 fn test_render_item_string() {
1629 let s = String::from("hello");
1631 assert_eq!(s.height(), 1);
1632 }
1633
1634 #[test]
1635 fn test_page_up_down() {
1636 let mut virt: Virtualized<i32> = Virtualized::new(100);
1637 for i in 0..50 {
1638 virt.push(i);
1639 }
1640 virt.set_visible_count(10);
1641
1642 assert_eq!(virt.scroll_offset(), 0);
1644
1645 virt.page_down();
1647 assert_eq!(virt.scroll_offset(), 10);
1648
1649 virt.page_down();
1651 assert_eq!(virt.scroll_offset(), 20);
1652
1653 virt.page_up();
1655 assert_eq!(virt.scroll_offset(), 10);
1656
1657 virt.page_up();
1659 assert_eq!(virt.scroll_offset(), 0);
1660
1661 virt.page_up();
1663 assert_eq!(virt.scroll_offset(), 0);
1664 }
1665
1666 #[test]
1671 fn test_render_scales_with_visible_not_total() {
1672 use ftui_render::grapheme_pool::GraphemePool;
1673 use std::time::Instant;
1674
1675 let small_items: Vec<String> = (0..1_000).map(|i| format!("Line {}", i)).collect();
1677 let small_list = VirtualizedList::new(&small_items);
1678 let mut small_state = VirtualizedListState::new();
1679
1680 let area = Rect::new(0, 0, 80, 24);
1681 let mut pool = GraphemePool::new();
1682 let mut frame = Frame::new(80, 24, &mut pool);
1683
1684 small_list.render(area, &mut frame, &mut small_state);
1686
1687 let start = Instant::now();
1688 for _ in 0..100 {
1689 frame.buffer.clear();
1690 small_list.render(area, &mut frame, &mut small_state);
1691 }
1692 let small_time = start.elapsed();
1693
1694 let large_items: Vec<String> = (0..100_000).map(|i| format!("Line {}", i)).collect();
1696 let large_list = VirtualizedList::new(&large_items);
1697 let mut large_state = VirtualizedListState::new();
1698
1699 large_list.render(area, &mut frame, &mut large_state);
1701
1702 let start = Instant::now();
1703 for _ in 0..100 {
1704 frame.buffer.clear();
1705 large_list.render(area, &mut frame, &mut large_state);
1706 }
1707 let large_time = start.elapsed();
1708
1709 assert!(
1711 large_time < small_time * 3,
1712 "Render does not scale O(visible): 1K={:?}, 100K={:?}",
1713 small_time,
1714 large_time
1715 );
1716 }
1717
1718 #[test]
1719 fn test_scroll_is_constant_time() {
1720 use std::time::Instant;
1721
1722 let mut small: Virtualized<i32> = Virtualized::new(1_000);
1723 for i in 0..1_000 {
1724 small.push(i);
1725 }
1726 small.set_visible_count(24);
1727
1728 let mut large: Virtualized<i32> = Virtualized::new(100_000);
1729 for i in 0..100_000 {
1730 large.push(i);
1731 }
1732 large.set_visible_count(24);
1733
1734 let iterations = 10_000;
1735
1736 let start = Instant::now();
1737 for _ in 0..iterations {
1738 small.scroll(1);
1739 small.scroll(-1);
1740 }
1741 let small_time = start.elapsed();
1742
1743 let start = Instant::now();
1744 for _ in 0..iterations {
1745 large.scroll(1);
1746 large.scroll(-1);
1747 }
1748 let large_time = start.elapsed();
1749
1750 assert!(
1752 large_time < small_time * 3,
1753 "Scroll is not O(1): 1K={:?}, 100K={:?}",
1754 small_time,
1755 large_time
1756 );
1757 }
1758
1759 #[test]
1760 fn render_partially_offscreen_top_skips_item() {
1761 use ftui_render::grapheme_pool::GraphemePool;
1762
1763 struct IndexedItem(usize);
1765 impl RenderItem for IndexedItem {
1766 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1767 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1768 for y in area.y..area.bottom() {
1769 frame.buffer.set(area.x, y, Cell::from_char(ch));
1770 }
1771 }
1772 fn height(&self) -> u16 {
1773 2
1774 }
1775 }
1776
1777 let items = vec![
1780 IndexedItem(0),
1781 IndexedItem(1),
1782 IndexedItem(2),
1783 IndexedItem(3),
1784 ];
1785 let list = VirtualizedList::new(&items).fixed_height(2);
1786
1787 let mut state = VirtualizedListState::new().with_overscan(1);
1789 state.scroll_offset = 1; let mut pool = GraphemePool::new();
1792 let mut frame = Frame::new(10, 5, &mut pool);
1793
1794 list.render(Rect::new(0, 0, 10, 5), &mut frame, &mut state);
1796
1797 let cell = frame.buffer.get(0, 0).unwrap();
1805 assert_eq!(cell.content.as_char(), Some('1'));
1806 }
1807
1808 #[test]
1809 fn render_bottom_boundary_clips_partial_item() {
1810 use ftui_render::grapheme_pool::GraphemePool;
1811
1812 struct IndexedItem(u16);
1813 impl RenderItem for IndexedItem {
1814 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1815 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1816 for y in area.y..area.bottom() {
1817 frame.buffer.set(area.x, y, Cell::from_char(ch));
1818 }
1819 }
1820 fn height(&self) -> u16 {
1821 2
1822 }
1823 }
1824
1825 let items = vec![IndexedItem(0), IndexedItem(1), IndexedItem(2)];
1826 let list = VirtualizedList::new(&items)
1827 .fixed_height(2)
1828 .show_scrollbar(false);
1829 let mut state = VirtualizedListState::new();
1830
1831 let mut pool = GraphemePool::new();
1832 let mut frame = Frame::new(4, 4, &mut pool);
1833
1834 list.render(Rect::new(0, 0, 4, 3), &mut frame, &mut state);
1836
1837 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1838 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('0'));
1839 assert_eq!(frame.buffer.get(0, 2).unwrap().content.as_char(), Some('1'));
1840 assert_eq!(frame.buffer.get(0, 3).unwrap().content.as_char(), None);
1842 }
1843
1844 #[test]
1845 fn render_after_fling_advances_visible_rows() {
1846 use ftui_render::grapheme_pool::GraphemePool;
1847
1848 struct IndexedItem(u16);
1849 impl RenderItem for IndexedItem {
1850 fn render(&self, area: Rect, frame: &mut Frame, _selected: bool) {
1851 let ch = char::from_digit(self.0 as u32, 10).unwrap();
1852 for y in area.y..area.bottom() {
1853 frame.buffer.set(area.x, y, Cell::from_char(ch));
1854 }
1855 }
1856 }
1857
1858 let items: Vec<IndexedItem> = (0..10).map(IndexedItem).collect();
1859 let list = VirtualizedList::new(&items)
1860 .fixed_height(1)
1861 .show_scrollbar(false);
1862 let mut state = VirtualizedListState::new();
1863
1864 let mut pool = GraphemePool::new();
1865 let mut frame = Frame::new(4, 3, &mut pool);
1866 let area = Rect::new(0, 0, 4, 3);
1867
1868 list.render(area, &mut frame, &mut state);
1870 assert_eq!(state.scroll_offset(), 0);
1871 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('0'));
1872
1873 state.fling(40.0);
1875 state.tick(Duration::from_millis(100), items.len());
1876 assert_eq!(state.scroll_offset(), 4);
1877
1878 frame.buffer.clear();
1879 list.render(area, &mut frame, &mut state);
1880 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('4'));
1881 }
1882
1883 #[test]
1884 fn test_memory_bounded_by_ring_capacity() {
1885 use crate::log_ring::LogRing;
1886
1887 let mut ring: LogRing<String> = LogRing::new(1_000);
1888
1889 for i in 0..100_000 {
1891 ring.push(format!("Line {}", i));
1892 }
1893
1894 assert_eq!(ring.len(), 1_000);
1896 assert_eq!(ring.total_count(), 100_000);
1897 assert_eq!(ring.first_index(), 99_000);
1898
1899 assert!(ring.get(99_999).is_some());
1901 assert!(ring.get(99_000).is_some());
1902 assert!(ring.get(0).is_none());
1904 assert!(ring.get(98_999).is_none());
1905 }
1906
1907 #[test]
1908 fn test_visible_range_constant_regardless_of_total() {
1909 let mut small: Virtualized<i32> = Virtualized::new(100);
1910 for i in 0..100 {
1911 small.push(i);
1912 }
1913 let small_range = small.visible_range(24);
1914
1915 let mut large: Virtualized<i32> = Virtualized::new(100_000);
1916 for i in 0..100_000 {
1917 large.push(i);
1918 }
1919 let large_range = large.visible_range(24);
1920
1921 assert_eq!(small_range.end - small_range.start, 24);
1923 assert_eq!(large_range.end - large_range.start, 24);
1924 }
1925
1926 #[test]
1927 fn test_virtualized_list_state_page_up_down() {
1928 let mut state = VirtualizedListState::new();
1929 state.visible_count = 10;
1930
1931 state.page_down(50);
1933 assert_eq!(state.scroll_offset(), 10);
1934
1935 state.page_down(50);
1937 assert_eq!(state.scroll_offset(), 20);
1938
1939 state.page_up(50);
1941 assert_eq!(state.scroll_offset(), 10);
1942
1943 state.page_up(50);
1945 assert_eq!(state.scroll_offset(), 0);
1946 }
1947
1948 #[test]
1953 fn test_variable_heights_fenwick_new() {
1954 let tracker = VariableHeightsFenwick::new(2, 10);
1955 assert_eq!(tracker.len(), 10);
1956 assert!(!tracker.is_empty());
1957 assert_eq!(tracker.default_height(), 2);
1958 }
1959
1960 #[test]
1961 fn test_variable_heights_fenwick_empty() {
1962 let tracker = VariableHeightsFenwick::new(1, 0);
1963 assert!(tracker.is_empty());
1964 assert_eq!(tracker.total_height(), 0);
1965 }
1966
1967 #[test]
1968 fn test_variable_heights_fenwick_from_heights() {
1969 let heights = vec![3, 2, 5, 1, 4];
1970 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
1971
1972 assert_eq!(tracker.len(), 5);
1973 assert_eq!(tracker.get(0), 3);
1974 assert_eq!(tracker.get(1), 2);
1975 assert_eq!(tracker.get(2), 5);
1976 assert_eq!(tracker.get(3), 1);
1977 assert_eq!(tracker.get(4), 4);
1978 assert_eq!(tracker.total_height(), 15);
1979 }
1980
1981 #[test]
1982 fn test_variable_heights_fenwick_offset_of_item() {
1983 let heights = vec![3, 2, 5, 1, 4];
1985 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
1986
1987 assert_eq!(tracker.offset_of_item(0), 0);
1988 assert_eq!(tracker.offset_of_item(1), 3);
1989 assert_eq!(tracker.offset_of_item(2), 5);
1990 assert_eq!(tracker.offset_of_item(3), 10);
1991 assert_eq!(tracker.offset_of_item(4), 11);
1992 assert_eq!(tracker.offset_of_item(5), 15); }
1994
1995 #[test]
1996 fn test_variable_heights_fenwick_find_item_at_offset() {
1997 let heights = vec![3, 2, 5, 1, 4];
1999 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2000
2001 assert_eq!(tracker.find_item_at_offset(0), 0);
2003 assert_eq!(tracker.find_item_at_offset(1), 0);
2005 assert_eq!(tracker.find_item_at_offset(3), 1);
2007 assert_eq!(tracker.find_item_at_offset(5), 2);
2009 assert_eq!(tracker.find_item_at_offset(10), 3);
2011 assert_eq!(tracker.find_item_at_offset(11), 4);
2013 assert_eq!(tracker.find_item_at_offset(15), 5);
2015 }
2016
2017 #[test]
2018 fn test_variable_heights_fenwick_visible_count() {
2019 let heights = vec![3, 2, 5, 1, 4];
2021 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2022
2023 assert_eq!(tracker.visible_count(0, 5), 2);
2025
2026 assert_eq!(tracker.visible_count(0, 4), 1);
2028
2029 assert_eq!(tracker.visible_count(0, 10), 3);
2031
2032 assert_eq!(tracker.visible_count(2, 6), 2);
2034 }
2035
2036 #[test]
2037 fn test_variable_heights_fenwick_set() {
2038 let mut tracker = VariableHeightsFenwick::new(1, 5);
2039
2040 assert_eq!(tracker.get(0), 1);
2042 assert_eq!(tracker.total_height(), 5);
2043
2044 tracker.set(2, 10);
2046 assert_eq!(tracker.get(2), 10);
2047 assert_eq!(tracker.total_height(), 14); }
2049
2050 #[test]
2051 fn test_variable_heights_fenwick_resize() {
2052 let mut tracker = VariableHeightsFenwick::new(2, 3);
2053 assert_eq!(tracker.len(), 3);
2054 assert_eq!(tracker.total_height(), 6);
2055
2056 tracker.resize(5);
2058 assert_eq!(tracker.len(), 5);
2059 assert_eq!(tracker.total_height(), 10);
2060 assert_eq!(tracker.get(4), 2);
2061
2062 tracker.resize(2);
2064 assert_eq!(tracker.len(), 2);
2065 assert_eq!(tracker.total_height(), 4);
2066 }
2067
2068 #[test]
2069 fn test_virtualized_with_variable_heights_fenwick() {
2070 let mut virt: Virtualized<i32> = Virtualized::new(100).with_variable_heights_fenwick(2, 10);
2071
2072 for i in 0..10 {
2073 virt.push(i);
2074 }
2075
2076 let range = virt.visible_range(6);
2078 assert_eq!(range.end - range.start, 3);
2079 }
2080
2081 #[test]
2082 fn test_variable_heights_fenwick_performance() {
2083 use std::time::Instant;
2084
2085 let n = 100_000;
2087 let heights: Vec<u16> = (0..n).map(|i| (i % 10 + 1) as u16).collect();
2088 let tracker = VariableHeightsFenwick::from_heights(&heights, 1);
2089
2090 let _ = tracker.find_item_at_offset(500_000);
2092 let _ = tracker.offset_of_item(50_000);
2093
2094 let start = Instant::now();
2096 let mut _sink = 0usize;
2097 for i in 0..10_000 {
2098 _sink = _sink.wrapping_add(tracker.find_item_at_offset((i * 50) as u32));
2099 }
2100 let find_time = start.elapsed();
2101
2102 let start = Instant::now();
2104 let mut _sink2 = 0u32;
2105 for i in 0..10_000 {
2106 _sink2 = _sink2.wrapping_add(tracker.offset_of_item((i * 10) % n));
2107 }
2108 let offset_time = start.elapsed();
2109
2110 eprintln!("=== VariableHeightsFenwick Performance (n={n}) ===");
2111 eprintln!("10k find_item_at_offset: {:?}", find_time);
2112 eprintln!("10k offset_of_item: {:?}", offset_time);
2113
2114 assert!(
2116 find_time < std::time::Duration::from_millis(50),
2117 "find_item_at_offset too slow: {:?}",
2118 find_time
2119 );
2120 assert!(
2121 offset_time < std::time::Duration::from_millis(50),
2122 "offset_of_item too slow: {:?}",
2123 offset_time
2124 );
2125 }
2126
2127 #[test]
2128 fn test_variable_heights_fenwick_scales_logarithmically() {
2129 use std::time::Instant;
2130
2131 let small_n = 1_000;
2133 let small_heights: Vec<u16> = (0..small_n).map(|i| (i % 5 + 1) as u16).collect();
2134 let small_tracker = VariableHeightsFenwick::from_heights(&small_heights, 1);
2135
2136 let large_n = 100_000;
2138 let large_heights: Vec<u16> = (0..large_n).map(|i| (i % 5 + 1) as u16).collect();
2139 let large_tracker = VariableHeightsFenwick::from_heights(&large_heights, 1);
2140
2141 let iterations = 5_000;
2142
2143 let start = Instant::now();
2145 for i in 0..iterations {
2146 let _ = small_tracker.find_item_at_offset((i * 2) as u32);
2147 }
2148 let small_time = start.elapsed();
2149
2150 let start = Instant::now();
2152 for i in 0..iterations {
2153 let _ = large_tracker.find_item_at_offset((i * 200) as u32);
2154 }
2155 let large_time = start.elapsed();
2156
2157 assert!(
2159 large_time < small_time * 5,
2160 "Not O(log n): small={:?}, large={:?}",
2161 small_time,
2162 large_time
2163 );
2164 }
2165}