use crate::tree::{RNode, try_with_tree, with_tree};
use day_reactive::Scope;
use day_spec::{ListSource, RawHandle, props::RowHeight};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
pub struct ListDriver {
pub row_height: RowHeight,
pub track_viewport: bool,
pub len: Box<dyn Fn() -> usize>,
pub token_at: Box<dyn Fn(usize) -> u64>,
pub build: Box<dyn Fn(usize, RNode) -> BuiltRow>,
pub reorder: Option<ListReorderDriver>,
pub delete: Option<ListDeleteDriver>,
pub swipe: Option<ListSwipeDriver>,
pub sections: Option<ListSectionsDriver>,
}
pub struct ListSectionsDriver {
pub key_at: Box<dyn Fn(usize) -> u64>,
pub build: Box<dyn Fn(usize, RNode) -> BuiltRow>,
}
pub struct ListReorderDriver {
pub can_move: Box<dyn Fn(usize, usize) -> i64>,
pub moved: Box<dyn Fn(usize, usize)>,
}
pub struct ListDeleteDriver {
pub can_delete: Box<dyn Fn(usize) -> bool>,
pub deleted: Box<dyn Fn(usize)>,
}
pub struct ListSwipeDriver {
pub actions_at: Box<dyn Fn(usize, day_spec::SwipeEdge) -> Vec<day_spec::ListSwipeAction>>,
pub performed: Box<dyn Fn(usize, day_spec::SwipeEdge, usize)>,
}
pub struct BuiltRow {
pub scope: Scope,
pub rebind: Rc<dyn Fn(usize)>,
}
pub(crate) struct BoundCell {
pub anchor: RNode,
pub active: bool,
pub scope: Scope,
pub rebind: Rc<dyn Fn(usize)>,
pub native_width: Option<f64>,
pub laid_width: Option<f64>,
pub laid_height: Option<f64>,
pub header: bool,
pub pinned: bool,
}
pub(crate) struct ListState {
pub driver: Rc<ListDriver>,
pub cells: HashMap<usize, BoundCell>,
pub sections: Option<Rc<SectionMap>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DisplayRow {
Header(usize),
Item(usize),
}
pub(crate) struct SectionMap {
driver: Rc<ListDriver>,
rows: RefCell<Vec<DisplayRow>>,
display: RefCell<Vec<usize>>,
heads: RefCell<Vec<(usize, u64)>>,
stale: Cell<bool>,
}
impl SectionMap {
fn new(driver: Rc<ListDriver>) -> Self {
SectionMap {
driver,
rows: RefCell::new(Vec::new()),
display: RefCell::new(Vec::new()),
heads: RefCell::new(Vec::new()),
stale: Cell::new(true),
}
}
pub(crate) fn invalidate(&self) {
self.stale.set(true);
}
fn ensure(&self) {
let driver = &self.driver;
let n = (driver.len)();
if !self.stale.get() && self.display.borrow().len() == n {
return;
}
let Some(sections) = driver.sections.as_ref() else {
return;
};
let mut rows = Vec::with_capacity(n + n / 4 + 1);
let mut display = Vec::with_capacity(n);
let mut heads = Vec::new();
let mut seen: HashMap<u64, u64> = HashMap::new();
let mut previous = None;
for i in 0..n {
let key = (sections.key_at)(i);
if previous != Some(key) {
let nth = seen.entry(key).or_default();
heads.push((rows.len(), Self::header_token(key, *nth)));
*nth += 1;
rows.push(DisplayRow::Header(i));
previous = Some(key);
}
display.push(rows.len());
rows.push(DisplayRow::Item(i));
}
*self.rows.borrow_mut() = rows;
*self.display.borrow_mut() = display;
*self.heads.borrow_mut() = heads;
self.stale.set(false);
}
pub(crate) fn len(&self) -> usize {
self.ensure();
self.rows.borrow().len()
}
pub(crate) fn row(&self, display: usize) -> Option<DisplayRow> {
self.ensure();
self.rows.borrow().get(display).copied()
}
pub(crate) fn item_of(&self, display: usize) -> Option<usize> {
match self.row(display)? {
DisplayRow::Item(i) => Some(i),
DisplayRow::Header(_) => None,
}
}
pub(crate) fn nearest_item(&self, display: usize) -> Option<usize> {
match self.row(display)? {
DisplayRow::Item(i) | DisplayRow::Header(i) => Some(i),
}
}
pub(crate) fn display_of(&self, item: usize) -> Option<usize> {
self.ensure();
self.display.borrow().get(item).copied()
}
pub(crate) fn span(&self, display: usize) -> Option<SectionSpan> {
self.item_of(display)?;
let heads = self.heads.borrow();
let section = heads
.partition_point(|(h, _)| *h <= display)
.checked_sub(1)?;
let last = match heads.get(section + 1) {
Some((next, _)) => next - 1,
None => self.rows.borrow().len() - 1,
};
Some(SectionSpan {
first: heads[section].0 + 1,
last,
headers: section + 1,
})
}
pub(crate) fn header_token_at(&self, display: usize) -> Option<u64> {
self.ensure();
let heads = self.heads.borrow();
let at = heads.binary_search_by_key(&display, |(h, _)| *h).ok()?;
Some(heads[at].1)
}
fn header_token(key: u64, nth: u64) -> u64 {
(key ^ nth.wrapping_mul(0x9E37_79B9_7F4A_7C15)).rotate_left(29) ^ 0xD1B5_4A32_D192_ED03
}
fn move_within(&self, from: usize, to: usize) -> Option<(SectionSpan, usize, usize)> {
let span = self.span(from)?;
let to = to.clamp(span.first, span.last);
Some((span, from - span.headers, to - span.headers))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct SectionSpan {
pub first: usize,
pub last: usize,
pub headers: usize,
}
impl SectionSpan {
fn display_of(&self, item: usize) -> Option<usize> {
let display = item + self.headers;
(self.first..=self.last)
.contains(&display)
.then_some(display)
}
}
day_reactive::tls_slots! {
list;
static SECTION_MAPS: RefCell<HashMap<day_spec::NodeId, std::rc::Weak<SectionMap>>> =
RefCell::new(HashMap::new());
static SCRIPTED_REORDERS: RefCell<HashMap<day_spec::NodeId, ScriptedReorder>> =
RefCell::new(HashMap::new());
}
fn section_map(node: RNode) -> Option<Rc<SectionMap>> {
let id = crate::tree::rnode_to_id(node);
SECTION_MAPS.with(|m| m.borrow().get(&id).and_then(std::rc::Weak::upgrade))
}
pub(crate) fn register_sections(node: RNode, driver: Rc<ListDriver>) -> Rc<SectionMap> {
let map = Rc::new(SectionMap::new(driver));
SECTION_MAPS.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, w| w.strong_count() > 0);
m.insert(crate::tree::rnode_to_id(node), Rc::downgrade(&map));
});
map
}
pub(crate) fn translate_native_event(
id: day_spec::NodeId,
ev: day_spec::Event,
) -> Option<day_spec::Event> {
use day_spec::Event;
if !matches!(
ev,
Event::SelectionChanged(_) | Event::SelectionSet(_) | Event::ListActivated(_)
) {
return Some(ev);
}
let Some(map) = SECTION_MAPS.with(|m| m.borrow().get(&id).and_then(std::rc::Weak::upgrade))
else {
return Some(ev);
};
match ev {
Event::SelectionChanged(i) if i >= 0 => map
.item_of(i as usize)
.map(|i| Event::SelectionChanged(i as i64)),
Event::SelectionSet(rows) => Some(Event::SelectionSet(
rows.iter()
.filter(|r| **r >= 0)
.filter_map(|r| map.item_of(*r as usize))
.map(|i| i as i64)
.collect(),
)),
Event::ListActivated(i) => map.item_of(i).map(Event::ListActivated),
other => Some(other),
}
}
pub enum CellStep {
Build {
anchor: RNode,
},
Rebind {
rebind: Rc<dyn Fn(usize)>,
anchor: RNode,
},
}
pub fn install_list(node: RNode, driver: ListDriver) {
with_tree(|t| t.install_list(node, driver));
}
pub fn list_reload(node: RNode) {
with_tree(|t| t.list_reload(node));
}
pub fn list_splice(node: RNode, deltas: Vec<day_spec::props::RowDelta>) {
with_tree(|t| t.list_splice(node, deltas));
}
pub fn list_scroll_to_end(node: RNode) {
with_tree(|t| t.list_scroll_to_end(node));
}
pub fn list_scroll_to_row(node: RNode, row: usize) {
with_tree(|t| t.list_scroll_to_row(node, row));
}
pub fn list_set_selected(node: RNode, rows: Vec<usize>) {
with_tree(|t| t.list_set_selected(node, rows));
}
pub fn list_set_editing(node: RNode, on: bool) {
with_tree(|t| t.list_set_editing(node, on));
}
pub fn list_try_reorder(node: RNode, from: usize, to: usize) -> Result<usize, &'static str> {
let hook = SCRIPTED_REORDERS.with(|m| m.borrow().get(&crate::tree::rnode_to_id(node)).cloned());
if let Some(hook) = hook {
return hook(from, to);
}
let driver = with_tree(|t| t.list_driver(node)).ok_or("no list at this node")?;
let Some(re) = driver.reorder.as_ref() else {
return Err("list is not reorderable");
};
let len = (driver.len)();
if from >= len || to >= len {
return Err("row index out of bounds");
}
let span = section_map(node).and_then(|map| {
let span = map.span(map.display_of(from)?)?;
Some((map, span))
});
let to = match &span {
Some((map, span)) => {
let display = map.display_of(to).unwrap_or(span.last);
display.clamp(span.first, span.last) - span.headers
}
None => to,
};
let accepted = (re.can_move)(from, to);
if accepted < 0 {
return Err("reorder denied by the guard");
}
let accepted = (accepted as usize).min(len - 1);
if let Some((_, span)) = &span
&& span.display_of(accepted).is_none()
{
return Err("reorder would leave the row's section");
}
if accepted != from {
(re.moved)(from, accepted);
}
list_reload(node);
Ok(accepted)
}
type ScriptedReorder = Rc<dyn Fn(usize, usize) -> Result<usize, &'static str>>;
pub fn set_scripted_reorder(
node: RNode,
reorder: impl Fn(usize, usize) -> Result<usize, &'static str> + 'static,
) {
let id = crate::tree::rnode_to_id(node);
SCRIPTED_REORDERS.with(|m| m.borrow_mut().insert(id, Rc::new(reorder)));
}
pub fn list_try_delete(node: RNode, index: usize) -> Result<(), &'static str> {
let driver = with_tree(|t| t.list_driver(node)).ok_or("no list at this node")?;
let Some(del) = driver.delete.as_ref() else {
return Err("list is not deletable");
};
if index >= (driver.len)() {
return Err("row index out of bounds");
}
if !(del.can_delete)(index) {
return Err("delete denied by the guard");
}
(del.deleted)(index);
list_reload(node);
Ok(())
}
pub fn list_try_swipe(
node: RNode,
index: usize,
edge: day_spec::SwipeEdge,
action: usize,
expect: Option<&str>,
) -> Result<String, String> {
let driver = with_tree(|t| t.list_driver(node)).ok_or("no list at this node")?;
let Some(sw) = driver.swipe.as_ref() else {
return Err("list has no swipe actions".into());
};
if index >= (driver.len)() {
return Err("row index out of bounds".into());
}
let offer = (sw.actions_at)(index, edge);
let Some(a) = offer.get(action) else {
return Err("no such action in this row's offer".into());
};
if let Some(want) = expect
&& a.label != want
{
return Err(format!(
"the offer holds {:?}, expected {want:?} — not pressed",
a.label
));
}
let label = a.label.clone();
(sw.performed)(index, edge, action);
Ok(label)
}
fn driver_viewport(
node: RNode,
driver: &ListDriver,
sections: Option<Rc<SectionMap>>,
) -> Option<Rc<dyn Fn(usize)>> {
if !driver.track_viewport {
return None;
}
let previous = std::cell::Cell::new(None);
Some(Rc::new(move |row| {
let row = match §ions {
Some(map) => match map.nearest_item(row) {
Some(item) => item,
None => return,
},
None => row,
};
if previous.replace(Some(row)) != Some(row) {
crate::tree::enqueue_event(
crate::tree::rnode_to_id(node),
day_spec::Event::Custom {
tag: "day-list-first-visible",
num: row as f64,
text: String::new(),
},
);
}
}))
}
pub(crate) fn make_source(
node: RNode,
driver: Rc<ListDriver>,
sections: Option<Rc<SectionMap>>,
) -> ListSource {
let row_of = {
let sections = sections.clone();
move |display: usize| match §ions {
Some(map) => map.row(display),
None => Some(DisplayRow::Item(display)),
}
};
let item_of = {
let row_of = row_of.clone();
move |display: usize| match row_of(display) {
Some(DisplayRow::Item(i)) => Some(i),
_ => None,
}
};
let first_visible = driver_viewport(node, &driver, sections.clone());
let sectioned = sections.is_some();
ListSource {
first_visible,
len: {
let (d, sections) = (driver.clone(), sections.clone());
Rc::new(move || match §ions {
Some(map) => map.len(),
None => (d.len)(),
})
},
token_at: {
let (d, row_of, sections) = (driver.clone(), row_of.clone(), sections.clone());
Rc::new(move |i| match row_of(i) {
Some(DisplayRow::Item(i)) => (d.token_at)(i),
Some(DisplayRow::Header(first)) => sections
.as_ref()
.and_then(|map| map.header_token_at(i))
.unwrap_or_else(|| (d.token_at)(first)),
None => (d.token_at)(i),
})
},
is_header: sections.as_ref().map(|map| {
let map = map.clone();
Rc::new(move |i| matches!(map.row(i), Some(DisplayRow::Header(_))))
as Rc<dyn Fn(usize) -> bool>
}),
bind_row: bind_with(node, driver.clone(), row_of.clone(), false),
bind_pinned: bind_with(node, driver.clone(), row_of.clone(), true),
layout_cell: Rc::new(move |cell, width| {
let _ = try_with_tree(|t| t.list_layout_cell_width(node, cell as usize, width));
}),
cell_height: Rc::new(move |cell| {
try_with_tree(|t| t.list_cell_height(node, cell as usize)).unwrap_or(0.0)
}),
recycle: Rc::new(move |cell| {
let _ = try_with_tree(|t| t.list_recycle_cell(node, cell as usize));
}),
release: Rc::new(move |cell| {
let key = cell as usize;
crate::tree::run_posted_or_defer(Box::new(move || {
let _ = try_with_tree(|t| t.list_release_cell(node, key));
}));
}),
delete: driver
.delete
.as_ref()
.filter(|_| !sectioned)
.map(|_| day_spec::ListDelete {
can_delete: {
let d = driver.clone();
Rc::new(move |i| (d.delete.as_ref().expect("mapped from Some").can_delete)(i))
},
delete_row: {
let d = driver.clone();
Rc::new(move |i| (d.delete.as_ref().expect("mapped from Some").deleted)(i))
},
}),
reorder: driver.reorder.as_ref().map(|_| day_spec::ListReorder {
can_move: {
let (d, sections) = (driver.clone(), sections.clone());
Rc::new(move |from, to| {
let re = d.reorder.as_ref().expect("mapped from Some");
let Some(map) = §ions else {
return (re.can_move)(from, to);
};
let Some((span, from, to)) = map.move_within(from, to) else {
return -1;
};
let accepted = (re.can_move)(from, to);
usize::try_from(accepted)
.ok()
.and_then(|i| span.display_of(i))
.map_or(-1, |display| display as i64)
})
},
move_row: {
let (d, sections) = (driver.clone(), sections.clone());
Rc::new(move |from, to| {
let re = d.reorder.as_ref().expect("mapped from Some");
let Some(map) = §ions else {
return (re.moved)(from, to);
};
if let Some((_, from, to)) = map.move_within(from, to) {
(re.moved)(from, to);
map.invalidate();
}
})
},
}),
swipe: driver.swipe.as_ref().map(|_| day_spec::ListSwipe {
actions_at: {
let (d, item_of) = (driver.clone(), item_of.clone());
Rc::new(move |i, e| match item_of(i) {
Some(i) => (d.swipe.as_ref().expect("mapped from Some").actions_at)(i, e),
None => Vec::new(),
})
},
perform: {
let (d, item_of) = (driver.clone(), item_of.clone());
Rc::new(move |i, e, a| {
if let Some(i) = item_of(i) {
(d.swipe.as_ref().expect("mapped from Some").performed)(i, e, a)
}
})
},
}),
}
}
fn bind_with(
node: RNode,
d: Rc<ListDriver>,
row_of: impl Fn(usize) -> Option<DisplayRow> + 'static,
pinned: bool,
) -> Rc<dyn Fn(usize, RawHandle)> {
Rc::new(move |index, cell| {
let key = cell as usize;
let (header, at) = match row_of(index) {
Some(DisplayRow::Header(first)) => (true, first),
Some(DisplayRow::Item(i)) => (false, i),
None => (false, index),
};
let Some(step) = try_with_tree(|t| t.list_prepare_cell(node, key, cell, header)) else {
return;
};
match step {
CellStep::Build { anchor } => {
let built = match (header, d.sections.as_ref()) {
(true, Some(s)) => (s.build)(at, anchor),
_ => (d.build)(at, anchor),
};
with_tree(|t| t.list_store_cell(node, key, anchor, built, header));
}
CellStep::Rebind { rebind, .. } => rebind(at),
}
day_reactive::flush_sync();
with_tree(|t| {
t.list_layout_cell(node, key);
if pinned {
t.list_pin_cell(node, key);
}
});
})
}