use std::collections::HashSet;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::data::{absolute_path, get_pointer};
pub const DEFAULT_ESTIMATED_ITEM_HEIGHT: f32 = 40.0;
pub const DEFAULT_OVERSCAN: usize = crate::layout::WINDOW_OVERSCAN as usize;
pub const WINDOW_THRESHOLD: usize = crate::layout::WINDOW_THRESHOLD as usize;
fn js_json(v: &Value) -> String {
serde_json::to_string(&crate::json::canonical(v)).unwrap_or_default()
}
fn key_string(v: Option<&Value>) -> Option<String> {
match v {
None | Some(Value::Null) => None,
Some(Value::String(s)) if s.is_empty() => None,
Some(v @ (Value::Object(_) | Value::Array(_))) => Some(js_json(v)),
Some(v) => Some(crate::json::to_js_string(v)),
}
}
pub fn instance_segment(key: &str) -> String {
if !key.contains(['~', '.']) {
return key.to_string();
}
key.replace('~', "~0").replace('.', "~1")
}
pub fn template_item_keys(items: &[Value], key_pointer: Option<&str>) -> Vec<String> {
if let Some(p) = key_pointer {
let pointer = if p.starts_with('/') { std::borrow::Cow::Borrowed(p) } else { std::borrow::Cow::Owned(format!("/{p}")) };
let own: Option<Vec<String>> = items.iter().map(|item| key_string(get_pointer(item, &pointer))).collect();
if let Some(own) = own {
let mut distinct: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::with_capacity_and_hasher(own.len(), Default::default());
if own.iter().all(|k| distinct.insert(k.as_str())) {
drop(distinct);
return own;
}
}
}
let mut seen: HashSet<String> = HashSet::with_capacity(items.len());
let pointer = key_pointer.map(|k| if k.starts_with('/') { k.to_string() } else { format!("/{k}") });
items
.iter()
.enumerate()
.map(|(index, item)| {
let mut key = index.to_string();
if let Some(p) = &pointer {
key = match key_string(get_pointer(item, p)) {
Some(own) if !seen.contains(&own) => own,
_ => format!("#{index}"),
};
while seen.contains(&key) {
key = format!("#{key}");
}
}
seen.insert(key.clone());
key
})
.collect()
}
pub fn table_row_keys(rows: &[Value], row_key: &str) -> Vec<String> {
let token = row_key.replace('~', "~0").replace('/', "~1");
template_item_keys(rows, Some(&format!("/{token}")))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TemplateInstance {
pub key: String,
pub path: String,
pub index: usize,
pub instance: String,
}
pub fn template_instances(data: &Value, template: &crate::types::Template, scope: &str, instance: &str) -> Vec<TemplateInstance> {
let path = absolute_path(&template.path, scope);
let Some(list) = get_pointer(data, &path).and_then(Value::as_array) else { return Vec::new() };
let keys = template_item_keys(list, template.key.as_deref());
keys.into_iter().enumerate().map(|(index, key)| TemplateInstance { path: format!("{path}/{index}"), index, instance: format!("{instance}.{}", instance_segment(&key)), key }).collect()
}
pub fn item_extents(measured: &[Option<f64>], row: f64) -> Vec<f64> {
let (sum, n) = measured.iter().flatten().fold((0.0, 0usize), |(s, n), m| (s + m, n + 1));
let estimate = if n > 0 { sum / n as f64 } else { row };
measured.iter().map(|m| m.unwrap_or(estimate)).collect()
}
pub fn item_offsets(extents: &[f64], gap: f64) -> Vec<f64> {
let mut out = Vec::with_capacity(extents.len() + 1);
let mut at = 0.0;
for (i, e) in extents.iter().enumerate() {
out.push(at);
at += e + if i + 1 < extents.len() { gap } else { 0.0 };
}
out.push(at);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct WindowRange {
pub start: usize,
pub end: usize,
pub before: f64,
pub after: f64,
pub total: f64,
}
pub fn virtual_window(extents: &[f64], gap: f64, scroll: f64, viewport: f64, overscan: usize) -> WindowRange {
let count = extents.len();
let off = item_offsets(extents, gap);
let total = off[count];
let (mut lo, mut hi) = (0, count);
while lo < hi {
let mid = (lo + hi) / 2;
if off[mid] + extents[mid] < scroll {
lo = mid + 1;
} else {
hi = mid;
}
}
let start = lo;
let end = start + off[start..count].partition_point(|&o| o < scroll + viewport);
let start = start.saturating_sub(overscan);
let end = (end + overscan).min(count);
if end <= start {
return WindowRange { start, end: start, before: 0.0, after: total, total };
}
WindowRange { start, end, before: off[start], after: total - (off[end - 1] + extents[end - 1]), total }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScrollAlign {
Start,
Center,
End,
#[default]
Nearest,
}
impl ScrollAlign {
pub fn parse(s: &str) -> Option<ScrollAlign> {
Some(match s {
"start" => ScrollAlign::Start,
"center" => ScrollAlign::Center,
"end" => ScrollAlign::End,
"nearest" => ScrollAlign::Nearest,
_ => return None,
})
}
}
#[allow(clippy::too_many_arguments)]
pub fn scroll_offset_for_index(extents: &[f64], gap: f64, index: usize, viewport: f64, scroll: f64, align: ScrollAlign, inset: f64) -> f64 {
if index >= extents.len() {
return scroll;
}
let off = item_offsets(extents, gap);
scroll_offset_at(off[index], extents[index], off[extents.len()], viewport, scroll, align, inset)
}
pub fn scroll_offset_at(top: f64, size: f64, total: f64, viewport: f64, scroll: f64, align: ScrollAlign, inset: f64) -> f64 {
let next = match align {
ScrollAlign::Start => top - inset,
ScrollAlign::End => top + size - viewport,
ScrollAlign::Center => top + size / 2.0 - (viewport + inset) / 2.0,
ScrollAlign::Nearest if top - inset < scroll => top - inset,
ScrollAlign::Nearest if top + size > scroll + viewport => top + size - viewport,
ScrollAlign::Nearest => scroll,
};
next.min((total - viewport).max(0.0)).max(0.0)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListSection {
pub value: String,
pub start: usize,
pub count: usize,
}
pub fn list_sections(items: &[Value], section_by: &str) -> Vec<ListSection> {
let pointer = if section_by.starts_with('/') { section_by.to_string() } else { format!("/{section_by}") };
let mut out: Vec<ListSection> = Vec::new();
for (index, item) in items.iter().enumerate() {
let value = match get_pointer(item, &pointer) {
None | Some(Value::Null) => String::new(),
Some(v @ (Value::Object(_) | Value::Array(_))) => js_json(v),
Some(v) => crate::json::to_js_string(v),
};
match out.last_mut() {
Some(last) if last.value == value => last.count += 1,
_ => out.push(ListSection { value, start: index, count: 1 }),
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SectionRow {
Header(usize),
Item(usize),
}
pub fn section_rows(sections: &[ListSection]) -> Vec<SectionRow> {
let mut rows = Vec::with_capacity(sections.iter().map(|s| s.count + 1).sum());
for (i, s) in sections.iter().enumerate() {
rows.push(SectionRow::Header(i));
rows.extend((0..s.count).map(|k| SectionRow::Item(s.start + k)));
}
rows
}
#[allow(clippy::too_many_arguments)]
pub 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 {
let Some(row) = rows.iter().position(|r| *r == SectionRow::Item(data_index)) else { return scroll };
let header = rows[..row].iter().rposition(|r| matches!(r, SectionRow::Header(_)));
let inset = match header {
Some(h) if sticky_headers => row_extents.get(h).copied().unwrap_or(0.0),
_ => 0.0,
};
scroll_offset_for_index(row_extents, gap, row, viewport, scroll, align, inset)
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StickyHeader {
pub row: usize,
pub offset: f64,
}
pub fn sticky_header(row_offsets: &[f64], row_extents: &[f64], header_rows: &[usize], scroll: f64) -> Option<StickyHeader> {
sticky_header_by(|r| row_offsets[r], |r| row_extents[r], header_rows, scroll)
}
pub fn sticky_header_by(offset: impl Fn(usize) -> f64, extent: impl Fn(usize) -> f64, header_rows: &[usize], scroll: f64) -> Option<StickyHeader> {
let pinned = header_rows.partition_point(|&h| offset(h) <= scroll);
let pinned = pinned.checked_sub(1)?;
let row = header_rows[pinned];
let at = match header_rows.get(pinned + 1) {
Some(&next) => scroll.min(offset(next) - extent(row)),
None => scroll,
};
Some(StickyHeader { row, offset: at })
}
#[derive(Debug, Clone)]
pub struct ListWindow {
heights: rustc_hash::FxHashMap<String, f32>,
measured_sum: f64,
pub estimated_item_height: f32,
pub overscan: usize,
pub gap: f32,
pub scroll_offset: f32,
version: u64,
cache: Option<OffsetsCache>,
}
#[derive(Debug, Clone)]
struct OffsetsCache {
keys: usize,
len: usize,
version: u64,
gap: f32,
estimate: f32,
offsets: Arc<Vec<f32>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VisibleRange {
pub start: usize,
pub end: usize,
}
impl ListWindow {
pub fn new(estimated_item_height: f32, overscan: usize, gap: f32) -> ListWindow {
ListWindow { heights: Default::default(), measured_sum: 0.0, estimated_item_height, overscan, gap, scroll_offset: 0.0, version: 0, cache: None }
}
pub fn record(&mut self, key: &str, height: f32) -> bool {
match self.heights.get(key) {
Some(h) if (h - height).abs() < 0.001 => false,
old => {
self.measured_sum += (height - old.copied().unwrap_or(0.0)) as f64;
self.heights.insert(key.to_string(), height);
self.version += 1;
true
}
}
}
pub fn estimate(&self) -> f32 {
self.measured_mean().unwrap_or(self.estimated_item_height)
}
pub fn measured_mean(&self) -> Option<f32> {
(!self.heights.is_empty()).then(|| (self.measured_sum / self.heights.len() as f64) as f32)
}
pub fn extents<'a>(&self, keys: impl Iterator<Item = &'a str>) -> Vec<f64> {
let estimate = self.estimate();
keys.map(|k| self.heights.get(k).copied().unwrap_or(estimate) as f64).collect()
}
pub fn height_of(&self, key: &str) -> f32 {
self.heights.get(key).copied().unwrap_or_else(|| self.estimate())
}
pub fn forget(&mut self, key: &str) {
if let Some(h) = self.heights.remove(key) {
self.measured_sum -= h as f64;
self.version += 1;
}
}
pub fn clear(&mut self) {
self.heights.clear();
self.measured_sum = 0.0;
self.version += 1;
}
pub fn offsets_cached(&mut self, keys: &Arc<Vec<String>>) -> Arc<Vec<f32>> {
let id = Arc::as_ptr(keys) as usize;
if let Some(c) = &self.cache {
if c.keys == id && c.len == keys.len() && c.version == self.version && c.gap == self.gap && c.estimate == self.estimate() {
return c.offsets.clone();
}
}
let offsets = Arc::new(self.offsets(keys.iter().map(String::as_str)));
self.cache = Some(OffsetsCache { keys: id, len: keys.len(), version: self.version, gap: self.gap, estimate: self.estimate(), offsets: offsets.clone() });
offsets
}
pub fn sticky_header_cached(&mut self, keys: &Arc<Vec<String>>, header_rows: &[usize], scroll: f64) -> Option<(StickyHeader, f64)> {
let offsets = self.offsets_cached(keys);
let count = keys.len();
let gap = self.gap as f64;
let headers = &header_rows[..header_rows.partition_point(|&h| h < count)];
let offset = |r: usize| offsets[r] as f64;
let extent = |r: usize| offsets[r + 1] as f64 - offsets[r] as f64 - if r + 1 < count { gap } else { 0.0 };
sticky_header_by(offset, extent, headers, scroll).map(|pin| (pin, offset(pin.row)))
}
pub fn offsets<'a>(&self, keys: impl Iterator<Item = &'a str>) -> Vec<f32> {
let mut out = Vec::new();
let mut y = 0.0f32;
let mut first = true;
let estimate = self.estimate();
for key in keys {
if !first {
y += self.gap;
}
first = false;
out.push(y);
y += self.heights.get(key).copied().unwrap_or(estimate);
}
out.push(y);
out
}
pub fn content_height<'a>(&self, keys: impl Iterator<Item = &'a str>) -> f32 {
self.offsets(keys).last().copied().unwrap_or(0.0)
}
pub fn visible_range(&self, offsets: &[f32], viewport: f32) -> VisibleRange {
let count = offsets.len().saturating_sub(1);
if count == 0 {
return VisibleRange::default();
}
let top = self.scroll_offset.max(0.0);
let bottom = top + viewport.max(0.0);
let end_of = |i: usize| if i + 1 < count { offsets[i + 1] - self.gap } else { offsets[count] };
let (mut lo, mut hi) = (0, count);
while lo < hi {
let mid = (lo + hi) / 2;
if end_of(mid) < top {
lo = mid + 1;
} else {
hi = mid;
}
}
let first = lo;
let last = first + offsets[first..count].partition_point(|&o| o < bottom);
let start = first.saturating_sub(self.overscan);
let end = (last + self.overscan).min(count);
VisibleRange { start: start.min(end), end }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unmeasured_items_take_row_then_the_measured_mean() {
assert_eq!(item_extents(&[None, None], 36.0), vec![36.0, 36.0]);
assert_eq!(item_extents(&[Some(40.0), None, Some(60.0)], 36.0), vec![40.0, 50.0, 60.0]);
let keys = ["a", "b", "c"];
let mut w = ListWindow::new(36.0, 5, 0.0);
assert_eq!(w.extents(keys.iter().copied()), vec![36.0; 3]);
w.record("a", 40.0);
w.record("c", 60.0);
assert_eq!(w.extents(keys.iter().copied()), item_extents(&[Some(40.0), None, Some(60.0)], 36.0));
}
#[test]
fn window_is_constant_per_scroll_step() {
let keys: Vec<String> = (0..10_000).map(|i| format!("k{i}")).collect();
let mut w = ListWindow::new(40.0, 5, 0.0);
w.record("k3", 80.0);
let offsets = w.offsets(keys.iter().map(String::as_str));
assert_eq!(offsets.len(), 10_001);
assert_eq!(offsets[10_000], 80.0 * 10_000.0, "unmeasured rows take the measured mean");
w.scroll_offset = 0.0;
let r = w.visible_range(&offsets, 800.0);
assert_eq!(r.start, 0);
assert!(r.end <= 20 && r.end >= 10, "{r:?}");
w.scroll_offset = 200_000.0;
let r = w.visible_range(&offsets, 800.0);
assert!(r.end - r.start <= 2 * 5 + 22);
assert!(offsets[r.start] <= 200_000.0);
assert!(offsets[r.end.min(9_999)] >= 200_000.0 + 800.0 - 40.0 * 6.0 || r.end == 10_000);
let extents = w.extents(keys.iter().map(String::as_str));
let shared = virtual_window(&extents, 0.0, 200_000.0, 800.0, 5);
assert_eq!((r.start, r.end), (shared.start, shared.end));
}
}