oxiui_table/height_cache.rs
1//! Prefix-sum cumulative height cache with O(log n) binary search for row-by-offset lookup.
2//! Row caching: bounded LRU cache for last-N materialized rows.
3
4use crate::Cell;
5use std::collections::VecDeque;
6
7/// Prefix-sum cumulative height cache.
8///
9/// Stores per-row heights and lazily rebuilds a prefix-sum array whenever
10/// the heights are mutated. All lookup operations are O(log n) via binary search.
11pub struct CumulativeHeightCache {
12 row_heights: Vec<f32>,
13 /// `prefix_sums[0]` = 0; `prefix_sums[i]` = sum of `row_heights[0..i]`.
14 prefix_sums: Vec<f32>,
15 dirty: bool,
16}
17
18impl CumulativeHeightCache {
19 /// Create an empty, dirty cache.
20 pub fn new() -> Self {
21 Self {
22 row_heights: Vec::new(),
23 prefix_sums: Vec::new(),
24 dirty: true,
25 }
26 }
27
28 /// Replace all row heights with `heights`. Marks the cache dirty.
29 pub fn set_heights(&mut self, heights: Vec<f32>) {
30 self.row_heights = heights;
31 self.dirty = true;
32 }
33
34 /// Set `row_count` rows all to the same `height`. Marks the cache dirty.
35 pub fn set_uniform_height(&mut self, row_count: usize, height: f32) {
36 self.row_heights = vec![height; row_count];
37 self.dirty = true;
38 }
39
40 /// Mark the cache dirty so the prefix-sum array is rebuilt on the next access.
41 pub fn mark_dirty(&mut self) {
42 self.dirty = true;
43 }
44
45 fn rebuild(&mut self) {
46 let n = self.row_heights.len();
47 self.prefix_sums = Vec::with_capacity(n + 1);
48 self.prefix_sums.push(0.0_f32);
49 let mut acc = 0.0_f32;
50 for &h in &self.row_heights {
51 acc += h;
52 self.prefix_sums.push(acc);
53 }
54 self.dirty = false;
55 }
56
57 fn ensure_built(&mut self) {
58 if self.dirty {
59 self.rebuild();
60 }
61 }
62
63 /// Find the row index whose vertical span contains offset `y`. O(log n).
64 ///
65 /// Returns `0` when the cache is empty or `y < 0`.
66 pub fn row_at_offset(&mut self, y: f32) -> usize {
67 self.ensure_built();
68 if self.prefix_sums.is_empty() {
69 return 0;
70 }
71 // `partition_point` returns the first index where `prefix_sums[idx] > y`.
72 // Subtracting one gives the row whose top edge is at or before `y`.
73 let idx = self.prefix_sums.partition_point(|&ps| ps <= y);
74 let max_row = self.row_heights.len().saturating_sub(1);
75 idx.saturating_sub(1).min(max_row)
76 }
77
78 /// Return the `(top, bottom)` Y-coordinate range for `row`.
79 pub fn row_y_range(&mut self, row: usize) -> (f32, f32) {
80 self.ensure_built();
81 let start = self.prefix_sums.get(row).copied().unwrap_or(0.0);
82 let end = self.prefix_sums.get(row + 1).copied().unwrap_or(start);
83 (start, end)
84 }
85
86 /// Return the range of row indices at least partially visible in the viewport
87 /// `[viewport_y, viewport_y + viewport_height)`.
88 pub fn visible_range(
89 &mut self,
90 viewport_y: f32,
91 viewport_height: f32,
92 ) -> std::ops::Range<usize> {
93 self.ensure_built();
94 let start = self.row_at_offset(viewport_y);
95 let end_y = viewport_y + viewport_height;
96 let end_row = self.row_at_offset(end_y);
97 // Include any partially-visible row at the bottom edge.
98 let end = (end_row + 1).min(self.row_heights.len());
99 start..end
100 }
101
102 /// Total height of all rows in logical pixels.
103 pub fn total_height(&mut self) -> f32 {
104 self.ensure_built();
105 self.prefix_sums.last().copied().unwrap_or(0.0)
106 }
107
108 /// Number of rows in the cache.
109 pub fn row_count(&self) -> usize {
110 self.row_heights.len()
111 }
112}
113
114impl Default for CumulativeHeightCache {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120// ── RowCache ─────────────────────────────────────────────────────────────────
121
122/// Bounded LRU row cache: stores the last `max` materialized rows, keyed by
123/// row index, to avoid re-fetching identical rows from a `RowSource`.
124///
125/// The eviction policy is LRU-approximate: the oldest-inserted entry is
126/// evicted when the cache is full. Entries are also bumped to the back of
127/// the deque on re-insertion (cache-hit update).
128pub struct RowCache {
129 data: VecDeque<(usize, Vec<Cell>)>,
130 max: usize,
131}
132
133impl RowCache {
134 /// Create a new cache that holds at most `max` rows.
135 ///
136 /// Setting `max` to `0` effectively disables caching (every lookup misses).
137 pub fn new(max: usize) -> Self {
138 Self {
139 data: VecDeque::new(),
140 max,
141 }
142 }
143
144 /// Look up a row by index. Returns `None` on a cache miss.
145 pub fn get(&self, index: usize) -> Option<&Vec<Cell>> {
146 self.data
147 .iter()
148 .find(|(i, _)| *i == index)
149 .map(|(_, cells)| cells)
150 }
151
152 /// Insert or update a row in the cache.
153 ///
154 /// If an entry for `index` already exists it is removed first so the
155 /// refreshed entry goes to the back (most-recently-used position). When
156 /// the cache is at capacity the front (least-recently-used) entry is
157 /// evicted before insertion.
158 pub fn insert(&mut self, index: usize, cells: Vec<Cell>) {
159 if self.max == 0 {
160 return;
161 }
162 // Remove any stale entry for this row index.
163 self.data.retain(|(i, _)| *i != index);
164 // Evict the oldest entry if we are at capacity.
165 if self.data.len() >= self.max {
166 self.data.pop_front();
167 }
168 self.data.push_back((index, cells));
169 }
170
171 /// Invalidate the entire cache. Call when the underlying `RowSource` changes.
172 pub fn invalidate(&mut self) {
173 self.data.clear();
174 }
175
176 /// Number of currently cached rows.
177 pub fn len(&self) -> usize {
178 self.data.len()
179 }
180
181 /// Returns `true` when the cache holds no rows.
182 pub fn is_empty(&self) -> bool {
183 self.data.is_empty()
184 }
185}