Skip to main content

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}