1use std::collections::HashSet;
14use std::sync::Arc;
15
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19use crate::data::{absolute_path, get_pointer};
20
21pub const DEFAULT_ESTIMATED_ITEM_HEIGHT: f32 = 40.0;
23pub const DEFAULT_OVERSCAN: usize = crate::layout::WINDOW_OVERSCAN as usize;
25pub const WINDOW_THRESHOLD: usize = crate::layout::WINDOW_THRESHOLD as usize;
28
29fn js_json(v: &Value) -> String {
35 serde_json::to_string(&crate::json::canonical(v)).unwrap_or_default()
36}
37
38fn key_string(v: Option<&Value>) -> Option<String> {
41 match v {
42 None | Some(Value::Null) => None,
43 Some(Value::String(s)) if s.is_empty() => None,
44 Some(v @ (Value::Object(_) | Value::Array(_))) => Some(js_json(v)),
45 Some(v) => Some(crate::json::to_js_string(v)),
46 }
47}
48
49pub fn instance_segment(key: &str) -> String {
53 if !key.contains(['~', '.']) {
54 return key.to_string();
55 }
56 key.replace('~', "~0").replace('.', "~1")
57}
58
59pub fn template_item_keys(items: &[Value], key_pointer: Option<&str>) -> Vec<String> {
64 if let Some(p) = key_pointer {
68 let pointer = if p.starts_with('/') { std::borrow::Cow::Borrowed(p) } else { std::borrow::Cow::Owned(format!("/{p}")) };
69 let own: Option<Vec<String>> = items.iter().map(|item| key_string(get_pointer(item, &pointer))).collect();
70 if let Some(own) = own {
71 let mut distinct: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::with_capacity_and_hasher(own.len(), Default::default());
72 if own.iter().all(|k| distinct.insert(k.as_str())) {
73 drop(distinct);
74 return own;
75 }
76 }
77 }
78 let mut seen: HashSet<String> = HashSet::with_capacity(items.len());
79 let pointer = key_pointer.map(|k| if k.starts_with('/') { k.to_string() } else { format!("/{k}") });
80 items
81 .iter()
82 .enumerate()
83 .map(|(index, item)| {
84 let mut key = index.to_string();
85 if let Some(p) = &pointer {
86 key = match key_string(get_pointer(item, p)) {
87 Some(own) if !seen.contains(&own) => own,
88 _ => format!("#{index}"),
89 };
90 while seen.contains(&key) {
91 key = format!("#{key}");
92 }
93 }
94 seen.insert(key.clone());
95 key
96 })
97 .collect()
98}
99
100pub fn table_row_keys(rows: &[Value], row_key: &str) -> Vec<String> {
102 let token = row_key.replace('~', "~0").replace('/', "~1");
103 template_item_keys(rows, Some(&format!("/{token}")))
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct TemplateInstance {
109 pub key: String,
111 pub path: String,
113 pub index: usize,
114 pub instance: String,
118}
119
120pub fn template_instances(data: &Value, template: &crate::types::Template, scope: &str, instance: &str) -> Vec<TemplateInstance> {
125 let path = absolute_path(&template.path, scope);
126 let Some(list) = get_pointer(data, &path).and_then(Value::as_array) else { return Vec::new() };
127 let keys = template_item_keys(list, template.key.as_deref());
128 keys.into_iter().enumerate().map(|(index, key)| TemplateInstance { path: format!("{path}/{index}"), index, instance: format!("{instance}.{}", instance_segment(&key)), key }).collect()
129}
130
131pub fn item_extents(measured: &[Option<f64>], row: f64) -> Vec<f64> {
141 let (sum, n) = measured.iter().flatten().fold((0.0, 0usize), |(s, n), m| (s + m, n + 1));
142 let estimate = if n > 0 { sum / n as f64 } else { row };
143 measured.iter().map(|m| m.unwrap_or(estimate)).collect()
144}
145
146pub fn item_offsets(extents: &[f64], gap: f64) -> Vec<f64> {
150 let mut out = Vec::with_capacity(extents.len() + 1);
151 let mut at = 0.0;
152 for (i, e) in extents.iter().enumerate() {
153 out.push(at);
154 at += e + if i + 1 < extents.len() { gap } else { 0.0 };
155 }
156 out.push(at);
157 out
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
163pub struct WindowRange {
164 pub start: usize,
165 pub end: usize,
166 pub before: f64,
167 pub after: f64,
168 pub total: f64,
169}
170
171pub fn virtual_window(extents: &[f64], gap: f64, scroll: f64, viewport: f64, overscan: usize) -> WindowRange {
176 let count = extents.len();
177 let off = item_offsets(extents, gap);
178 let total = off[count];
179 let (mut lo, mut hi) = (0, count);
181 while lo < hi {
182 let mid = (lo + hi) / 2;
183 if off[mid] + extents[mid] < scroll {
184 lo = mid + 1;
185 } else {
186 hi = mid;
187 }
188 }
189 let start = lo;
190 let end = start + off[start..count].partition_point(|&o| o < scroll + viewport);
191 let start = start.saturating_sub(overscan);
192 let end = (end + overscan).min(count);
193 if end <= start {
194 return WindowRange { start, end: start, before: 0.0, after: total, total };
195 }
196 WindowRange { start, end, before: off[start], after: total - (off[end - 1] + extents[end - 1]), total }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
201#[serde(rename_all = "lowercase")]
202pub enum ScrollAlign {
203 Start,
204 Center,
205 End,
206 #[default]
207 Nearest,
208}
209
210impl ScrollAlign {
211 pub fn parse(s: &str) -> Option<ScrollAlign> {
212 Some(match s {
213 "start" => ScrollAlign::Start,
214 "center" => ScrollAlign::Center,
215 "end" => ScrollAlign::End,
216 "nearest" => ScrollAlign::Nearest,
217 _ => return None,
218 })
219 }
220}
221
222#[allow(clippy::too_many_arguments)]
227pub fn scroll_offset_for_index(extents: &[f64], gap: f64, index: usize, viewport: f64, scroll: f64, align: ScrollAlign, inset: f64) -> f64 {
228 if index >= extents.len() {
229 return scroll;
230 }
231 let off = item_offsets(extents, gap);
232 scroll_offset_at(off[index], extents[index], off[extents.len()], viewport, scroll, align, inset)
233}
234
235pub fn scroll_offset_at(top: f64, size: f64, total: f64, viewport: f64, scroll: f64, align: ScrollAlign, inset: f64) -> f64 {
238 let next = match align {
239 ScrollAlign::Start => top - inset,
240 ScrollAlign::End => top + size - viewport,
241 ScrollAlign::Center => top + size / 2.0 - (viewport + inset) / 2.0,
242 ScrollAlign::Nearest if top - inset < scroll => top - inset,
243 ScrollAlign::Nearest if top + size > scroll + viewport => top + size - viewport,
244 ScrollAlign::Nearest => scroll,
245 };
246 next.min((total - viewport).max(0.0)).max(0.0)
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct ListSection {
256 pub value: String,
258 pub start: usize,
260 pub count: usize,
261}
262
263pub fn list_sections(items: &[Value], section_by: &str) -> Vec<ListSection> {
266 let pointer = if section_by.starts_with('/') { section_by.to_string() } else { format!("/{section_by}") };
267 let mut out: Vec<ListSection> = Vec::new();
268 for (index, item) in items.iter().enumerate() {
269 let value = match get_pointer(item, &pointer) {
270 None | Some(Value::Null) => String::new(),
271 Some(v @ (Value::Object(_) | Value::Array(_))) => js_json(v),
272 Some(v) => crate::json::to_js_string(v),
273 };
274 match out.last_mut() {
275 Some(last) if last.value == value => last.count += 1,
276 _ => out.push(ListSection { value, start: index, count: 1 }),
277 }
278 }
279 out
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "lowercase")]
285pub enum SectionRow {
286 Header(usize),
287 Item(usize),
288}
289
290pub fn section_rows(sections: &[ListSection]) -> Vec<SectionRow> {
293 let mut rows = Vec::with_capacity(sections.iter().map(|s| s.count + 1).sum());
294 for (i, s) in sections.iter().enumerate() {
295 rows.push(SectionRow::Header(i));
296 rows.extend((0..s.count).map(|k| SectionRow::Item(s.start + k)));
297 }
298 rows
299}
300
301#[allow(clippy::too_many_arguments)]
307pub fn scroll_offset_for_item(rows: &[SectionRow], row_extents: &[f64], gap: f64, data_index: usize, viewport: f64, scroll: f64, align: ScrollAlign, sticky_headers: bool) -> f64 {
308 let Some(row) = rows.iter().position(|r| *r == SectionRow::Item(data_index)) else { return scroll };
309 let header = rows[..row].iter().rposition(|r| matches!(r, SectionRow::Header(_)));
310 let inset = match header {
311 Some(h) if sticky_headers => row_extents.get(h).copied().unwrap_or(0.0),
312 _ => 0.0,
313 };
314 scroll_offset_for_index(row_extents, gap, row, viewport, scroll, align, inset)
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
320pub struct StickyHeader {
321 pub row: usize,
322 pub offset: f64,
323}
324
325pub fn sticky_header(row_offsets: &[f64], row_extents: &[f64], header_rows: &[usize], scroll: f64) -> Option<StickyHeader> {
330 sticky_header_by(|r| row_offsets[r], |r| row_extents[r], header_rows, scroll)
331}
332
333pub fn sticky_header_by(offset: impl Fn(usize) -> f64, extent: impl Fn(usize) -> f64, header_rows: &[usize], scroll: f64) -> Option<StickyHeader> {
336 let pinned = header_rows.partition_point(|&h| offset(h) <= scroll);
337 let pinned = pinned.checked_sub(1)?;
338 let row = header_rows[pinned];
339 let at = match header_rows.get(pinned + 1) {
340 Some(&next) => scroll.min(offset(next) - extent(row)),
341 None => scroll,
342 };
343 Some(StickyHeader { row, offset: at })
344}
345
346#[derive(Debug, Clone)]
347pub struct ListWindow {
348 heights: rustc_hash::FxHashMap<String, f32>,
350 measured_sum: f64,
353 pub estimated_item_height: f32,
354 pub overscan: usize,
355 pub gap: f32,
356 pub scroll_offset: f32,
357 version: u64,
359 cache: Option<OffsetsCache>,
361}
362
363#[derive(Debug, Clone)]
365struct OffsetsCache {
366 keys: usize,
367 len: usize,
368 version: u64,
369 gap: f32,
370 estimate: f32,
371 offsets: Arc<Vec<f32>>,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
375pub struct VisibleRange {
376 pub start: usize,
378 pub end: usize,
380}
381
382impl ListWindow {
383 pub fn new(estimated_item_height: f32, overscan: usize, gap: f32) -> ListWindow {
384 ListWindow { heights: Default::default(), measured_sum: 0.0, estimated_item_height, overscan, gap, scroll_offset: 0.0, version: 0, cache: None }
385 }
386
387 pub fn record(&mut self, key: &str, height: f32) -> bool {
389 match self.heights.get(key) {
390 Some(h) if (h - height).abs() < 0.001 => false,
391 old => {
392 self.measured_sum += (height - old.copied().unwrap_or(0.0)) as f64;
393 self.heights.insert(key.to_string(), height);
394 self.version += 1;
395 true
396 }
397 }
398 }
399
400 pub fn estimate(&self) -> f32 {
405 self.measured_mean().unwrap_or(self.estimated_item_height)
406 }
407
408 pub fn measured_mean(&self) -> Option<f32> {
410 (!self.heights.is_empty()).then(|| (self.measured_sum / self.heights.len() as f64) as f32)
411 }
412
413 pub fn extents<'a>(&self, keys: impl Iterator<Item = &'a str>) -> Vec<f64> {
415 let estimate = self.estimate();
416 keys.map(|k| self.heights.get(k).copied().unwrap_or(estimate) as f64).collect()
417 }
418
419 pub fn height_of(&self, key: &str) -> f32 {
420 self.heights.get(key).copied().unwrap_or_else(|| self.estimate())
421 }
422
423 pub fn forget(&mut self, key: &str) {
424 if let Some(h) = self.heights.remove(key) {
425 self.measured_sum -= h as f64;
426 self.version += 1;
427 }
428 }
429
430 pub fn clear(&mut self) {
431 self.heights.clear();
432 self.measured_sum = 0.0;
433 self.version += 1;
434 }
435
436 pub fn offsets_cached(&mut self, keys: &Arc<Vec<String>>) -> Arc<Vec<f32>> {
439 let id = Arc::as_ptr(keys) as usize;
440 if let Some(c) = &self.cache {
441 if c.keys == id && c.len == keys.len() && c.version == self.version && c.gap == self.gap && c.estimate == self.estimate() {
442 return c.offsets.clone();
443 }
444 }
445 let offsets = Arc::new(self.offsets(keys.iter().map(String::as_str)));
446 self.cache = Some(OffsetsCache { keys: id, len: keys.len(), version: self.version, gap: self.gap, estimate: self.estimate(), offsets: offsets.clone() });
447 offsets
448 }
449
450 pub fn sticky_header_cached(&mut self, keys: &Arc<Vec<String>>, header_rows: &[usize], scroll: f64) -> Option<(StickyHeader, f64)> {
456 let offsets = self.offsets_cached(keys);
457 let count = keys.len();
458 let gap = self.gap as f64;
459 let headers = &header_rows[..header_rows.partition_point(|&h| h < count)];
460 let offset = |r: usize| offsets[r] as f64;
461 let extent = |r: usize| offsets[r + 1] as f64 - offsets[r] as f64 - if r + 1 < count { gap } else { 0.0 };
462 sticky_header_by(offset, extent, headers, scroll).map(|pin| (pin, offset(pin.row)))
463 }
464
465 pub fn offsets<'a>(&self, keys: impl Iterator<Item = &'a str>) -> Vec<f32> {
469 let mut out = Vec::new();
470 let mut y = 0.0f32;
471 let mut first = true;
472 let estimate = self.estimate();
473 for key in keys {
474 if !first {
475 y += self.gap;
476 }
477 first = false;
478 out.push(y);
479 y += self.heights.get(key).copied().unwrap_or(estimate);
480 }
481 out.push(y);
482 out
483 }
484
485 pub fn content_height<'a>(&self, keys: impl Iterator<Item = &'a str>) -> f32 {
487 self.offsets(keys).last().copied().unwrap_or(0.0)
488 }
489
490 pub fn visible_range(&self, offsets: &[f32], viewport: f32) -> VisibleRange {
496 let count = offsets.len().saturating_sub(1);
497 if count == 0 {
498 return VisibleRange::default();
499 }
500 let top = self.scroll_offset.max(0.0);
501 let bottom = top + viewport.max(0.0);
502 let end_of = |i: usize| if i + 1 < count { offsets[i + 1] - self.gap } else { offsets[count] };
504 let (mut lo, mut hi) = (0, count);
505 while lo < hi {
506 let mid = (lo + hi) / 2;
507 if end_of(mid) < top {
508 lo = mid + 1;
509 } else {
510 hi = mid;
511 }
512 }
513 let first = lo;
514 let last = first + offsets[first..count].partition_point(|&o| o < bottom);
515 let start = first.saturating_sub(self.overscan);
516 let end = (last + self.overscan).min(count);
517 VisibleRange { start: start.min(end), end }
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn unmeasured_items_take_row_then_the_measured_mean() {
527 assert_eq!(item_extents(&[None, None], 36.0), vec![36.0, 36.0]);
528 assert_eq!(item_extents(&[Some(40.0), None, Some(60.0)], 36.0), vec![40.0, 50.0, 60.0]);
529 let keys = ["a", "b", "c"];
531 let mut w = ListWindow::new(36.0, 5, 0.0);
532 assert_eq!(w.extents(keys.iter().copied()), vec![36.0; 3]);
533 w.record("a", 40.0);
534 w.record("c", 60.0);
535 assert_eq!(w.extents(keys.iter().copied()), item_extents(&[Some(40.0), None, Some(60.0)], 36.0));
536 }
537
538 #[test]
539 fn window_is_constant_per_scroll_step() {
540 let keys: Vec<String> = (0..10_000).map(|i| format!("k{i}")).collect();
541 let mut w = ListWindow::new(40.0, 5, 0.0);
542 w.record("k3", 80.0);
543 let offsets = w.offsets(keys.iter().map(String::as_str));
544 assert_eq!(offsets.len(), 10_001);
545 assert_eq!(offsets[10_000], 80.0 * 10_000.0, "unmeasured rows take the measured mean");
546 w.scroll_offset = 0.0;
547 let r = w.visible_range(&offsets, 800.0);
548 assert_eq!(r.start, 0);
549 assert!(r.end <= 20 && r.end >= 10, "{r:?}");
550 w.scroll_offset = 200_000.0;
551 let r = w.visible_range(&offsets, 800.0);
552 assert!(r.end - r.start <= 2 * 5 + 22);
553 assert!(offsets[r.start] <= 200_000.0);
554 assert!(offsets[r.end.min(9_999)] >= 200_000.0 + 800.0 - 40.0 * 6.0 || r.end == 10_000);
555 let extents = w.extents(keys.iter().map(String::as_str));
557 let shared = virtual_window(&extents, 0.0, 200_000.0, 800.0, 5);
558 assert_eq!((r.start, r.end), (shared.start, shared.end));
559 }
560}