use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::hash::Hash;
use std::rc::Rc;
use day_core::*;
use day_reactive::{AnyBinding, Binding, Scope, Signal, watch};
use day_spec::props::*;
use crate::{Decorate, Decorated, IntoReactive, IntoText, MenuEntry, Reactive, TextSource};
use day_spec::{Event, Proposal, Rect, Size, kinds};
pub fn when<P: Piece>(
cond: impl Fn() -> bool + 'static,
build_arm: impl Fn() -> P + 'static,
) -> When {
When {
cond: Rc::new(cond),
then: Rc::new(move || AnyPiece::new(build_arm())),
otherwise: None,
}
}
pub struct When {
cond: Rc<dyn Fn() -> bool>,
then: Rc<dyn Fn() -> AnyPiece>,
otherwise: Option<Rc<dyn Fn() -> AnyPiece>>,
}
#[day_macros::builder(WhenBuilder)]
impl When {
pub fn otherwise<P: Piece>(mut self, build_arm: impl Fn() -> P + 'static) -> Self {
self.otherwise = Some(Rc::new(move || AnyPiece::new(build_arm())));
self
}
}
impl Piece for When {
fn build(self, cx: &mut BuildCx) -> RNode {
let anchor = cx.layout_only(
Rc::new(PassThrough),
Flex {
is_group: true,
..Default::default()
},
Boundary::No,
);
let state: Rc<RefCell<Option<Scope>>> = Rc::new(RefCell::new(None));
let owner = Scope::current();
let When {
cond,
then,
otherwise,
} = self;
let mount = {
let state = state.clone();
move |on: bool| {
if !with_tree(|t| t.node_exists(anchor)) {
return;
}
if let Some(scope) = state.borrow_mut().take() {
scope.dispose();
while with_tree(|t| t.child_count(anchor)) > 0 {
let child = with_tree(|t| t.first_child(anchor));
match child {
Some(c) => with_tree(|t| t.remove_subtree(c)),
None => break,
}
}
}
let arm = if on { Some(&then) } else { otherwise.as_ref() };
if let Some(arm) = arm {
let scope = owner.enter(Scope::child);
scope.enter(|| {
let mut cx = BuildCx::new(anchor);
let _ = arm().build(&mut cx);
});
*state.borrow_mut() = Some(scope);
}
}
};
let initial = {
let cond = cond.clone();
day_reactive::untrack(move || cond())
};
mount(initial);
watch(
move || cond(),
move |now, old| {
if Some(now) != old {
mount(*now);
}
},
);
anchor
}
}
fn in_row_scope<R>(build: impl FnOnce() -> R) -> R {
Scope::child().enter(build)
}
pub struct ItemSlot<T: 'static, K: 'static> {
sig: Signal<T>,
key: Signal<K>,
}
impl<T: 'static, K: 'static> Clone for ItemSlot<T, K> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static, K: 'static> Copy for ItemSlot<T, K> {}
impl<T: Clone + 'static, K: Clone + 'static> ItemSlot<T, K> {
pub fn get(self) -> T {
self.sig.get()
}
pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
self.sig.with(f)
}
pub fn field<V: Clone>(self, f: impl FnOnce(&T) -> V) -> V {
self.sig.with(f)
}
pub fn key(self) -> K {
self.key.get()
}
}
pub fn keyed<K, P>(
key: impl Fn() -> Option<K> + 'static,
build: impl Fn(&K) -> P + 'static,
) -> Keyed<K>
where
K: PartialEq + Clone + 'static,
P: Piece,
{
Keyed {
key: Rc::new(key),
build: Rc::new(move |k| AnyPiece::new(build(k))),
}
}
pub fn switch<K, P>(selector: impl Fn() -> K + 'static, arm: impl Fn(&K) -> P + 'static) -> Keyed<K>
where
K: PartialEq + Clone + 'static,
P: Piece,
{
keyed(move || Some(selector()), arm)
}
pub struct Keyed<K: 'static> {
key: Rc<dyn Fn() -> Option<K>>,
build: Rc<dyn Fn(&K) -> AnyPiece>,
}
impl<K: PartialEq + Clone + 'static> Piece for Keyed<K> {
fn build(self, cx: &mut BuildCx) -> RNode {
let anchor = cx.layout_only(
Rc::new(PassThrough),
Flex {
is_group: true,
..Default::default()
},
Boundary::No,
);
let owner = Scope::current();
let Keyed { key, build } = self;
let live: Rc<RefCell<Option<Scope>>> = Rc::new(RefCell::new(None));
let mount = {
let live = live.clone();
move |k: &Option<K>| {
if !with_tree(|t| t.node_exists(anchor)) {
return;
}
if let Some(scope) = live.borrow_mut().take() {
scope.dispose();
while with_tree(|t| t.child_count(anchor)) > 0 {
match with_tree(|t| t.first_child(anchor)) {
Some(c) => with_tree(|t| t.remove_subtree(c)),
None => break,
}
}
}
if let Some(k) = k {
let scope = owner.enter(Scope::child);
scope.enter(|| {
let mut cx = BuildCx::new(anchor);
let _ = build(k).build(&mut cx);
});
*live.borrow_mut() = Some(scope);
}
}
};
let initial = {
let key = key.clone();
day_reactive::untrack(move || key())
};
mount(&initial);
watch(
move || key(),
move |now, old| {
if Some(now) != old {
mount(now);
}
},
);
anchor
}
}
pub trait LoadSource<T: 'static, M>: 'static {
fn into_load_reader(self) -> Rc<dyn Fn() -> day_reactive::Load<T>>;
}
pub struct ResourceMark;
impl<T: Clone + 'static> LoadSource<T, ResourceMark> for day_reactive::Resource<T> {
fn into_load_reader(self) -> Rc<dyn Fn() -> day_reactive::Load<T>> {
Rc::new(move || self.signal().get())
}
}
impl<T: Clone + 'static> LoadSource<T, crate::SignalMark> for Signal<day_reactive::Load<T>> {
fn into_load_reader(self) -> Rc<dyn Fn() -> day_reactive::Load<T>> {
Rc::new(move || self.get())
}
}
impl<T: 'static, F: Fn() -> day_reactive::Load<T> + 'static> LoadSource<T, crate::FnMark> for F {
fn into_load_reader(self) -> Rc<dyn Fn() -> day_reactive::Load<T>> {
Rc::new(self)
}
}
pub struct Loaded<T: 'static> {
source: Rc<dyn Fn() -> day_reactive::Load<T>>,
last: Rc<RefCell<T>>,
}
impl<T: 'static> Clone for Loaded<T> {
fn clone(&self) -> Self {
Loaded {
source: self.source.clone(),
last: self.last.clone(),
}
}
}
impl<T: Clone + 'static> Loaded<T> {
pub fn get(&self) -> T {
if let day_reactive::Load::Ready(v) = (self.source)() {
*self.last.borrow_mut() = v.clone();
v
} else {
self.last.borrow().clone()
}
}
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let v = self.get();
f(&v)
}
}
pub fn load<T, P, M>(
state: impl LoadSource<T, M>,
ready: impl Fn(Loaded<T>) -> P + 'static,
) -> LoadPiece<T>
where
T: Clone + 'static,
P: Piece,
{
LoadPiece {
source: state.into_load_reader(),
ready: Rc::new(move |v| AnyPiece::new(ready(v))),
loading: None,
failed: None,
}
}
pub struct LoadPiece<T: 'static> {
source: Rc<dyn Fn() -> day_reactive::Load<T>>,
ready: Rc<dyn Fn(Loaded<T>) -> AnyPiece>,
loading: Option<Rc<dyn Fn() -> AnyPiece>>,
failed: Option<FailedArm>,
}
type FailedArm = Rc<dyn Fn(&(dyn std::error::Error + 'static)) -> AnyPiece>;
impl<T: Clone + 'static> LoadPiece<T> {
pub fn loading<P: Piece>(mut self, build: impl Fn() -> P + 'static) -> Self {
self.loading = Some(Rc::new(move || AnyPiece::new(build())));
self
}
pub fn failed<P: Piece>(
mut self,
build: impl Fn(&(dyn std::error::Error + 'static)) -> P + 'static,
) -> Self {
self.failed = Some(Rc::new(move |e| AnyPiece::new(build(e))));
self
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LoadArm {
Loading,
Ready,
Failed,
}
impl<T: Clone + 'static> Piece for LoadPiece<T> {
fn build(self, cx: &mut BuildCx) -> RNode {
let LoadPiece {
source,
ready,
loading,
failed,
} = self;
let arm_source = source.clone();
let error_source = source.clone();
keyed(
move || {
Some(match arm_source() {
day_reactive::Load::Loading => LoadArm::Loading,
day_reactive::Load::Ready(_) => LoadArm::Ready,
day_reactive::Load::Failed(_) => LoadArm::Failed,
})
},
move |arm| match arm {
LoadArm::Loading => loading.as_ref().map(|f| f()),
LoadArm::Failed => {
failed
.as_ref()
.and_then(|f| match day_reactive::untrack(|| error_source()) {
day_reactive::Load::Failed(e) => Some(f(&*e)),
_ => None,
})
}
LoadArm::Ready => match day_reactive::untrack(|| source()) {
day_reactive::Load::Ready(v) => Some(ready(Loaded {
source: source.clone(),
last: Rc::new(RefCell::new(v)),
})),
_ => None,
},
},
)
.build(cx)
}
}
pub fn adaptive<C: PieceSeq>(children: C) -> Adaptive<C> {
Adaptive {
children,
spacing: 0.0,
at: day_spec::WidthClass::Compact,
align: CrossAlign::Center,
}
}
pub struct Adaptive<C: PieceSeq> {
children: C,
spacing: f64,
at: day_spec::WidthClass,
align: CrossAlign,
}
#[day_macros::builder(AdaptiveBuilder)]
impl<C: PieceSeq> Adaptive<C> {
pub fn spacing(mut self, s: impl Into<f64>) -> Self {
self.spacing = s.into();
self
}
pub fn stack_at(mut self, at: day_spec::WidthClass) -> Self {
self.at = at;
self
}
pub fn align(mut self, a: crate::VAlign) -> Self {
self.align = match a {
crate::VAlign::Top => CrossAlign::Leading,
crate::VAlign::Center => CrossAlign::Center,
crate::VAlign::Bottom => CrossAlign::Trailing,
crate::VAlign::FirstBaseline => CrossAlign::FirstBaseline,
};
self
}
}
struct AdaptiveLayout {
axis: std::cell::Cell<Axis>,
spacing: f64,
row_align: CrossAlign,
}
impl AdaptiveLayout {
fn stack(&self) -> StackLayout {
let axis = self.axis.get();
StackLayout {
axis,
spacing: self.spacing,
align: match axis {
Axis::Vertical => CrossAlign::Leading,
Axis::Horizontal => self.row_align,
},
}
}
}
impl Layout for AdaptiveLayout {
fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
self.stack().measure(cx, children, p)
}
fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
self.stack().place(cx, children, bounds)
}
fn baseline(&self, cx: &mut dyn LayoutOps, children: &[RNode], size: Size) -> Option<f64> {
self.stack().baseline(cx, children, size)
}
}
impl<C: PieceSeq> Piece for Adaptive<C> {
fn build(self, cx: &mut BuildCx) -> RNode {
let at = self.at;
let stacked = move || size_class().is_some_and(|c| c.width <= at);
let layout = Rc::new(AdaptiveLayout {
axis: std::cell::Cell::new(if day_reactive::untrack(stacked) {
Axis::Vertical
} else {
Axis::Horizontal
}),
spacing: self.spacing,
row_align: self.align,
});
let node = cx.native(
kinds::CONTAINER,
&ContainerProps::default(),
layout.clone(),
Flex::default(),
Boundary::No,
);
cx.under(node, |cx| self.children.build_each(cx));
watch(stacked, move |now: &bool, _| {
layout.axis.set(if *now {
Axis::Vertical
} else {
Axis::Horizontal
});
day_core::invalidate_size(node);
});
node
}
}
pub trait RowSource {
type Slot: Copy + 'static;
type Ref: 'static;
type Conn: RowConn<Slot = Self::Slot, Ref = Self::Ref>;
fn connect(self) -> Self::Conn;
}
pub trait RowConn: 'static {
type Slot: Copy + 'static;
type Ref: 'static;
fn refresh(&self) -> Vec<u64>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn token_at(&self, index: usize) -> u64;
fn tokens_now(&self) -> Vec<u64>;
fn slot_at(&self, index: usize) -> Option<Self::Slot>;
fn rebind(&self, slot: &Self::Slot, index: usize);
fn select_ref(&self, index: usize) -> Option<Self::Ref>;
fn values_flow_by_reload(&self) -> bool;
fn take_row_events(&self) -> Option<Vec<day_spec::props::RowDelta>> {
None
}
fn commit_move(&self, from: usize, to: usize);
fn commit_delete(&self, index: usize);
}
pub fn items<T, K>(
f: impl Fn() -> Vec<T> + 'static,
key_of: impl Fn(&T) -> K + 'static,
) -> Items<T, K>
where
T: Clone + 'static,
K: Clone + Hash + 'static,
{
Items {
f: Rc::new(f),
key_of: Rc::new(key_of),
}
}
pub struct Items<T: 'static, K: 'static> {
f: Rc<dyn Fn() -> Vec<T>>,
key_of: Rc<dyn Fn(&T) -> K>,
}
impl<T: Clone + 'static, K: Clone + Hash + 'static> RowSource for Items<T, K> {
type Slot = ItemSlot<T, K>;
type Ref = K;
type Conn = ItemsConn<T, K>;
fn connect(self) -> ItemsConn<T, K> {
ItemsConn {
f: self.f,
key_of: self.key_of,
snapshot: RefCell::new(Vec::new()),
tokens: RefCell::new(Vec::new()),
}
}
}
pub struct ItemsConn<T: 'static, K: 'static> {
f: Rc<dyn Fn() -> Vec<T>>,
key_of: Rc<dyn Fn(&T) -> K>,
snapshot: RefCell<Vec<T>>,
tokens: RefCell<Vec<u64>>,
}
impl<T: Clone + 'static, K: Clone + Hash + 'static> RowConn for ItemsConn<T, K> {
type Slot = ItemSlot<T, K>;
type Ref = K;
fn refresh(&self) -> Vec<u64> {
let its = (self.f)();
let toks: Vec<u64> = its.iter().map(|t| key_token(&(self.key_of)(t))).collect();
*self.snapshot.borrow_mut() = its;
*self.tokens.borrow_mut() = toks.clone();
toks
}
fn len(&self) -> usize {
self.snapshot.borrow().len()
}
fn token_at(&self, index: usize) -> u64 {
self.tokens.borrow().get(index).copied().unwrap_or(0)
}
fn tokens_now(&self) -> Vec<u64> {
self.tokens.borrow().clone()
}
fn slot_at(&self, index: usize) -> Option<ItemSlot<T, K>> {
let item = self.snapshot.borrow().get(index).cloned()?;
let key = (self.key_of)(&item);
Some(ItemSlot {
sig: Signal::new(item),
key: Signal::new(key),
})
}
fn rebind(&self, slot: &ItemSlot<T, K>, index: usize) {
let Some(item) = self.snapshot.borrow().get(index).cloned() else {
return; };
slot.key.set((self.key_of)(&item));
slot.sig.set(item);
}
fn select_ref(&self, index: usize) -> Option<K> {
self.snapshot.borrow().get(index).map(|t| (self.key_of)(t))
}
fn values_flow_by_reload(&self) -> bool {
true
}
fn commit_move(&self, from: usize, to: usize) {
let mut snap = self.snapshot.borrow_mut();
let mut toks = self.tokens.borrow_mut();
if from >= snap.len() || to >= snap.len() {
return;
}
let item = snap.remove(from);
snap.insert(to, item);
let tok = toks.remove(from);
toks.insert(to, tok);
}
fn commit_delete(&self, index: usize) {
let mut snap = self.snapshot.borrow_mut();
let mut toks = self.tokens.borrow_mut();
if index >= snap.len() {
return;
}
snap.remove(index);
toks.remove(index);
}
}
struct EachRow<S: 'static> {
token: u64,
slot: S,
scope: Scope,
root: RNode,
}
pub fn each<S, P>(source: S, build_row: impl Fn(S::Slot) -> P + 'static) -> impl Piece
where
S: RowSource + 'static,
P: Piece,
{
piece_fn(move |cx| {
let anchor = cx.layout_only(
Rc::new(PassThrough),
Flex {
is_group: true,
..Default::default()
},
Boundary::No,
);
let owner = Scope::current();
let conn = Rc::new(source.connect());
let rows: Rc<RefCell<Vec<EachRow<S::Slot>>>> = Rc::new(RefCell::new(Vec::new()));
let build_row = Rc::new(build_row);
let sync = {
let rows = rows.clone();
let conn = conn.clone();
let build_row = build_row.clone();
move |tokens: &Vec<u64>| {
if !with_tree(|t| t.node_exists(anchor)) {
return;
}
if cfg!(debug_assertions) {
let mut seen = HashSet::new();
for k in tokens {
assert!(seen.insert(*k), "day: duplicate key in `each` diff");
}
}
let mut old = std::mem::take(&mut *rows.borrow_mut());
let mut next: Vec<EachRow<S::Slot>> = Vec::with_capacity(tokens.len());
for (index, tok) in tokens.iter().enumerate() {
if let Some(pos) = old.iter().position(|r| r.token == *tok) {
let row = old.remove(pos);
conn.rebind(&row.slot, index);
next.push(row);
} else {
let scope = owner.enter(Scope::child);
let built = scope.enter(|| {
let slot = conn.slot_at(index)?;
let mut cx = BuildCx::new(anchor);
Some((slot, in_row_scope(|| build_row(slot).build(&mut cx))))
});
match built {
Some((slot, root)) => next.push(EachRow {
token: *tok,
slot,
scope,
root,
}),
None => scope.dispose(),
}
}
}
for row in old {
row.scope.dispose();
with_tree(|t| t.remove_subtree(row.root));
}
let order: Vec<RNode> = next.iter().map(|r| r.root).collect();
with_tree(|t| t.reorder_children(anchor, order));
*rows.borrow_mut() = next;
}
};
let initial = {
let conn = conn.clone();
day_reactive::untrack(move || conn.refresh())
};
sync(&initial);
let conn2 = conn.clone();
watch(move || conn2.refresh(), move |new, _| sync(new));
anchor
})
}
fn key_token<K: Hash>(k: &K) -> u64 {
use std::hash::Hasher;
let mut h = std::collections::hash_map::DefaultHasher::new();
k.hash(&mut h);
h.finish()
}
pub struct List<S: RowSource> {
source: Option<S>,
build_row: Rc<dyn Fn(S::Slot) -> AnyPiece>,
row_height: RowHeight,
on_select: Option<Rc<dyn Fn(S::Ref)>>,
on_activate: Option<Rc<dyn Fn(S::Ref)>>,
on_selection: Option<SelectionFn<S::Ref>>,
multi_select: bool,
selected_rows: Option<Rc<dyn Fn() -> Vec<usize>>>,
selection: Option<ListSelection<S::Ref>>,
scroll_to_end: Option<day_reactive::Trigger>,
scroll_to_row: Option<AnyBinding<Option<usize>>>,
first_visible_row: Option<AnyBinding<usize>>,
stick_to_bottom: bool,
reorder: Option<Reorder>,
delete: Option<Delete>,
swipe_leading: Option<SwipeProvider>,
swipe_trailing: Option<SwipeProvider>,
separators: Option<bool>,
editing: crate::Reactive<bool>,
sections: Option<ListSections<S::Slot>>,
}
struct ListSections<Slot> {
key: Rc<dyn Fn(usize) -> u64>,
header: Rc<dyn Fn(Slot) -> AnyPiece>,
}
type SelectionFn<R> = Rc<dyn Fn(Vec<R>)>;
pub trait SelectionKey {
fn selection_key(&self) -> u64;
}
struct ListSelection<R> {
key: Rc<dyn Fn() -> Option<u64>>,
write: Rc<dyn Fn(Option<u64>)>,
key_of: Rc<dyn Fn(&R) -> u64>,
}
#[derive(Clone)]
enum ItemIcon {
Vector(Reactive<day_spec::VectorName>),
Symbol(day_spec::Symbol),
}
impl ItemIcon {
fn spec(&self, tracked: bool) -> day_spec::Icon {
match self {
ItemIcon::Vector(r) => {
let name = if tracked { r.get() } else { r.get_untracked() };
day_spec::Icon::Image(name.as_str().to_owned())
}
ItemIcon::Symbol(s) => day_spec::Icon::Symbol(*s),
}
}
}
pub struct ListItem {
title: TextSource,
subtitle: Option<TextSource>,
detail: Option<TextSource>,
icon: Option<ItemIcon>,
icon_tint: Option<Reactive<day_spec::Color>>,
checked: Option<Reactive<bool>>,
}
pub fn list_item<M>(title: impl IntoText<M>) -> ListItem {
ListItem {
title: title.into_text(),
subtitle: None,
detail: None,
icon: None,
icon_tint: None,
checked: None,
}
}
#[day_macros::builder(ListItemBuilder)]
impl ListItem {
pub fn subtitle<M>(mut self, text: impl IntoText<M>) -> Self {
self.subtitle = Some(text.into_text());
self
}
pub fn detail<M>(mut self, text: impl IntoText<M>) -> Self {
self.detail = Some(text.into_text());
self
}
pub fn icon<M>(mut self, name: impl IntoReactive<day_spec::VectorName, M>) -> Self {
self.icon = Some(ItemIcon::Vector(name.into_reactive()));
self
}
pub fn symbol(mut self, symbol: day_spec::Symbol) -> Self {
self.icon = Some(ItemIcon::Symbol(symbol));
self
}
pub fn icon_tint<M>(mut self, color: impl IntoReactive<day_spec::Color, M>) -> Self {
self.icon_tint = Some(color.into_reactive());
self
}
pub fn checked<M>(mut self, on: impl IntoReactive<bool, M>) -> Self {
self.checked = Some(on.into_reactive());
self
}
}
impl Piece for ListItem {
fn build(self, cx: &mut BuildCx) -> RNode {
if day_core::capability(day_spec::Cap::ListItem) == day_spec::Support::Native {
self.build_native(cx)
} else {
self.compose().build(cx)
}
}
}
impl ListItem {
fn build_native(self, cx: &mut BuildCx) -> RNode {
let text = |t: &Option<TextSource>| t.as_ref().map(TextSource::initial).unwrap_or_default();
let props = ListItemProps {
title: self.title.initial(),
subtitle: text(&self.subtitle),
detail: text(&self.detail),
icon: self.icon.as_ref().map(|i| i.spec(false)),
icon_tint: self.icon_tint.as_ref().map(Reactive::get_untracked),
checked: self.checked.as_ref().is_some_and(Reactive::get_untracked),
};
let node = cx.leaf(
kinds::LIST_ITEM,
&props,
Flex {
grow_w: true,
whole_row: true,
..Default::default()
},
);
let patch = move |p: ListItemPatch, size: bool| {
with_tree(|t| t.patch(node, Box::new(p), size));
};
let bind_text =
|src: Option<TextSource>, seed: String, make: fn(String) -> ListItemPatch| {
if let Some(src @ TextSource::Dyn(_)) = src {
day_reactive::bind_seeded(
seed,
move || src.resolve(),
move |t: &String| patch(make(t.clone()), true),
);
}
};
bind_text(Some(self.title), props.title, ListItemPatch::Title);
bind_text(self.subtitle, props.subtitle, ListItemPatch::Subtitle);
bind_text(self.detail, props.detail, ListItemPatch::Detail);
if let Some(icon @ ItemIcon::Vector(Reactive::Dyn(_))) = self.icon {
day_reactive::bind_seeded(
props.icon,
move || Some(icon.spec(true)),
move |i| patch(ListItemPatch::Icon(i.clone()), false),
);
}
if let Some(tint @ Reactive::Dyn(_)) = self.icon_tint {
day_reactive::bind_seeded(
props.icon_tint,
move || Some(tint.get()),
move |c| patch(ListItemPatch::IconTint(*c), false),
);
}
if let Some(on @ Reactive::Dyn(_)) = self.checked {
day_reactive::bind_seeded(
props.checked,
move || on.get(),
move |b: &bool| patch(ListItemPatch::Checked(*b), true),
);
}
node
}
fn compose(self) -> impl Piece {
use crate::{column, image, label, row, vector, when};
let tint = self.icon_tint;
let leading = self.icon.map(move |icon| match icon {
ItemIcon::Vector(src) => {
let glyph = vector(move || src.get()).template();
match tint {
Some(t) => glyph
.tint(move || t.get())
.decorative()
.frame(20.0, 20.0)
.any(),
None => glyph.decorative().frame(20.0, 20.0).any(),
}
}
ItemIcon::Symbol(sym) => match sym.outline_svg() {
Some(svg) => image(svg.into_bytes()).template().frame(20.0, 20.0).any(),
None => crate::spacer().frame(20.0, 20.0).any(),
},
});
let titles = column((
label(self.title),
self.subtitle
.map(|t| label(t).font(day_spec::Font::Footnote).secondary()),
))
.spacing(2.0)
.align(crate::HAlign::Leading)
.grow_w();
let detail = self.detail.map(|t| label(t).secondary());
let checked = self.checked.clone();
let mark = checked.map(|on| {
when(
move || on.get(),
|| {
let svg = day_spec::Symbol::Check.outline_svg().unwrap_or_default();
image(svg.into_bytes())
.template()
.decorative()
.frame(16.0, 16.0)
},
)
});
let selected = self.checked;
row((leading, titles, detail, mark))
.spacing(crate::Space::M)
.align(crate::VAlign::Center)
.grow_w()
.a11y(move |a| match selected {
Some(on) => a.selected(move || on.get()),
None => a,
})
}
}
type SwipeProvider = Rc<dyn Fn(usize) -> Vec<SwipeAction>>;
pub struct SwipeAction {
label: TextSource,
destructive: bool,
tint: Option<day_spec::Color>,
symbol: Option<day_spec::Symbol>,
handler: Rc<dyn Fn()>,
}
pub fn swipe_action<M>(label: impl IntoText<M>) -> SwipeAction {
SwipeAction {
label: label.into_text(),
destructive: false,
tint: None,
symbol: None,
handler: Rc::new(|| {}),
}
}
impl SwipeAction {
pub fn destructive(mut self) -> Self {
self.destructive = true;
self
}
pub fn tint(mut self, color: day_spec::Color) -> Self {
self.tint = Some(color);
self
}
pub fn symbol(mut self, symbol: day_spec::Symbol) -> Self {
self.symbol = Some(symbol);
self
}
pub fn action(mut self, f: impl Fn() + 'static) -> Self {
self.handler = Rc::new(f);
self
}
}
#[derive(Clone)]
pub struct Reorder {
moved: Rc<dyn Fn(usize, usize)>,
guard: Option<Rc<dyn Fn(usize, usize) -> ReorderVerdict>>,
enabled: Reactive<bool>,
}
impl Reorder {
pub fn new(moved: impl Fn(usize, usize) + 'static) -> Reorder {
Reorder {
moved: Rc::new(moved),
guard: None,
enabled: Reactive::Const(true),
}
}
pub fn guard(mut self, g: impl Fn(usize, usize) -> ReorderVerdict + 'static) -> Reorder {
self.guard = Some(Rc::new(g));
self
}
pub fn enabled<M>(mut self, on: impl IntoReactive<bool, M>) -> Reorder {
self.enabled = on.into_reactive();
self
}
}
#[derive(Clone)]
pub struct Delete {
deleted: Rc<dyn Fn(usize)>,
label: Option<TextSource>,
guard: Option<Rc<dyn Fn(usize) -> bool>>,
enabled: Reactive<bool>,
}
impl Delete {
pub fn new(deleted: impl Fn(usize) + 'static) -> Delete {
Delete {
deleted: Rc::new(deleted),
label: None,
guard: None,
enabled: Reactive::Const(true),
}
}
pub fn label<M>(mut self, text: impl IntoText<M>) -> Delete {
self.label = Some(text.into_text());
self
}
pub fn guard(mut self, g: impl Fn(usize) -> bool + 'static) -> Delete {
self.guard = Some(Rc::new(g));
self
}
pub fn enabled<M>(mut self, on: impl IntoReactive<bool, M>) -> Delete {
self.enabled = on.into_reactive();
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReorderVerdict {
Allow,
Deny,
Retarget(usize),
}
pub fn list<S, P>(source: S, build_row: impl Fn(S::Slot) -> P + 'static) -> List<S>
where
S: RowSource + 'static,
P: Piece,
{
List {
source: Some(source),
build_row: Rc::new(move |slot| AnyPiece::new(build_row(slot))),
row_height: RowHeight::Automatic,
on_select: None,
on_activate: None,
on_selection: None,
multi_select: false,
selected_rows: None,
selection: None,
scroll_to_end: None,
scroll_to_row: None,
first_visible_row: None,
stick_to_bottom: false,
reorder: None,
delete: None,
swipe_leading: None,
swipe_trailing: None,
separators: None,
editing: crate::Reactive::Const(false),
sections: None,
}
}
#[day_macros::builder(ListBuilder<S>)]
impl<S: RowSource + 'static> List<S> {
pub fn row_height(mut self, h: RowHeight) -> Self {
self.row_height = h;
self
}
pub fn on_activate(mut self, f: impl Fn(S::Ref) + 'static) -> Self {
self.on_activate = Some(Rc::new(f));
self
}
pub fn on_select(mut self, f: impl Fn(S::Ref) + 'static) -> Self {
self.on_select = Some(Rc::new(f));
self
}
pub fn multi_select(mut self) -> Self {
self.multi_select = true;
self
}
pub fn on_selection(mut self, f: impl Fn(Vec<S::Ref>) + 'static) -> Self {
self.on_selection = Some(Rc::new(f));
self
}
pub fn selected_rows(mut self, rows: impl Fn() -> Vec<usize> + 'static) -> Self {
self.selected_rows = Some(Rc::new(rows));
self
}
pub fn selection(mut self, selected: impl Binding<Option<u64>>) -> Self
where
S::Ref: SelectionKey,
{
let selected = AnyBinding::new(selected);
self.selection = Some(ListSelection {
key: Rc::new(move || selected.get()),
write: Rc::new(move |key| selected.set_if_changed(key)),
key_of: Rc::new(|row: &S::Ref| row.selection_key()),
});
self
}
pub fn scroll_to_end(mut self, trigger: day_reactive::Trigger) -> Self {
self.scroll_to_end = Some(trigger);
self
}
pub fn stick_to_bottom(mut self) -> Self {
self.stick_to_bottom = true;
self
}
pub fn scroll_to_row(mut self, target: impl Binding<Option<usize>>) -> Self {
self.scroll_to_row = Some(AnyBinding::new(target));
self
}
pub fn first_visible_row(mut self, first: impl Binding<usize>) -> Self {
self.first_visible_row = Some(AnyBinding::new(first));
self
}
pub fn reorderable(mut self, reorder: Reorder) -> Self {
self.reorder = Some(reorder);
self
}
pub fn deletable(mut self, delete: Delete) -> Self {
self.delete = Some(delete);
self
}
pub fn swipe_leading(mut self, provider: impl Fn(usize) -> Vec<SwipeAction> + 'static) -> Self {
self.swipe_leading = Some(Rc::new(provider));
self
}
pub fn swipe_trailing(
mut self,
provider: impl Fn(usize) -> Vec<SwipeAction> + 'static,
) -> Self {
self.swipe_trailing = Some(Rc::new(provider));
self
}
pub fn separators(mut self) -> Self {
self.separators = Some(true);
self
}
pub fn no_separators(mut self) -> Self {
self.separators = Some(false);
self
}
pub fn sections<K: Hash, P: Piece>(
mut self,
key: impl Fn(usize) -> K + 'static,
header: impl Fn(S::Slot) -> P + 'static,
) -> Self {
self.sections = Some(ListSections {
key: Rc::new(move |i| key_token(&key(i))),
header: Rc::new(move |slot| AnyPiece::new(header(slot))),
});
self
}
pub fn editing<M>(mut self, on: impl crate::IntoReactive<bool, M>) -> Self {
self.editing = on.into_reactive();
self
}
}
fn slot_builder<C: RowConn>(
conn: Rc<C>,
builder: Rc<dyn Fn(C::Slot) -> AnyPiece>,
) -> Box<dyn Fn(usize, RNode) -> BuiltRow> {
Box::new(move |index, anchor| {
let scope = Scope::child();
let conn = conn.clone();
let builder = builder.clone();
let rebind = scope.enter(move || {
let Some(slot) = conn.slot_at(index) else {
return Rc::new(|_: usize| {}) as Rc<dyn Fn(usize)>;
};
let mut rowcx = BuildCx::new(anchor);
in_row_scope(|| builder(slot).build(&mut rowcx));
Rc::new(move |i: usize| conn.rebind(&slot, i)) as Rc<dyn Fn(usize)>
});
BuiltRow { scope, rebind }
})
}
impl<S: RowSource + 'static> Piece for List<S> {
fn build(mut self, cx: &mut BuildCx) -> RNode {
let props = ListProps {
activatable: self.on_activate.is_some(),
row_height: self.row_height,
selectable: self.on_select.is_some()
|| self.on_selection.is_some()
|| self.selection.is_some()
|| self.on_activate.is_some(),
multi_select: self.multi_select,
reorderable: self.reorder.is_some(),
deletable: self.delete.is_some() && self.sections.is_none(),
delete_label: self
.delete
.as_ref()
.and_then(|d| d.label.as_ref())
.map(TextSource::initial)
.unwrap_or_default(),
swipe_actions: self.swipe_leading.is_some() || self.swipe_trailing.is_some(),
separators: self.separators,
editing: self.editing.get_untracked(),
sections: self.sections.is_some(),
};
let node = cx.leaf(
kinds::LIST,
&props,
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
);
let conn = Rc::new(self.source.take().expect("List built once").connect());
let section_keys: Rc<RefCell<Vec<u64>>> = Rc::new(RefCell::new(Vec::new()));
let key_of_row = self.sections.as_ref().map(|s| s.key.clone());
let refresh = {
let (conn, keys, key_of_row) = (conn.clone(), section_keys.clone(), key_of_row.clone());
Rc::new(move || {
let tokens = conn.refresh();
if let Some(key) = &key_of_row {
*keys.borrow_mut() = (0..conn.len()).map(|i| key(i)).collect();
}
(tokens, keys.borrow().clone())
})
};
let pending_echo: Rc<RefCell<Option<Vec<u64>>>> = Rc::new(RefCell::new(None));
if let Some(activate) = self.on_activate.clone() {
let conn = conn.clone();
cx.on(node, move |event| {
if let Event::ListActivated(row) = event
&& let Some(reference) = conn.select_ref(*row)
{
activate(reference);
}
});
}
if self.on_select.is_some() || self.on_selection.is_some() {
let (on_select, on_selection) = (self.on_select.clone(), self.on_selection.clone());
let conn = conn.clone();
cx.on(node, move |ev| match ev {
Event::SelectionChanged(i) => {
if let Some(f) = &on_select
&& let Some(r) = conn.select_ref(*i as usize)
{
f(r);
}
if let Some(f) = &on_selection {
f(conn.select_ref(*i as usize).into_iter().collect());
}
}
Event::SelectionSet(rows) => {
if let Some(f) = &on_selection {
f(rows
.iter()
.filter_map(|i| conn.select_ref(*i as usize))
.collect());
}
}
_ => {}
});
}
let user_pick: Rc<Cell<Option<Option<u64>>>> = Rc::new(Cell::new(None));
let reveal = Rc::new(Cell::new(false));
let apply_selection: Option<Rc<dyn Fn()>> = self.selection.as_ref().map(|sel| {
let (key, conn, reveal) = (sel.key.clone(), conn.clone(), reveal.clone());
Rc::new(move || {
let wanted = day_reactive::untrack(|| key());
let row = wanted.and_then(|k| conn.tokens_now().iter().position(|t| *t == k));
let scroll = row.filter(|_| reveal.replace(false));
day_reactive::on_main(move || {
day_core::list_set_selected(node, row.into_iter().collect());
if let Some(row) = scroll {
list_scroll_to_row(node, row);
}
});
}) as Rc<dyn Fn()>
});
if let Some(sel) = &self.selection {
let (write, key_of) = (sel.write.clone(), sel.key_of.clone());
let (conn, pick) = (conn.clone(), user_pick.clone());
cx.on(node, move |ev| {
let row = match ev {
Event::SelectionChanged(i) => conn.select_ref(*i as usize),
Event::SelectionSet(rows) => {
rows.first().and_then(|i| conn.select_ref(*i as usize))
}
_ => return,
};
let key = row.as_ref().map(|r| key_of(r));
pick.set(Some(key));
write(key);
});
}
if let Some(on_reorder) = self.reorder.as_ref().map(|r| r.moved.clone()) {
cx.on(node, move |ev| {
if let Event::ListReorder { from, to } = ev {
on_reorder(*from, *to);
}
});
}
if let Some(on_delete) = self.delete.as_ref().map(|d| d.deleted.clone()) {
cx.on(node, move |ev| {
if let Event::ListDelete(index) = ev {
on_delete(*index);
}
});
}
if self.swipe_leading.is_some() || self.swipe_trailing.is_some() {
let (leading, trailing) = (self.swipe_leading.clone(), self.swipe_trailing.clone());
cx.on(node, move |ev| {
if let Event::ListSwipe {
index,
edge,
action,
} = ev
{
let provider = match edge {
day_spec::SwipeEdge::Leading => leading.as_ref(),
day_spec::SwipeEdge::Trailing => trailing.as_ref(),
};
if let Some(p) = provider
&& let Some(a) = p(*index).get(*action)
{
(a.handler)();
}
}
});
}
if let Some(signal) = self.first_visible_row {
cx.on(node, move |event| {
if let Event::Custom { tag, num, text } = event
&& (*tag == "day-list-first-visible"
|| (tag.is_empty() && text == "day-list-first-visible"))
&& num.is_finite()
&& *num >= 0.0
{
signal.set_if_changed(*num as usize);
}
});
}
let driver = ListDriver {
track_viewport: self.first_visible_row.is_some(),
row_height: self.row_height,
len: {
let conn = conn.clone();
Box::new(move || conn.len())
},
token_at: {
let conn = conn.clone();
Box::new(move |i| conn.token_at(i))
},
build: slot_builder(conn.clone(), self.build_row.clone()),
sections: self.sections.as_ref().map(|sections| ListSectionsDriver {
key_at: {
let keys = section_keys.clone();
Box::new(move |i| keys.borrow().get(i).copied().unwrap_or_default())
},
build: slot_builder(conn.clone(), sections.header.clone()),
}),
delete: self.delete.as_ref().map(|delete| ListDeleteDriver {
can_delete: {
let (guard, enabled) = (delete.guard.clone(), delete.enabled.clone());
Box::new(move |i| {
enabled.get_untracked() && guard.as_ref().is_none_or(|g| g(i))
})
},
deleted: {
let (conn, echo, keys) =
(conn.clone(), pending_echo.clone(), section_keys.clone());
Box::new(move |index| {
if index >= conn.len() {
return;
}
conn.commit_delete(index);
if index < keys.borrow().len() {
keys.borrow_mut().remove(index);
}
*echo.borrow_mut() = Some(conn.tokens_now());
enqueue_event(rnode_to_id(node), Event::ListDelete(index));
})
},
}),
reorder: self.reorder.as_ref().map(|reorder| ListReorderDriver {
can_move: {
let (guard, enabled) = (reorder.guard.clone(), reorder.enabled.clone());
Box::new(move |from, to| {
if !enabled.get_untracked() {
return -1;
}
match guard.as_ref().map(|g| g(from, to)) {
None | Some(ReorderVerdict::Allow) => to as i64,
Some(ReorderVerdict::Deny) => -1,
Some(ReorderVerdict::Retarget(i)) => i as i64,
}
})
},
moved: {
let (conn, echo, keys) =
(conn.clone(), pending_echo.clone(), section_keys.clone());
Box::new(move |from, to| {
if from >= conn.len() || to >= conn.len() {
return;
}
conn.commit_move(from, to);
{
let mut keys = keys.borrow_mut();
if from < keys.len() && to < keys.len() {
let k = keys.remove(from);
keys.insert(to, k);
}
}
*echo.borrow_mut() = Some(conn.tokens_now());
enqueue_event(rnode_to_id(node), Event::ListReorder { from, to });
})
},
}),
swipe: (self.swipe_leading.is_some() || self.swipe_trailing.is_some()).then(|| {
ListSwipeDriver {
actions_at: {
let (leading, trailing) =
(self.swipe_leading.clone(), self.swipe_trailing.clone());
Box::new(move |i, edge| {
let provider = match edge {
day_spec::SwipeEdge::Leading => leading.as_ref(),
day_spec::SwipeEdge::Trailing => trailing.as_ref(),
};
provider.map_or_else(Vec::new, |p| {
p(i).iter()
.map(|a| day_spec::ListSwipeAction {
label: a.label.resolve(),
destructive: a.destructive,
tint: a.tint,
symbol: a.symbol,
})
.collect()
})
})
},
performed: {
let conn = conn.clone();
Box::new(move |index, edge, action| {
if index >= conn.len() {
return;
}
enqueue_event(
rnode_to_id(node),
Event::ListSwipe {
index,
edge,
action,
},
);
})
},
}
}),
};
install_list(node, driver);
{
let (conn, echo, stick) = (conn.clone(), pending_echo.clone(), self.stick_to_bottom);
let reselect = apply_selection.clone();
let initial = {
let refresh = refresh.clone();
day_reactive::untrack(move || refresh())
};
list_reload(node);
let last: RefCell<(Vec<u64>, Vec<u64>)> = RefCell::new(initial);
watch(
move || refresh(),
move |rows: &(Vec<u64>, Vec<u64>), _| {
let toks = &rows.0;
let pending = pending_echo.borrow_mut().take();
let is_echo = pending.as_ref().is_some_and(|e| e == toks);
let unchanged = !conn.values_flow_by_reload() && *last.borrow() == *rows;
*last.borrow_mut() = rows.clone();
if !is_echo && pending.is_some() {
let _ = conn.take_row_events();
list_reload(node);
if let Some(apply) = &reselect {
apply();
}
} else if !is_echo && !unchanged {
match conn.take_row_events() {
Some(deltas) if !deltas.is_empty() => list_splice(node, deltas),
Some(_) => {}
None => list_reload(node),
}
if stick {
list_scroll_to_end(node);
}
if let Some(apply) = &reselect {
apply();
}
} else {
let _ = conn.take_row_events();
}
},
);
let _ = echo;
}
if let Some(trigger) = self.scroll_to_end {
watch(
move || trigger.track(),
move |_: &(), _| list_scroll_to_end(node),
);
}
if let Some(sig) = self.scroll_to_row {
watch(
move || sig.get(),
move |row: &Option<usize>, _| {
if let Some(row) = row {
list_scroll_to_row(node, *row);
}
},
);
}
if let editing @ crate::Reactive::Dyn(_) =
std::mem::replace(&mut self.editing, crate::Reactive::Const(false))
{
watch(
move || editing.get(),
move |on: &bool, old| {
if old.is_some() {
list_set_editing(node, *on);
}
},
);
}
if let (Some(sel), Some(apply)) = (self.selection.take(), apply_selection) {
let first = Cell::new(true);
day_reactive::bind(
move || (sel.key)(),
move |key: &Option<u64>| {
let initial = first.replace(false);
let picked = user_pick.take() == Some(*key);
reveal.set(!initial && !picked && key.is_some());
apply();
},
);
}
if let Some(rows) = self.selected_rows {
day_reactive::bind(
move || rows(),
move |rows: &Vec<usize>| {
let rows = rows.clone();
day_reactive::on_main(move || day_core::list_set_selected(node, rows));
},
);
}
node
}
}
#[cfg(feature = "model")]
mod model_rows {
use super::*;
use day_model::{Elem, Identified, Keyed, Source, Store};
pub struct ModelSlot<T: 'static> {
store: Store<Keyed<T>>,
cur: Signal<u64>,
}
impl<T> Clone for ModelSlot<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ModelSlot<T> {}
impl<T: Identified + Clone + 'static> ModelSlot<T> {
pub fn for_key(store: Store<Keyed<T>>, key: u64) -> ModelSlot<T> {
ModelSlot {
store,
cur: Signal::new(key),
}
}
pub fn rebind_key(&self, key: u64) {
self.cur.set_if_changed(key);
}
}
impl<T: Identified + 'static> ModelSlot<T> {
pub fn key(self) -> u64 {
self.cur.get()
}
pub fn item(self) -> Elem<T> {
self.store.elem(self.cur.get())
}
fn elem_now(self) -> Elem<T> {
self.store.elem(self.cur.get_untracked())
}
}
impl<T: Identified + 'static> Source<T> for ModelSlot<T> {
const DYNAMIC: bool = true;
fn track_extra(self) {
self.cur.track();
}
fn path(self) -> day_model::Path {
self.elem_now().path()
}
fn node(self) -> day_model::NodeId {
self.elem_now().node()
}
fn components(self, out: &mut Vec<u64>) {
self.elem_now().components(out);
}
fn with_value_untracked<R>(self, f: impl FnOnce(Option<&T>) -> R) -> R {
self.elem_now().with_value_untracked(f)
}
fn update_value(self, f: impl FnOnce(&mut T)) -> bool {
self.elem_now().update_value(f)
}
fn bump_version(self) {
self.elem_now().bump_version();
}
}
impl<T: Identified + 'static> SelectionKey for Elem<T> {
fn selection_key(&self) -> u64 {
self.key()
}
}
impl<T: Identified + Clone + 'static> RowSource for Store<Keyed<T>> {
type Slot = ModelSlot<T>;
type Ref = Elem<T>;
type Conn = StoreConn<T>;
fn connect(self) -> StoreConn<T> {
StoreConn {
store: self,
rows: None,
keys: RefCell::new(Vec::new()),
}
}
}
pub struct Rows<T: 'static> {
store: Store<Keyed<T>>,
f: Rc<dyn Fn() -> Vec<u64>>,
}
impl<T: Identified + Clone + 'static> RowSource for Rows<T> {
type Slot = ModelSlot<T>;
type Ref = Elem<T>;
type Conn = StoreConn<T>;
fn connect(self) -> StoreConn<T> {
StoreConn {
store: self.store,
rows: Some(self.f),
keys: RefCell::new(Vec::new()),
}
}
}
pub trait StoreRows<T: 'static> {
fn rows(self, f: impl Fn() -> Vec<u64> + 'static) -> Rows<T>;
}
impl<T: Identified + Clone + 'static> StoreRows<T> for Store<Keyed<T>> {
fn rows(self, f: impl Fn() -> Vec<u64> + 'static) -> Rows<T> {
Rows {
store: self,
f: Rc::new(f),
}
}
}
pub struct StoreConn<T: 'static> {
store: Store<Keyed<T>>,
rows: Option<Rc<dyn Fn() -> Vec<u64>>>,
keys: RefCell<Vec<u64>>,
}
impl<T: Identified + Clone + 'static> RowConn for StoreConn<T> {
type Slot = ModelSlot<T>;
type Ref = Elem<T>;
fn refresh(&self) -> Vec<u64> {
let keys = match &self.rows {
Some(f) => f(),
None => self.store.keys(),
};
*self.keys.borrow_mut() = keys.clone();
keys
}
fn len(&self) -> usize {
self.keys.borrow().len()
}
fn token_at(&self, index: usize) -> u64 {
self.keys.borrow().get(index).copied().unwrap_or(0)
}
fn tokens_now(&self) -> Vec<u64> {
self.keys.borrow().clone()
}
fn slot_at(&self, index: usize) -> Option<ModelSlot<T>> {
let key = self.keys.borrow().get(index).copied()?;
Some(ModelSlot {
store: self.store,
cur: Signal::new(key),
})
}
fn rebind(&self, slot: &ModelSlot<T>, index: usize) {
if let Some(key) = self.keys.borrow().get(index).copied() {
slot.cur.set_if_changed(key);
}
}
fn select_ref(&self, index: usize) -> Option<Elem<T>> {
let key = self.keys.borrow().get(index).copied()?;
Some(self.store.elem(key))
}
fn values_flow_by_reload(&self) -> bool {
false
}
fn commit_move(&self, from: usize, to: usize) {
let mut keys = self.keys.borrow_mut();
if from >= keys.len() || to >= keys.len() {
return;
}
let k = keys.remove(from);
keys.insert(to, k);
}
fn commit_delete(&self, index: usize) {
let mut keys = self.keys.borrow_mut();
if index >= keys.len() {
return;
}
keys.remove(index);
}
}
pub struct StoreTree<T: 'static> {
store: Store<Keyed<T>>,
children: Rc<dyn Fn(Option<u64>) -> Vec<u64>>,
}
pub trait StoreTrees<T: 'static> {
fn tree(self, children: impl Fn(Option<u64>) -> Vec<u64> + 'static) -> StoreTree<T>;
}
impl<T: Identified + Clone + 'static> StoreTrees<T> for Store<Keyed<T>> {
fn tree(self, children: impl Fn(Option<u64>) -> Vec<u64> + 'static) -> StoreTree<T> {
StoreTree {
store: self,
children: Rc::new(children),
}
}
}
impl<T: Identified + Clone + 'static> super::NodeSource for StoreTree<T> {
type Slot = ModelSlot<T>;
type Key = u64;
type Conn = StoreTreeConn<T>;
fn connect(self) -> StoreTreeConn<T> {
StoreTreeConn {
store: self.store,
children: self.children,
shape: RefCell::new(super::TreeShape::default()),
}
}
}
pub struct StoreTreeConn<T: 'static> {
store: Store<Keyed<T>>,
children: Rc<dyn Fn(Option<u64>) -> Vec<u64>>,
shape: RefCell<super::TreeShape>,
}
impl<T: Identified + Clone + 'static> super::TreeConn for StoreTreeConn<T> {
type Slot = ModelSlot<T>;
type Key = u64;
fn refresh(&self) -> Vec<(u64, Option<u64>)> {
let mut shape = super::TreeShape::default();
let mut seen = HashSet::new();
let mut stack: Vec<(Option<u64>, u64)> = (self.children)(None)
.into_iter()
.rev()
.map(|k| (None, k))
.collect();
while let Some((parent, key)) = stack.pop() {
if !seen.insert(key) {
debug_assert!(false, "day: cycle or duplicate key in store tree");
continue;
}
shape.tokens.push(key);
shape.parents.insert(key, parent);
shape.children.entry(parent).or_default().push(key);
for child in (self.children)(Some(key)).into_iter().rev() {
stack.push((Some(key), child));
}
}
let fp = shape.fingerprint();
*self.shape.borrow_mut() = shape;
fp
}
fn tokens_now(&self) -> Vec<u64> {
self.shape.borrow().tokens.clone()
}
fn children_len(&self, parent: Option<u64>) -> usize {
self.shape
.borrow()
.children
.get(&parent)
.map(|v| v.len())
.unwrap_or(0)
}
fn child_token(&self, parent: Option<u64>, index: usize) -> u64 {
self.shape
.borrow()
.children
.get(&parent)
.and_then(|v| v.get(index).copied())
.unwrap_or(0)
}
fn parent_of(&self, token: u64) -> Option<Option<u64>> {
self.shape.borrow().parents.get(&token).copied()
}
fn key_of(&self, token: u64) -> Option<u64> {
self.shape
.borrow()
.parents
.contains_key(&token)
.then_some(token)
}
fn token_of(&self, key: &u64) -> u64 {
*key
}
fn slot_for(&self, token: u64) -> Option<ModelSlot<T>> {
self.shape
.borrow()
.parents
.contains_key(&token)
.then(|| ModelSlot::for_key(self.store, token))
}
fn rebind(&self, slot: &ModelSlot<T>, token: u64) {
slot.rebind_key(token);
}
fn values_flow_by_reload(&self) -> bool {
false
}
}
}
#[cfg(feature = "model")]
pub use model_rows::{ModelSlot, Rows, StoreRows, StoreTree, StoreTreeConn, StoreTrees};
pub use day_spec::MoveVerdict;
pub trait NodeSource {
type Slot: Copy + 'static;
type Key: Clone + Hash + Eq + 'static;
type Conn: TreeConn<Slot = Self::Slot, Key = Self::Key>;
fn connect(self) -> Self::Conn;
}
pub trait TreeConn: 'static {
type Slot: Copy + 'static;
type Key: Clone + Hash + Eq + 'static;
fn refresh(&self) -> Vec<(u64, Option<u64>)>;
fn tokens_now(&self) -> Vec<u64>;
fn children_len(&self, parent: Option<u64>) -> usize;
fn child_token(&self, parent: Option<u64>, index: usize) -> u64;
fn parent_of(&self, token: u64) -> Option<Option<u64>>;
fn key_of(&self, token: u64) -> Option<Self::Key>;
fn token_of(&self, key: &Self::Key) -> u64;
fn slot_for(&self, token: u64) -> Option<Self::Slot>;
fn rebind(&self, slot: &Self::Slot, token: u64);
fn values_flow_by_reload(&self) -> bool;
}
pub fn branches<T, K>(
f: impl Fn() -> Vec<T> + 'static,
key_of: impl Fn(&T) -> K + 'static,
parent_of: impl Fn(&T) -> Option<K> + 'static,
) -> Branches<T, K>
where
T: Clone + 'static,
K: Clone + Hash + Eq + 'static,
{
Branches {
f: Rc::new(f),
key_of: Rc::new(key_of),
parent_of: Rc::new(parent_of),
}
}
type ParentOf<T, K> = Rc<dyn Fn(&T) -> Option<K>>;
pub struct Branches<T: 'static, K: 'static> {
f: Rc<dyn Fn() -> Vec<T>>,
key_of: Rc<dyn Fn(&T) -> K>,
parent_of: ParentOf<T, K>,
}
impl<T: Clone + 'static, K: Clone + Hash + Eq + 'static> NodeSource for Branches<T, K> {
type Slot = ItemSlot<T, K>;
type Key = K;
type Conn = BranchesConn<T, K>;
fn connect(self) -> BranchesConn<T, K> {
BranchesConn {
f: self.f,
key_of: self.key_of,
parent_of: self.parent_of,
snapshot: RefCell::new(Vec::new()),
shape: RefCell::new(TreeShape::default()),
}
}
}
#[derive(Default)]
struct TreeShape {
tokens: Vec<u64>,
children: std::collections::HashMap<Option<u64>, Vec<u64>>,
parents: std::collections::HashMap<u64, Option<u64>>,
index_of: std::collections::HashMap<u64, usize>,
}
impl TreeShape {
fn fingerprint(&self) -> Vec<(u64, Option<u64>)> {
self.tokens
.iter()
.map(|t| (*t, self.parents.get(t).copied().flatten()))
.collect()
}
}
pub struct BranchesConn<T: 'static, K: 'static> {
f: Rc<dyn Fn() -> Vec<T>>,
key_of: Rc<dyn Fn(&T) -> K>,
parent_of: ParentOf<T, K>,
snapshot: RefCell<Vec<T>>,
shape: RefCell<TreeShape>,
}
impl<T: Clone + 'static, K: Clone + Hash + Eq + 'static> TreeConn for BranchesConn<T, K> {
type Slot = ItemSlot<T, K>;
type Key = K;
fn refresh(&self) -> Vec<(u64, Option<u64>)> {
let its = (self.f)();
let mut shape = TreeShape::default();
let toks: Vec<u64> = its.iter().map(|t| key_token(&(self.key_of)(t))).collect();
let present: HashSet<u64> = toks.iter().copied().collect();
for (i, item) in its.iter().enumerate() {
let tok = toks[i];
let parent = (self.parent_of)(item)
.map(|k| key_token(&k))
.filter(|p| present.contains(p));
shape.tokens.push(tok);
shape.parents.insert(tok, parent);
shape.children.entry(parent).or_default().push(tok);
shape.index_of.insert(tok, i);
}
*self.snapshot.borrow_mut() = its;
let fp = shape.fingerprint();
*self.shape.borrow_mut() = shape;
fp
}
fn tokens_now(&self) -> Vec<u64> {
self.shape.borrow().tokens.clone()
}
fn children_len(&self, parent: Option<u64>) -> usize {
self.shape
.borrow()
.children
.get(&parent)
.map(|v| v.len())
.unwrap_or(0)
}
fn child_token(&self, parent: Option<u64>, index: usize) -> u64 {
self.shape
.borrow()
.children
.get(&parent)
.and_then(|v| v.get(index).copied())
.unwrap_or(0)
}
fn parent_of(&self, token: u64) -> Option<Option<u64>> {
self.shape.borrow().parents.get(&token).copied()
}
fn key_of(&self, token: u64) -> Option<K> {
let idx = self.shape.borrow().index_of.get(&token).copied()?;
self.snapshot.borrow().get(idx).map(|t| (self.key_of)(t))
}
fn token_of(&self, key: &K) -> u64 {
key_token(key)
}
fn slot_for(&self, token: u64) -> Option<ItemSlot<T, K>> {
let idx = self.shape.borrow().index_of.get(&token).copied()?;
let item = self.snapshot.borrow().get(idx).cloned()?;
let key = (self.key_of)(&item);
Some(ItemSlot {
sig: Signal::new(item),
key: Signal::new(key),
})
}
fn rebind(&self, slot: &ItemSlot<T, K>, token: u64) {
let Some(idx) = self.shape.borrow().index_of.get(&token).copied() else {
return; };
let Some(item) = self.snapshot.borrow().get(idx).cloned() else {
return;
};
slot.key.set((self.key_of)(&item));
slot.sig.set(item);
}
fn values_flow_by_reload(&self) -> bool {
true
}
}
pub struct Move<K: 'static> {
moved: TreeMoveFn<K>,
guard: Option<TreeGuardFn<K>>,
enabled: Reactive<bool>,
}
impl<K: 'static> Move<K> {
pub fn new(moved: impl Fn(K, Option<K>, Option<usize>) + 'static) -> Move<K> {
Move {
moved: Rc::new(moved),
guard: None,
enabled: Reactive::Const(true),
}
}
pub fn guard(
mut self,
g: impl Fn(&K, Option<&K>, Option<usize>) -> MoveVerdict + 'static,
) -> Move<K> {
self.guard = Some(Rc::new(g));
self
}
pub fn enabled<M>(mut self, on: impl IntoReactive<bool, M>) -> Move<K> {
self.enabled = on.into_reactive();
self
}
}
pub struct TreePiece<S: NodeSource> {
source: Option<S>,
build_row: Rc<dyn Fn(S::Slot) -> AnyPiece>,
row_height: RowHeight,
indent: Option<f64>,
section_headers: bool,
on_expansion: Option<ExpansionFn<S::Key>>,
expanded: Option<AnyBinding<HashSet<S::Key>>>,
expandable: Option<KeyPredicate<S::Key>>,
selected: Option<Rc<dyn Fn() -> Vec<S::Key>>>,
on_selection: Option<SelectionKeysFn<S::Key>>,
multi_select: bool,
movable: bool,
on_move: Option<TreeMoveFn<S::Key>>,
move_guard: Option<TreeGuardFn<S::Key>>,
type_ahead: Option<KeyTextFn<S::Key>>,
reveal: Option<AnyBinding<Option<S::Key>>>,
row_id: Option<KeyIdFn<S::Key>>,
row_menu: Option<RowMenuEntriesFn<S::Key>>,
}
type ExpansionFn<K> = Rc<dyn Fn(K, bool)>;
type TreeMoveFn<K> = Rc<dyn Fn(K, Option<K>, Option<usize>)>;
type TreeGuardFn<K> = Rc<dyn Fn(&K, Option<&K>, Option<usize>) -> MoveVerdict>;
type KeyPredicate<K> = Rc<dyn Fn(&K) -> bool>;
type SelectionKeysFn<K> = Rc<dyn Fn(Vec<K>)>;
type KeyTextFn<K> = Rc<dyn Fn(&K) -> String>;
type KeyIdFn<K> = Rc<dyn Fn(&K) -> day_core::ElementId>;
type RowMenuEntriesFn<K> = Rc<dyn Fn(&K) -> Vec<MenuEntry>>;
pub fn tree<S, P>(source: S, build_row: impl Fn(S::Slot) -> P + 'static) -> TreePiece<S>
where
S: NodeSource + 'static,
P: Piece,
{
TreePiece {
source: Some(source),
build_row: Rc::new(move |slot| AnyPiece::new(build_row(slot))),
row_height: RowHeight::Automatic,
indent: None,
section_headers: false,
on_expansion: None,
expanded: None,
expandable: None,
selected: None,
on_selection: None,
multi_select: false,
movable: false,
on_move: None,
move_guard: None,
type_ahead: None,
reveal: None,
row_id: None,
row_menu: None,
}
}
#[day_macros::builder(TreeBuilder<S>)]
impl<S: NodeSource + 'static> TreePiece<S> {
pub fn row_height(mut self, h: RowHeight) -> Self {
self.row_height = h;
self
}
pub fn indent(mut self, pts: f64) -> Self {
self.indent = Some(pts);
self
}
pub fn section_headers(mut self) -> Self {
self.section_headers = true;
self
}
pub fn on_expansion(mut self, f: impl Fn(S::Key, bool) + 'static) -> Self {
self.on_expansion = Some(Rc::new(f));
self
}
pub fn expanded(mut self, set: impl Binding<HashSet<S::Key>>) -> Self {
self.expanded = Some(AnyBinding::new(set));
self
}
pub fn expandable(mut self, f: impl Fn(&S::Key) -> bool + 'static) -> Self {
self.expandable = Some(Rc::new(f));
self
}
pub fn selected(mut self, f: impl Fn() -> Vec<S::Key> + 'static) -> Self {
self.selected = Some(Rc::new(f));
self
}
pub fn on_selection(mut self, f: impl Fn(Vec<S::Key>) + 'static) -> Self {
self.on_selection = Some(Rc::new(f));
self
}
pub fn multi_select(mut self) -> Self {
self.multi_select = true;
self
}
pub fn movable(mut self, mv: Move<S::Key>) -> Self {
self.movable = true;
self.on_move = Some(mv.moved);
let enabled = mv.enabled;
let guard = mv.guard;
self.move_guard = Some(Rc::new(move |k: &S::Key, p: Option<&S::Key>, i| {
if !enabled.get_untracked() {
return MoveVerdict::Deny;
}
match &guard {
Some(g) => g(k, p, i),
None => MoveVerdict::Allow,
}
}));
self
}
pub fn type_ahead(mut self, f: impl Fn(&S::Key) -> String + 'static) -> Self {
self.type_ahead = Some(Rc::new(f));
self
}
pub fn reveal(mut self, target: impl Binding<Option<S::Key>>) -> Self {
self.reveal = Some(AnyBinding::new(target));
self
}
pub fn row_id(mut self, f: impl Fn(&S::Key) -> day_core::ElementId + 'static) -> Self {
self.row_id = Some(Rc::new(f));
self
}
pub fn row_context_menu(mut self, f: impl Fn(&S::Key) -> Vec<MenuEntry> + 'static) -> Self {
self.row_menu = Some(Rc::new(f));
self
}
fn build_composed(mut self, cx: &mut BuildCx) -> RNode {
const CHEVRON_W: f64 = 18.0;
const DEFAULT_INDENT: f64 = 14.0;
let conn = Rc::new(self.source.take().expect("TreePiece built once").connect());
let open_sig: Signal<HashSet<u64>> = Signal::new(HashSet::new());
let expanded_sig = self.expanded;
let is_open: Rc<dyn Fn(u64) -> bool> = {
let conn = conn.clone();
match expanded_sig {
Some(sig) => Rc::new(move |t| match conn.key_of(t) {
Some(k) => sig.with(|s| s.contains(&k)),
None => false,
}),
None => Rc::new(move |t| open_sig.with(|s| s.contains(&t))),
}
};
let set_open: Rc<dyn Fn(u64, bool)> = {
let (conn, is_open, changed) =
(conn.clone(), is_open.clone(), self.on_expansion.clone());
Rc::new(move |t, on| {
if day_reactive::untrack(|| is_open(t)) == on {
return;
}
let Some(key) = conn.key_of(t) else {
return;
};
if let Some(sig) = expanded_sig {
sig.update(|s| {
if on {
s.insert(key.clone());
} else {
s.remove(&key);
}
});
} else {
open_sig.update(|s| {
if on {
s.insert(t);
} else {
s.remove(&t);
}
});
}
if let Some(f) = &changed {
f(key, on);
}
})
};
let expandable_of: Rc<dyn Fn(u64) -> bool> = {
let (conn, f) = (conn.clone(), self.expandable.clone());
Rc::new(move |tok| match &f {
Some(f) => conn.key_of(tok).map(|k| f(&k)).unwrap_or(false),
None => conn.children_len(Some(tok)) > 0,
})
};
fn visible<C: TreeConn + ?Sized>(
conn: &C,
is_open: &dyn Fn(u64) -> bool,
parent: Option<u64>,
depth: u16,
out: &mut Vec<(u64, u16)>,
) {
for i in 0..conn.children_len(parent) {
let t = conn.child_token(parent, i);
out.push((t, depth));
if is_open(t) {
visible(conn, is_open, Some(t), depth + 1, out);
}
}
}
let flatten: Rc<dyn Fn() -> Vec<(u64, u16)>> = {
let (conn, is_open) = (conn.clone(), is_open.clone());
Rc::new(move || {
let _shape = conn.refresh();
let mut rows = Vec::new();
visible(&*conn, &*is_open, None, 0, &mut rows);
rows
})
};
let step = self.indent.unwrap_or(DEFAULT_INDENT);
let build_row = {
let (conn, user_row, row_id) =
(conn.clone(), self.build_row.clone(), self.row_id.clone());
let (is_open, expandable_of, set_open) =
(is_open.clone(), expandable_of.clone(), set_open.clone());
let row_menu = self.row_menu.clone();
move |slot: ItemSlot<(u64, u16), u64>| -> AnyPiece {
let (token0, depth0) = day_reactive::untrack(|| slot.get());
let Some(user_slot) = conn.slot_for(token0) else {
return AnyPiece::new(crate::spacer());
};
let depth = Rc::new(std::cell::Cell::new(depth0 as f64));
let chevron = {
let (is_open, expandable_of) = (is_open.clone(), expandable_of.clone());
let (set_open, is_open_tap) = (set_open.clone(), is_open.clone());
let expandable_tap = expandable_of.clone();
crate::label(move || {
let t = slot.get().0;
if expandable_of(t) {
if is_open(t) { "â–¼" } else { "â–¶" }
} else {
""
}
.to_string()
})
.align(TextAlign::Center)
.width(CHEVRON_W)
.on_tap(move || {
let t = day_reactive::untrack(|| slot.get().0);
if expandable_tap(t) {
set_open(t, !day_reactive::untrack(|| is_open_tap(t)));
}
})
};
let content = crate::row((chevron, user_row(user_slot)));
let shell = TreeRowShell {
depth: depth.clone(),
step,
content: AnyPiece::new(content),
}
.tweak({
let (conn, row_id) = (conn.clone(), row_id.clone());
move |n| {
let id_conn = conn.clone();
let set_ids = move |tok: u64| {
if let Some(rid) = &row_id
&& let Some(k) = id_conn.key_of(tok)
{
with_tree(|t| t.set_id(n, rid(&k).as_str().to_owned()));
}
};
set_ids(token0);
watch(
move || slot.get(),
move |(tok, d), _| {
conn.rebind(&user_slot, *tok);
depth.set(*d as f64);
set_ids(*tok);
},
);
}
});
match &row_menu {
Some(f) => {
let (conn, f) = (conn.clone(), f.clone());
AnyPiece::new(shell.context_menu(move |_p: day_spec::Point| {
match conn.key_of(day_reactive::untrack(|| slot.get().0)) {
Some(k) => f(&k),
None => Vec::new(),
}
}))
}
None => AnyPiece::new(shell),
}
}
};
let section_header: Rc<dyn Fn(u64) -> bool> = {
let (conn, expandable, sections) =
(conn.clone(), expandable_of.clone(), self.section_headers);
Rc::new(move |t| sections && conn.parent_of(t) == Some(None) && expandable(t))
};
let scroll_sig: Signal<Option<usize>> = Signal::new(None);
let mut lst = list(
items(
{
let flatten = flatten.clone();
move || flatten()
},
|r: &(u64, u16)| r.0,
),
build_row,
)
.row_height(self.row_height)
.apply_if(self.multi_select, |l| l.multi_select())
.scroll_to_row(scroll_sig);
let selected_tokens = Signal::new(Vec::<u64>::new());
let selection_revision = Signal::new(0u64);
if self.on_selection.is_some() || (self.section_headers && self.selected.is_some()) {
let conn = conn.clone();
let header = section_header.clone();
let callback = self.on_selection.clone();
lst = lst.on_selection(move |toks: Vec<u64>| {
if toks.iter().any(|t| header(*t)) {
selection_revision.update(|r| *r += 1);
return;
}
selected_tokens.set(toks.clone());
if let Some(f) = &callback {
f(toks.iter().filter_map(|t| conn.key_of(*t)).collect());
}
});
}
let mut repair_selection: Option<Rc<dyn Fn() -> Vec<usize>>> = None;
if self.selected.is_some() || (self.section_headers && self.on_selection.is_some()) {
let (conn, flatten, selected) = (conn.clone(), flatten.clone(), self.selected.clone());
let section_header = section_header.clone();
let rows: Rc<dyn Fn() -> Vec<usize>> = Rc::new(move || {
let rows = flatten();
let want: HashSet<u64> = match &selected {
Some(sel) => sel().iter().map(|k| conn.token_of(k)).collect(),
None => selected_tokens.get().into_iter().collect(),
};
rows.iter()
.enumerate()
.filter(|(_, r)| want.contains(&r.0) && !section_header(r.0))
.map(|(i, _)| i)
.collect()
});
repair_selection = Some(rows.clone());
lst = lst.selected_rows(move || rows());
}
let node = lst.build(cx);
if let Some(rows) = repair_selection {
day_reactive::bind(
move || selection_revision.get(),
move |revision| {
if *revision != 0 {
let rows = day_reactive::untrack(|| rows());
day_reactive::on_main(move || day_core::list_set_selected(node, rows));
}
},
);
}
{
let (on_move, conn_ev, set_open) =
(self.on_move.clone(), conn.clone(), set_open.clone());
cx.on(node, move |ev| match ev {
Event::TreeExpanded { token, expanded } => set_open(*token, *expanded),
Event::TreeMove {
token,
parent,
index,
} => {
if let Some(f) = &on_move
&& let Some(key) = conn_ev.key_of(*token)
{
f(key, parent.and_then(|p| conn_ev.key_of(p)), *index);
}
}
_ => {}
});
}
let driver = TreeDriver {
section_header: {
let f = section_header.clone();
Box::new(move |t| f(t))
},
row_height: self.row_height,
children_len: {
let conn = conn.clone();
Box::new(move |p| conn.children_len(p))
},
child_token: {
let conn = conn.clone();
Box::new(move |p, i| conn.child_token(p, i))
},
expandable: {
let f = expandable_of.clone();
Box::new(move |t| f(t))
},
expanded: {
let is_open = is_open.clone();
Box::new(move |t| day_reactive::untrack(|| is_open(t)))
},
build: Box::new(|_tok, _anchor| TreeBuiltRow {
scope: Scope::child(),
rebind: Rc::new(|_| {}),
}),
type_select_text: Box::new(|_| String::new()),
resolve_row: {
let (conn, row_id) = (conn.clone(), self.row_id.clone());
Box::new(move |id| {
let rid = row_id.as_ref()?;
conn.tokens_now().into_iter().find(|t| {
conn.key_of(*t)
.map(|k| rid(&k).as_str() == id)
.unwrap_or(false)
})
})
},
row_menu: None,
moves: self.movable.then(|| {
let can = {
let (conn, expandable_of, guard) =
(conn.clone(), expandable_of.clone(), self.move_guard.clone());
move |tok: u64, parent: Option<u64>, index: Option<usize>| {
let Some(key) = conn.key_of(tok) else {
return MoveVerdict::Deny;
};
let parent_key = match parent {
Some(p) => {
if p == tok || !expandable_of(p) {
return MoveVerdict::Deny;
}
let mut cur = Some(p);
while let Some(c) = cur {
if c == tok {
return MoveVerdict::Deny;
}
cur = conn.parent_of(c).flatten();
}
match conn.key_of(p) {
Some(k) => Some(k),
None => return MoveVerdict::Deny,
}
}
None => None,
};
match &guard {
Some(g) => g(&key, parent_key.as_ref(), index),
None => MoveVerdict::Allow,
}
}
};
let can_commit = can.clone();
TreeMovesDriver {
can_move: Box::new(can),
moved: Box::new(move |t, p, i| {
if can_commit(t, p, i) == MoveVerdict::Deny {
return;
}
enqueue_event(
rnode_to_id(node),
Event::TreeMove {
token: t,
parent: p,
index: i,
},
);
}),
}
}),
};
install_tree(node, driver);
if let Some(sig) = self.reveal {
let (conn, set_open, flatten) = (conn.clone(), set_open.clone(), flatten.clone());
watch(
move || sig.get(),
move |key: &Option<S::Key>, _| {
let Some(key) = key else { return };
let tok = conn.token_of(key);
let mut cur = conn.parent_of(tok).flatten();
while let Some(p) = cur {
set_open(p, true);
cur = conn.parent_of(p).flatten();
}
let row = day_reactive::untrack(|| flatten())
.iter()
.position(|r| r.0 == tok);
if let Some(row) = row {
scroll_sig.set(Some(row));
}
},
);
}
node
}
}
struct TreeIndent {
depth: Rc<std::cell::Cell<f64>>,
step: f64,
}
impl Layout for TreeIndent {
fn measure(&self, cx: &mut dyn LayoutOps, children: &[RNode], p: Proposal) -> Size {
let pad = self.depth.get() * self.step;
let child_p = Proposal::new(p.width.map(|w| (w - pad).max(0.0)), p.height);
let s = match children.first() {
Some(&c) => cx.measure_child(c, child_p),
None => Size::ZERO,
};
Size::new(s.width + pad, s.height)
}
fn place(&self, cx: &mut dyn LayoutOps, children: &[RNode], bounds: Rect) {
if let Some(&c) = children.first() {
let pad = (self.depth.get() * self.step).min(bounds.size.width);
let inner = Size::new(bounds.size.width - pad, bounds.size.height);
let _ = cx.measure_child(c, Proposal::exact(inner));
cx.place_child(c, Rect::new(pad, 0.0, inner.width, inner.height));
}
}
}
struct TreeRowShell {
depth: Rc<std::cell::Cell<f64>>,
step: f64,
content: AnyPiece,
}
impl Piece for TreeRowShell {
fn build(self, cx: &mut BuildCx) -> RNode {
let w = cx.native(
kinds::CONTAINER,
&ContainerProps {
background: None,
corner_radius: 0.0,
clips: false,
role: None,
},
Rc::new(TreeIndent {
depth: self.depth,
step: self.step,
}),
Flex {
grow_w: true,
..Default::default()
},
Boundary::No,
);
cx.under(w, |cx| {
let _ = self.content.build(cx);
});
w
}
}
impl<S: NodeSource + 'static> Piece for TreePiece<S> {
fn build(mut self, cx: &mut BuildCx) -> RNode {
let cap = if self.section_headers && !self.multi_select {
day_spec::Cap::TreeSections
} else {
day_spec::Cap::Tree
};
if day_core::capability(cap) != day_spec::Support::Native {
return self.build_composed(cx);
}
let props = TreeProps {
section_headers: self.section_headers,
row_height: self.row_height,
selectable: self.selected.is_some() || self.on_selection.is_some(),
multi_select: self.multi_select,
movable: self.movable,
indent: self.indent,
};
let node = cx.leaf(
kinds::TREE,
&props,
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
);
let conn = Rc::new(self.source.take().expect("TreePiece built once").connect());
let open_tokens: Rc<RefCell<HashSet<u64>>> = Rc::new(RefCell::new(HashSet::new()));
let expandable_of: Rc<dyn Fn(u64) -> bool> = {
let (conn, f) = (conn.clone(), self.expandable.clone());
Rc::new(move |tok| match &f {
Some(f) => conn.key_of(tok).map(|k| f(&k)).unwrap_or(false),
None => conn.children_len(Some(tok)) > 0,
})
};
let section_header: Rc<dyn Fn(u64) -> bool> = {
let (conn, expandable, sections) =
(conn.clone(), expandable_of.clone(), self.section_headers);
Rc::new(move |t| sections && conn.parent_of(t) == Some(None) && expandable(t))
};
{
let (on_selection, expanded_sig) = (self.on_selection.clone(), self.expanded);
let (on_move, conn_ev, open) =
(self.on_move.clone(), conn.clone(), open_tokens.clone());
let changed = self.on_expansion.clone();
let header = section_header.clone();
cx.on(node, move |ev| match ev {
Event::TreeSelection(tokens) => {
if let Some(f) = &on_selection {
f(tokens
.iter()
.filter(|t| !header(**t))
.filter_map(|t| conn_ev.key_of(*t))
.collect());
}
}
Event::TreeExpanded { token, expanded } => {
let Some(key) = conn_ev.key_of(*token) else {
return;
};
let was = expanded_sig
.map(|sig| day_reactive::untrack(|| sig.with(|s| s.contains(&key))))
.unwrap_or_else(|| open.borrow().contains(token));
if was == *expanded {
return;
}
match &expanded_sig {
Some(sig) => {
if let Some(key) = conn_ev.key_of(*token) {
sig.update(|set| {
if *expanded {
set.insert(key.clone());
} else {
set.remove(&key);
}
});
}
}
None => {
if *expanded {
open.borrow_mut().insert(*token);
} else {
open.borrow_mut().remove(token);
}
day_core::tree_set_expanded(node, *token, *expanded);
}
}
if let Some(f) = &changed {
f(key, *expanded);
}
}
Event::TreeMove {
token,
parent,
index,
} => {
if let Some(f) = &on_move
&& let Some(key) = conn_ev.key_of(*token)
{
f(key, parent.and_then(|p| conn_ev.key_of(p)), *index);
}
}
_ => {}
});
}
let driver = TreeDriver {
section_header: {
let f = section_header.clone();
Box::new(move |t| f(t))
},
row_height: self.row_height,
children_len: {
let conn = conn.clone();
Box::new(move |p| conn.children_len(p))
},
child_token: {
let conn = conn.clone();
Box::new(move |p, i| conn.child_token(p, i))
},
expandable: {
let f = expandable_of.clone();
Box::new(move |t| f(t))
},
expanded: {
let open = open_tokens.clone();
Box::new(move |t| open.borrow().contains(&t))
},
build: {
let (conn, build_row, row_id) =
(conn.clone(), self.build_row.clone(), self.row_id.clone());
Box::new(move |token, anchor| {
let scope = Scope::child();
let (conn, build_row, row_id) =
(conn.clone(), build_row.clone(), row_id.clone());
let rebind = scope.enter(move || {
let Some(slot) = conn.slot_for(token) else {
return Rc::new(|_: u64| {}) as Rc<dyn Fn(u64)>;
};
let mut rowcx = BuildCx::new(anchor);
let root = build_row(slot).build(&mut rowcx);
if let Some(rid) = &row_id
&& let Some(k) = conn.key_of(token)
{
with_tree(|t| t.set_id(root, rid(&k).as_str().to_owned()));
}
Rc::new(move |tok: u64| {
conn.rebind(&slot, tok);
if let Some(rid) = &row_id
&& let Some(k) = conn.key_of(tok)
{
with_tree(|t| t.set_id(root, rid(&k).as_str().to_owned()));
}
}) as Rc<dyn Fn(u64)>
});
TreeBuiltRow { scope, rebind }
})
},
type_select_text: {
let (conn, f) = (conn.clone(), self.type_ahead.clone());
Box::new(move |tok| match &f {
Some(f) => conn.key_of(tok).map(|k| f(&k)).unwrap_or_default(),
None => String::new(),
})
},
resolve_row: {
let (conn, row_id) = (conn.clone(), self.row_id.clone());
Box::new(move |id| {
let rid = row_id.as_ref()?;
conn.tokens_now().into_iter().find(|t| {
conn.key_of(*t)
.map(|k| rid(&k).as_str() == id)
.unwrap_or(false)
})
})
},
row_menu: self.row_menu.clone().map(|f| {
let conn = conn.clone();
let last: Rc<RefCell<Option<Scope>>> = Rc::default();
{
let last = last.clone();
Scope::current().on_cleanup(move || {
if let Some(s) = last.borrow_mut().take() {
s.dispose();
}
});
}
Box::new(move |tok: u64| {
let Some(key) = conn.key_of(tok) else {
return Vec::new();
};
if let Some(s) = last.borrow_mut().take() {
s.dispose();
}
let scope = Scope::child();
let items = scope.enter(|| crate::menus::lower_menu_scoped(f(&key)));
*last.borrow_mut() = Some(scope);
items
}) as day_core::tree_driver::RowMenuFn
}),
moves: self.movable.then(|| {
let can = {
let (conn, expandable_of, guard) =
(conn.clone(), expandable_of.clone(), self.move_guard.clone());
move |tok: u64, parent: Option<u64>, index: Option<usize>| {
let Some(key) = conn.key_of(tok) else {
return MoveVerdict::Deny;
};
let parent_key = match parent {
Some(p) => {
if p == tok || !expandable_of(p) {
return MoveVerdict::Deny;
}
let mut cur = Some(p);
while let Some(c) = cur {
if c == tok {
return MoveVerdict::Deny;
}
cur = conn.parent_of(c).flatten();
}
match conn.key_of(p) {
Some(k) => Some(k),
None => return MoveVerdict::Deny,
}
}
None => None,
};
match &guard {
Some(g) => g(&key, parent_key.as_ref(), index),
None => MoveVerdict::Allow,
}
}
};
let can_commit = can.clone();
TreeMovesDriver {
can_move: Box::new(can),
moved: Box::new(move |t, p, i| {
if can_commit(t, p, i) == MoveVerdict::Deny {
return;
}
enqueue_event(
rnode_to_id(node),
Event::TreeMove {
token: t,
parent: p,
index: i,
},
);
}),
}
}),
};
install_tree(node, driver);
let apply_expansion = {
let (conn, open) = (conn.clone(), open_tokens.clone());
let expanded_sig = self.expanded;
move || {
let desired: HashSet<u64> = match &expanded_sig {
Some(sig) => {
let conn = &conn;
sig.get_untracked()
.iter()
.map(|k| conn.token_of(k))
.collect()
}
None => open.borrow().clone(),
};
let present: HashSet<u64> = conn.tokens_now().into_iter().collect();
let mut ordered: Vec<u64> = desired
.iter()
.filter(|t| present.contains(t))
.copied()
.collect();
let depth_of = |mut t: u64| {
let mut d = 0usize;
while let Some(Some(p)) = conn.parent_of(t) {
d += 1;
t = p;
}
d
};
ordered.sort_by_key(|t| depth_of(*t));
for t in ordered {
tree_set_expanded(node, t, true);
}
open.borrow_mut().clone_from(&desired);
}
};
let apply_selection = {
let (conn, selected) = (conn.clone(), self.selected.clone());
move || {
if let Some(sel) = &selected {
let keys = day_reactive::untrack(|| sel());
let toks: Vec<u64> = keys.iter().map(|k| conn.token_of(k)).collect();
tree_set_selected(node, toks);
}
}
};
{
let conn2 = conn.clone();
let initial = {
let conn = conn.clone();
day_reactive::untrack(move || conn.refresh())
};
tree_reload(node);
apply_expansion();
apply_selection();
let last: RefCell<Vec<(u64, Option<u64>)>> = RefCell::new(initial);
let (apply_expansion, apply_selection) =
(apply_expansion.clone(), apply_selection.clone());
let conn3 = conn.clone();
watch(
move || conn2.refresh(),
move |shape: &Vec<(u64, Option<u64>)>, _| {
let unchanged = !conn3.values_flow_by_reload() && *last.borrow() == *shape;
*last.borrow_mut() = shape.clone();
if unchanged {
return;
}
tree_reload(node);
apply_expansion();
apply_selection();
},
);
}
if let Some(sig) = self.expanded {
let (conn, open) = (conn.clone(), open_tokens.clone());
watch(
move || sig.get(),
move |set: &HashSet<S::Key>, _| {
let desired: HashSet<u64> = set.iter().map(|k| conn.token_of(k)).collect();
let current = open.borrow().clone();
for t in desired.difference(¤t) {
tree_set_expanded(node, *t, true);
}
for t in current.difference(&desired) {
tree_set_expanded(node, *t, false);
}
open.borrow_mut().clone_from(&desired);
},
);
}
if let Some(sel) = self.selected.clone() {
let conn = conn.clone();
watch(
move || sel(),
move |keys: &Vec<S::Key>, _| {
let toks: Vec<u64> = keys.iter().map(|k| conn.token_of(k)).collect();
tree_set_selected(node, toks);
},
);
}
if let Some(sig) = self.reveal {
let (conn, open, expanded_sig) = (conn.clone(), open_tokens.clone(), self.expanded);
watch(
move || sig.get(),
move |key: &Option<S::Key>, _| {
let Some(key) = key else { return };
let tok = conn.token_of(key);
let mut ancestors = Vec::new();
let mut cur = conn.parent_of(tok).flatten();
while let Some(p) = cur {
ancestors.push(p);
cur = conn.parent_of(p).flatten();
}
for p in ancestors.iter().rev() {
tree_set_expanded(node, *p, true);
open.borrow_mut().insert(*p);
if let Some(sig) = &expanded_sig
&& let Some(k) = conn.key_of(*p)
{
sig.update(|set| {
set.insert(k.clone());
});
}
}
tree_reveal(node, tok);
},
);
}
node
}
}
pub fn test_host<P: Piece>(content: impl Fn() -> P + 'static) -> impl Piece {
let content = Rc::new(content);
piece_fn(move |cx| {
let generation = Signal::new(0u64);
day_core::conformance::set_host(Some(Rc::new(move || generation.update(|g| *g += 1))));
Scope::current().on_cleanup(|| day_core::conformance::set_host(None));
each(
items(move || vec![generation.get()], |g: &u64| *g),
move |_| match day_core::conformance::active() {
Some(case) => guarded_page(case),
None => AnyPiece::new(content()),
},
)
.build(cx)
})
}
pub fn guarded_page(case: day_core::conformance::Case) -> AnyPiece {
AnyPiece::new(piece_fn(move |cx| {
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
case.build_page().map(|page| page.build(&mut *cx))
}));
match built {
Ok(Some(root)) => root,
Ok(None) => crate::label("").build(cx),
Err(payload) => {
let message = day_core::conformance::panic_message(payload.as_ref());
day_core::conformance::note_page_panic(message.clone());
crate::label(format!("panicked: {message}")).build(cx)
}
}
}))
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::conformance::{Case, Drive, Fail};
use day_geometry::Insets;
use day_reactive::Signal;
use day_spec::props::RowHeight;
use day_spec::{Cap, Font, kinds};
use crate::*;
fn names_list(names: Signal<Vec<String>>) -> List<Items<String, String>> {
list(
items(move || names.get(), |n: &String| n.clone()),
|row: ItemSlot<String, String>| {
label(move || row.get())
.id_of(move || day_core::id!(conformance_row, row.get()))
.padding(Insets::symmetric(12.0, 6.0))
},
)
.row_height(RowHeight::Uniform(32.0))
}
fn names(list: &[&str]) -> Signal<Vec<String>> {
Signal::new(list.iter().map(|s| s.to_string()).collect())
}
#[day_macros::test(day_core)]
fn list_items_show_their_parts() -> Case {
Case::new()
.proves(kinds::LIST_ITEM)
.proves_cap(Cap::ListItem)
.requires(Cap::ListItem)
.page(|| {
let done = Signal::new(false);
let rows = names(&["Milk", "Bread"]);
column((
button("Done")
.action(move || done.update(|d| *d = !*d))
.id(day_core::id!(toggle_done)),
list(
items(move || rows.get(), |n: &String| n.clone()),
move |row: ItemSlot<String, String>| {
list_item(move || row.get())
.subtitle("aisle 4")
.checked(move || done.get())
.id_of(move || day_core::id!(conformance_item, row.get()))
},
)
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.assert_visible("conformance_item:Milk").await?;
d.tap("toggle_done").await?;
d.assert_visible("conformance_item:Bread").await
})
}
#[day_macros::test(day_core)]
fn list_rows_realize() -> Case {
Case::new()
.proves_duty("attach_list")
.proves_duty("adopt")
.proves(kinds::LIST)
.proves(kinds::LIST_CELL)
.page(|| {
names_list(names(&["Alpha", "Beta", "Gamma"]))
.id(day_core::id!(list))
.height(200.0)
})
.drive(|d: Drive| async move {
d.assert_text("conformance_row:Alpha", "Alpha").await?;
d.assert_text("conformance_row:Gamma", "Gamma").await
})
}
#[day_macros::test(day_core)]
fn list_follows_source() -> Case {
Case::new()
.proves_duty("release")
.proves(kinds::LIST)
.page(|| {
let rows = names(&["Alpha", "Beta"]);
column((
button("Add")
.action(move || rows.update(|v| v.push("Delta".into())))
.id(day_core::id!(add)),
button("Drop")
.action(move || rows.update(|v| v.retain(|n| n != "Alpha")))
.id(day_core::id!(drop)),
names_list(rows).id(day_core::id!(list)).height(200.0),
))
})
.drive(|d: Drive| async move {
d.assert_missing("conformance_row:Delta").await?;
d.tap("add").await?;
d.assert_text("conformance_row:Delta", "Delta").await?;
d.tap("drop").await?;
d.assert_missing("conformance_row:Alpha").await?;
d.assert_text("conformance_row:Beta", "Beta").await
})
}
#[day_macros::test(day_core)]
fn list_append_then_scroll() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows = names(&[]);
let scroll = Signal::new(None);
column((
button("Append").id(day_core::id!(append)).action(move || {
rows.update(|rows| rows.push(format!("Item {}", rows.len())));
scroll.set(Some(rows.with(|rows| rows.len() - 1)));
}),
names_list(rows)
.scroll_to_row(scroll)
.id(day_core::id!(list))
.height(120.0),
))
})
.drive(|d: Drive| async move {
for index in 0..8 {
d.tap("append").await?;
d.assert_visible(&format!("conformance_row:Item%20{index}"))
.await?;
}
Ok(())
})
}
const SIZED: [(&str, Font, &str); 5] = [
("small", Font::Caption2, "Small print"),
("large", Font::LargeTitle, "Large"),
("body", Font::Body, "Body text"),
(
"wrap",
Font::Footnote,
"A footnote long enough to wrap onto three lines or more in a narrow list, \
whatever the platform's type scale, so its row is several lines tall",
),
("end", Font::Body, "End"),
];
const GROWN: &str = "Body text that grew long enough to wrap onto several lines, \
the way a row does when its content changes after it was first sized";
fn sized_list(grown: Signal<bool>) -> List<Items<&'static str, &'static str>> {
list(
items(|| SIZED.iter().map(|r| r.0).collect(), |k: &&str| *k),
move |slot: ItemSlot<&'static str, &'static str>| {
let arm = |key: &'static str, font: Font, text: &'static str| {
when(
move || slot.get() == key,
move || {
label(move || {
if key == "body" && grown.get() {
GROWN.to_string()
} else {
text.to_string()
}
})
.font(font)
.id_of(move || day_core::id!(sized, slot.get()))
},
)
};
let [a, b, c, d, e] = SIZED;
column((
arm(a.0, a.1, a.2),
arm(b.0, b.1, b.2),
arm(c.0, c.1, c.2),
arm(d.0, d.1, d.2),
arm(e.0, e.1, e.2),
))
.spacing(0.0)
},
)
.row_height(RowHeight::Automatic)
.no_separators()
}
struct SizedRow {
key: &'static str,
content: f64,
row: f64,
top: f64,
}
fn sized_rows() -> Result<Vec<SizedRow>, Fail> {
day_core::try_with_tree(|t| {
SIZED
.iter()
.map(|(key, ..)| {
let id = format!("sized:{key}");
let node = t
.find_by_id(&id)
.ok_or_else(|| Fail(format!("no element {id:?}")))?;
let content = t
.node_frame(node)
.ok_or_else(|| Fail(format!("{id:?} has no Day frame")))?;
let mut anchor = node;
while t.node_kind(anchor) != Some(kinds::LIST_CELL) {
anchor = t
.parent_of(anchor)
.ok_or_else(|| Fail(format!("{id:?} is not inside a list cell")))?;
}
let row = t
.node_frame(anchor)
.ok_or_else(|| Fail(format!("{id:?}'s row has no Day frame")))?;
let native = t
.read_native(node)
.and_then(|n| n.frame)
.ok_or_else(|| Fail(format!("{id:?} has no native frame")))?;
Ok(SizedRow {
key,
content: content.size.height,
row: row.size.height,
top: native.origin.y,
})
})
.collect()
})
.unwrap_or_else(|| Err(Fail("the tree is busy".into())))
}
fn rows_fit() -> Result<Vec<(&'static str, f64)>, Fail> {
let rows = sized_rows()?;
let spare = |r: &SizedRow| (r.row - r.content) / 2.0;
let expected = |a: &SizedRow, b: &SizedRow| a.row - spare(a) + spare(b);
let chrome = (rows[1].top - rows[0].top) - expected(&rows[0], &rows[1]);
for pair in rows.windows(2) {
let (this, next) = (&pair[0], &pair[1]);
let pitch = next.top - this.top;
if pitch + 0.5 < this.content {
return Err(Fail(format!(
"row {:?} is {:.1} tall but the next row starts {pitch:.1} below it: the \
native row clips its content",
this.key, this.content
)));
}
let want = expected(this, next) + chrome;
if (pitch - want).abs() > 1.5 {
return Err(Fail(format!(
"row {:?} holds {:.1} of content in a {:.1} row, and the next row starts \
{pitch:.1} below it where {want:.1} was expected (the first row's chrome \
allowance is {chrome:.1}): the native row did not take its content's height",
this.key, this.content, this.row
)));
}
}
Ok(rows.into_iter().map(|r| (r.key, r.content)).collect())
}
fn height_of(rows: &[(&str, f64)], key: &str) -> f64 {
rows.iter().find(|r| r.0 == key).map_or(0.0, |r| r.1)
}
#[day_macros::test(day_core)]
fn list_rows_size_to_content() -> Case {
Case::new()
.proves(kinds::LIST)
.proves(kinds::LIST_CELL)
.page(|| {
let grown = Signal::new(false);
column((
button("Grow")
.action(move || grown.update(|g| *g = !*g))
.id(day_core::id!(grow)),
sized_list(grown)
.id(day_core::id!(list))
.width(240.0)
.height(480.0),
))
})
.drive(|d: Drive| async move {
d.assert_visible("sized:end").await?;
d.shot("sized-rows").await?;
let rows = rows_fit()?;
let (small, large) = (height_of(&rows, "small"), height_of(&rows, "large"));
d.check(
large > small * 1.5,
"a large-title row is taller than a caption row",
)?;
let body = height_of(&rows, "body");
d.check(
height_of(&rows, "wrap") > body * 1.5,
"the footnote wrapped",
)?;
d.tap("grow").await?;
d.wait_idle().await?;
d.pause(0.3).await?;
d.shot("sized-rows-grown").await?;
let grew = rows_fit()?;
d.check(height_of(&grew, "body") > body * 1.5, "the body row grew")?;
d.tap("grow").await?;
d.wait_idle().await?;
d.pause(0.3).await?;
let shrank = rows_fit()?;
d.check(
(height_of(&shrank, "body") - body).abs() < 1.0,
"the body row shrank back",
)
})
}
#[day_macros::test(day_core)]
fn list_select_and_activate() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let picked = Signal::new(String::new());
let opened = Signal::new(String::new());
column((
label(move || format!("picked {}", picked.get())).id(day_core::id!(picked)),
label(move || format!("opened {}", opened.get())).id(day_core::id!(opened)),
names_list(names(&["Alpha", "Beta", "Gamma"]))
.on_select(move |k| picked.set(k))
.on_activate(move |k| opened.set(k))
.id(day_core::id!(list))
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.select("list", 1).await?;
d.assert_text("picked", "picked Beta").await?;
d.activate("list", 2).await?;
d.assert_text("opened", "opened Gamma").await
})
}
#[day_macros::test(day_core)]
fn list_reorder_moves_row() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows = names(&["Alpha", "Beta", "Gamma"]);
column((
label(move || rows.get().join(",")).id(day_core::id!(order)),
names_list(rows)
.reorderable(Reorder::new(move |from, to| {
rows.update(|v| {
let item = v.remove(from);
v.insert(to.min(v.len()), item);
})
}))
.id(day_core::id!(list))
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.reorder("list", 0, 2).await?;
d.assert_text("order", "Beta,Gamma,Alpha").await?;
d.assert_text("conformance_row:Alpha", "Alpha").await
})
}
#[day_macros::test(day_core)]
fn list_delete_row() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows = names(&["Alpha", "Beta", "Gamma"]);
column((
label(move || rows.get().join(",")).id(day_core::id!(order)),
names_list(rows)
.deletable(Delete::new(move |at| {
rows.update(|v| {
v.remove(at);
})
}))
.id(day_core::id!(list))
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.delete_row("list", 1).await?;
d.assert_text("order", "Alpha,Gamma").await?;
d.assert_missing("conformance_row:Beta").await
})
}
#[day_macros::test(day_core)]
fn list_swipe_action() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows = names(&["Alpha", "Beta", "Gamma"]);
let flagged = Signal::new(String::new());
column((
label(move || format!("flagged {}", flagged.get())).id(day_core::id!(flagged)),
names_list(rows)
.swipe_trailing(move |at| {
let name = rows.get_untracked()[at].clone();
vec![swipe_action("Flag").action(move || flagged.set(name.clone()))]
})
.id(day_core::id!(list))
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.swipe_row("list", 2, false, 0).await?;
d.assert_text("flagged", "flagged Gamma").await
})
}
#[day_macros::test(day_core)]
fn list_scroll_to_row() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows: Signal<Vec<String>> =
Signal::new((0..300).map(|n| format!("Row {n}")).collect());
let jump: Signal<Option<usize>> = Signal::new(None);
column((
button("Jump")
.action(move || jump.set(Some(250)))
.id(day_core::id!(jump)),
names_list(rows)
.scroll_to_row(jump)
.id(day_core::id!(list))
.height(200.0),
))
})
.drive(|d: Drive| async move {
d.assert_missing("conformance_row:Row%20250").await?;
d.tap("jump").await?;
d.assert_text("conformance_row:Row%20250", "Row 250").await
})
}
struct RowAlive(std::rc::Rc<std::cell::Cell<usize>>);
impl RowAlive {
fn new(live: &std::rc::Rc<std::cell::Cell<usize>>) -> Self {
live.set(live.get() + 1);
RowAlive(live.clone())
}
}
impl Drop for RowAlive {
fn drop(&mut self) {
self.0.set(self.0.get().saturating_sub(1));
}
}
const ROWS_ALIVE_BOUND: usize = 256;
#[day_macros::test(day_core)]
fn list_recycles_distant_rows() -> Case {
let built = std::rc::Rc::new(std::cell::Cell::new(0usize));
let live = std::rc::Rc::new(std::cell::Cell::new(0usize));
let (count, alive) = (built.clone(), live.clone());
Case::new()
.proves(kinds::LIST)
.proves_cap(Cap::ListRecycling)
.timeout(180.0)
.requires_support("native list recycling", || {
if day_core::capability(Cap::ListRecycling) == day_spec::Support::Native {
day_spec::Support::Native
} else {
day_spec::Support::Unsupported
}
})
.page(move || {
count.set(0);
alive.set(0);
let (count, alive) = (count.clone(), alive.clone());
let rows = Signal::new((0..4000).collect::<Vec<usize>>());
let jump = Signal::new(None);
let next = Signal::new(40usize);
let pressed = Signal::new(String::new());
column((
button("Far").id(day_core::id!(far)).action(move || {
jump.set(Some(next.get_untracked()));
next.update(|n| *n += 40);
}),
button("Back")
.id(day_core::id!(back))
.action(move || jump.set(Some(0))),
label(move || pressed.get()).id(day_core::id!(pressed)),
list(items(move || rows.get(), |n: &usize| *n), move |slot| {
count.set(count.get() + 1);
let guard = RowAlive::new(&alive);
button(move || {
let _ = &guard;
format!("Row {}", slot.get())
})
.id_of(move || day_core::id!(conformance_row, slot.key()))
.action(move || pressed.set(format!("Pressed {}", slot.get())))
})
.row_height(RowHeight::Uniform(32.0))
.scroll_to_row(jump)
.height(160.0),
))
})
.drive(move |d: Drive| {
let (built, live) = (built.clone(), live.clone());
async move {
d.assert_text("conformance_row:0", "Row 0").await?;
for row in (40..=3600).step_by(40) {
d.tap("far").await?;
d.assert_text(&format!("conformance_row:{row}"), &format!("Row {row}"))
.await?;
d.assert_missing("conformance_row:1").await?;
d.tap(&format!("conformance_row:{row}")).await?;
d.assert_text("pressed", &format!("Pressed {row}")).await?;
d.tap("back").await?;
d.assert_text("conformance_row:0", "Row 0").await?;
d.tap("conformance_row:0").await?;
d.assert_text("pressed", "Pressed 0").await?;
}
d.wait_idle().await?;
d.pause(0.3).await?;
d.check(
live.get() <= ROWS_ALIVE_BOUND,
&format!(
"{} rows alive after 180 jumps of a five-row viewport ({} built): \
discarded cells keep their rows",
live.get(),
built.get()
),
)
}
})
}
#[day_macros::test(day_core)]
fn list_releases_discarded_rows() -> Case {
const TOTAL: usize = 3000;
const STRIDE: usize = 1237;
let built = std::rc::Rc::new(std::cell::Cell::new(0usize));
let live = std::rc::Rc::new(std::cell::Cell::new(0usize));
let (count, alive) = (built.clone(), live.clone());
Case::new()
.proves(kinds::LIST)
.proves_cap(Cap::ListRecycling)
.timeout(90.0)
.requires_support("native list recycling", || {
if day_core::capability(Cap::ListRecycling) == day_spec::Support::Native {
day_spec::Support::Native
} else {
day_spec::Support::Unsupported
}
})
.page(move || {
count.set(0);
alive.set(0);
let (count, alive) = (count.clone(), alive.clone());
let rows = Signal::new((0..TOTAL).collect::<Vec<usize>>());
let jump = Signal::new(None);
let at = Signal::new(0usize);
let pressed = Signal::new(String::new());
column((
button("Far").id(day_core::id!(far)).action(move || {
let next = (at.get_untracked() + STRIDE) % TOTAL;
at.set(next);
jump.set(Some(next));
}),
button("Shrink")
.id(day_core::id!(shrink))
.action(move || rows.set((0..5).collect())),
button("Regrow")
.id(day_core::id!(regrow))
.action(move || rows.set((0..TOTAL).collect())),
button("Rotate")
.id(day_core::id!(rotate))
.action(move || rows.update(|r| r.rotate_left(7))),
label(move || pressed.get()).id(day_core::id!(pressed)),
list(items(move || rows.get(), |n: &usize| *n), move |slot| {
count.set(count.get() + 1);
let guard = RowAlive::new(&alive);
button(move || {
let _ = &guard;
format!("Row {}", slot.get())
})
.id_of(move || day_core::id!(conformance_row, slot.key()))
.action(move || pressed.set(format!("Pressed {}", slot.get())))
})
.row_height(RowHeight::Uniform(32.0))
.scroll_to_row(jump)
.height(160.0),
))
})
.drive(move |d: Drive| {
let (built, live) = (built.clone(), live.clone());
async move {
d.assert_text("conformance_row:0", "Row 0").await?;
let (mut at, mut rotation) = (0usize, 0usize);
let key_at = |i: usize, rotation: usize| (i + rotation) % TOTAL;
for _ in 0..10 {
at = (at + STRIDE) % TOTAL;
d.tap("far").await?;
let key = key_at(at, rotation);
d.assert_text(&format!("conformance_row:{key}"), &format!("Row {key}"))
.await?;
d.tap(&format!("conformance_row:{key}")).await?;
d.assert_text("pressed", &format!("Pressed {key}")).await?;
d.tap("far").await?;
d.tap("far").await?;
at = (at + 2 * STRIDE) % TOTAL;
let key = key_at(at, rotation);
d.assert_text(&format!("conformance_row:{key}"), &format!("Row {key}"))
.await?;
d.tap("shrink").await?;
d.assert_text("conformance_row:3", "Row 3").await?;
if key >= 5 {
d.assert_missing(&format!("conformance_row:{key}")).await?;
}
d.tap("regrow").await?;
d.tap("rotate").await?;
rotation = 7;
at = (at + STRIDE) % TOTAL;
d.tap("far").await?;
let key = key_at(at, rotation);
d.assert_text(&format!("conformance_row:{key}"), &format!("Row {key}"))
.await?;
}
d.wait_idle().await?;
d.pause(0.3).await?;
d.check(
live.get() <= ROWS_ALIVE_BOUND,
&format!(
"{} rows alive after the churn ({} built): discarded cells keep \
their rows",
live.get(),
built.get()
),
)
}
})
}
#[day_macros::test(day_core)]
fn list_shrink_and_regrow() -> Case {
Case::new()
.proves(kinds::LIST)
.page(|| {
let rows = Signal::new((0..300).map(|n| format!("Row {n}")).collect());
let jump = Signal::new(None);
column((
button("Jump")
.id(day_core::id!(jump))
.action(move || jump.set(Some(250))),
button("Clear")
.id(day_core::id!(clear))
.action(move || rows.set(Vec::new())),
button("Restore")
.id(day_core::id!(restore))
.action(move || {
rows.set(vec!["New first".into(), "New last".into()]);
jump.set(Some(1));
}),
names_list(rows).scroll_to_row(jump).height(160.0),
))
})
.drive(|d: Drive| async move {
d.tap("jump").await?;
d.assert_text("conformance_row:Row%20250", "Row 250")
.await?;
d.tap("clear").await?;
d.assert_missing("conformance_row:Row%20250").await?;
d.tap("restore").await?;
d.assert_text("conformance_row:New%20first", "New first")
.await?;
d.assert_text("conformance_row:New%20last", "New last")
.await
})
}
type Node = (String, Option<String>);
fn names_tree(rows: Signal<Vec<Node>>) -> TreePiece<Branches<Node, String>> {
tree(
branches(
move || rows.get(),
|n: &Node| n.0.clone(),
|n: &Node| n.1.clone(),
),
|row: ItemSlot<Node, String>| {
label(move || row.get().0)
.id_of(move || day_core::id!(conformance_row, row.get().0))
.padding(Insets::symmetric(8.0, 4.0))
},
)
.row_height(RowHeight::Uniform(28.0))
.row_id(|k: &String| day_core::id!(tree_row, k))
}
fn fruit() -> Signal<Vec<Node>> {
let n = |name: &str, parent: Option<&str>| (name.to_string(), parent.map(str::to_owned));
Signal::new(vec![
n("Fruit", None),
n("Apple", Some("Fruit")),
n("Pear", Some("Fruit")),
n("Nuts", None),
])
}
#[day_macros::test(day_core)]
fn tree_rows_disclose() -> Case {
Case::new()
.proves_duty("attach_tree")
.proves_cap(Cap::Tree)
.proves(kinds::TREE)
.requires(Cap::Tree)
.page(|| names_tree(fruit()).id(day_core::id!(tree)).height(240.0))
.drive(|d: Drive| async move {
d.assert_text("conformance_row:Fruit", "Fruit").await?;
d.assert_text("conformance_row:Nuts", "Nuts").await?;
d.assert_missing("conformance_row:Apple").await?;
d.expand("tree", "tree_row:Fruit", true).await?;
d.assert_text("conformance_row:Apple", "Apple").await?;
d.expand("tree", "tree_row:Fruit", false).await?;
d.assert_missing("conformance_row:Apple").await
})
}
#[day_macros::test(day_core)]
fn tree_expanded_signal() -> Case {
Case::new()
.proves(kinds::TREE)
.requires(Cap::Tree)
.page(|| {
let open = Signal::new(std::collections::HashSet::from(["Fruit".to_string()]));
names_tree(fruit())
.expanded(open)
.id(day_core::id!(tree))
.height(240.0)
})
.drive(|d: Drive| async move { d.assert_text("conformance_row:Pear", "Pear").await })
}
#[day_macros::test(day_core)]
fn tree_section_expansion_contract() -> Case {
Case::new()
.proves_duty("attach_tree")
.proves_cap(Cap::TreeSections)
.proves(kinds::TREE)
.requires(Cap::TreeSections)
.page(|| {
let rows = fruit();
let open = Signal::new(std::collections::HashSet::from(["Fruit".to_owned()]));
let changes = Signal::new(0usize);
column((
label(move || {
format!("{}:{}", open.with(|s| s.contains("Fruit")), changes.get())
})
.id(day_core::id!(state)),
button("restore")
.id(day_core::id!(restore))
.action(move || {
open.set(std::collections::HashSet::from(["Fruit".to_owned()]))
}),
button("filter")
.id(day_core::id!(filter))
.action(move || rows.update(|r| r.retain(|n| n.0 != "Apple"))),
names_tree(rows)
.section_headers()
.expanded(open)
.on_expansion(move |_, _| changes.update(|n| *n += 1))
.id(day_core::id!(tree))
.height(240.0),
))
})
.drive(|d: Drive| async move {
d.assert_text("state", "true:0").await?;
d.assert_text("conformance_row:Apple", "Apple").await?;
d.expand("tree", "tree_row:Fruit", false).await?;
d.assert_text("state", "false:1").await?;
d.assert_missing("conformance_row:Apple").await?;
d.expand("tree", "tree_row:Fruit", false).await?;
d.assert_text("state", "false:1").await?;
d.tap("filter").await?;
d.assert_missing("conformance_row:Pear").await?;
d.tap("restore").await?;
d.assert_text("conformance_row:Pear", "Pear").await?;
d.assert_text("state", "true:1").await
})
}
#[day_macros::test(day_core)]
fn tree_move_reparents() -> Case {
Case::new()
.proves_cap(Cap::TreeMove)
.proves(kinds::TREE)
.requires(Cap::Tree)
.page(|| {
let rows = fruit();
let open = Signal::new(std::collections::HashSet::from([
"Fruit".to_string(),
"Nuts".to_string(),
]));
column((
label(move || {
rows.get()
.iter()
.map(|(n, p)| format!("{n}<{}", p.clone().unwrap_or_default()))
.collect::<Vec<_>>()
.join(",")
})
.id(day_core::id!(shape)),
names_tree(rows)
.expanded(open)
.expandable(|k: &String| k == "Fruit" || k == "Nuts")
.movable(Move::new(move |key, parent, _at| {
rows.update(|v| {
if let Some(n) = v.iter_mut().find(|n| n.0 == key) {
n.1 = parent;
}
})
}))
.id(day_core::id!(tree))
.height(240.0),
))
})
.drive(|d: Drive| async move {
d.tree_move("tree", "tree_row:Pear", Some("tree_row:Nuts"), None)
.await?;
d.assert_text("shape", "Fruit<,Apple<Fruit,Pear<Nuts,Nuts<")
.await?;
d.assert_text("conformance_row:Pear", "Pear").await
})
}
}