use std::cell::{Cell, RefCell};
use std::rc::Rc;
use day_core::*;
use day_reactive::{AnyBinding, Scope, Signal, bind, bind_seeded};
use day_spec::{Event, Size, kinds};
use crate::*;
pub fn navigate(path: &str) -> bool {
day_core::navigate(path)
}
pub fn nav_back() -> bool {
day_core::nav_back()
}
pub fn current_route() -> Option<String> {
day_core::current_route()
}
pub fn route_params() -> std::rc::Rc<Vec<(String, String)>> {
day_core::route_params()
}
pub fn route_param(name: &str) -> Option<String> {
day_core::route_param(name)
}
pub fn nav_link<M>(label: impl IntoText<M>, path: &str) -> Button {
let path = path.to_string();
button(label).action(move || {
let _ = day_core::navigate(&path);
})
}
pub trait Route: Clone + PartialEq + 'static {
fn key(&self) -> String;
fn from_key(key: &str) -> Option<Self>;
fn title(&self) -> String {
self.key()
}
}
impl Route for String {
fn key(&self) -> String {
self.clone()
}
fn from_key(key: &str) -> Option<Self> {
Some(key.to_string())
}
}
impl<R: Route> Route for Option<R> {
fn key(&self) -> String {
match self {
Some(r) => r.key(),
None => String::new(),
}
}
fn from_key(key: &str) -> Option<Self> {
if key.is_empty() {
Some(None)
} else {
R::from_key(key).map(Some)
}
}
}
#[macro_export]
macro_rules! routes {
($(#[$meta:meta])* $vis:vis enum $name:ident {
$($(#[$vmeta:meta])* $variant:ident => $key:literal $(($title:expr))?),+ $(,)?
}) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
$vis enum $name { $($(#[$vmeta])* $variant),+ }
impl $crate::Route for $name {
fn key(&self) -> String {
match self { $(Self::$variant => ($key).to_string()),+ }
}
fn from_key(key: &str) -> Option<Self> {
match key { $($key => Some(Self::$variant),)+ _ => None }
}
fn title(&self) -> String {
match self { $(Self::$variant => $crate::__route_title!($key $(, $title)?)),+ }
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __route_title {
($key:literal) => {
($key).to_string()
};
($key:literal, $title:expr) => {
$crate::route_title_text($title)
};
}
#[doc(hidden)]
pub fn route_title_text<M>(title: impl crate::IntoText<M>) -> String {
title.into_text().initial()
}
#[derive(Clone, Debug, Default)]
pub struct RoutePath {
segments: Vec<String>,
params: Vec<(String, String)>,
}
pub fn route(first: &impl Route) -> RoutePath {
RoutePath {
segments: vec![first.key()],
params: Vec::new(),
}
}
impl RoutePath {
pub fn then(mut self, next: &impl Route) -> Self {
self.segments.push(next.key());
self
}
pub fn param(mut self, name: &str, value: impl std::fmt::Display) -> Self {
self.params.push((name.to_string(), value.to_string()));
self
}
pub fn to_route(&self) -> String {
day_core::encode_route(&self.segments, &self.params)
}
pub fn navigate(&self) -> bool {
day_core::navigate(&self.to_route())
}
}
impl std::fmt::Display for RoutePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_route())
}
}
pub fn navigate_to(r: &impl Route) -> bool {
day_core::navigate(&day_core::encode_route(std::slice::from_ref(&r.key()), &[]))
}
pub fn nav_link_to<M>(label: impl IntoText<M>, path: RoutePath) -> Button {
let path = path.to_route();
button(label).action(move || {
let _ = day_core::navigate(&path);
})
}
type PopOwner = Rc<dyn Fn(bool)>;
#[derive(Clone)]
struct NavHostCx {
host: RNode,
sizes: Rc<RefCell<std::collections::HashMap<RNode, Size>>>,
owners: Rc<RefCell<Vec<PopOwner>>>,
split: Rc<Cell<bool>>,
guards: Rc<Cell<usize>>,
}
impl NavHostCx {
fn vote_guard(&self, on: bool) {
let was = self.guards.get();
let now = if on { was + 1 } else { was.saturating_sub(1) };
self.guards.set(now);
if (was == 0) != (now == 0) {
let host = self.host;
with_tree(|t| {
t.patch(
host,
Box::new(day_spec::props::NavPatch::GuardTop(now > 0)),
false,
)
});
}
}
}
day_reactive::tls_group! {
static NEXT_COVER_LAYER: Cell<u64> = const { Cell::new(1) };
static NAV_HOST_CX: RefCell<Vec<Option<NavHostCx>>> = const { RefCell::new(Vec::new()) };
static NAV_PAGE_ACTIVE: RefCell<Vec<Rc<dyn Fn() -> bool>>> = const { RefCell::new(Vec::new()) };
static ROUTED_ONE_OF_N: RefCell<std::collections::HashMap<usize, usize>> =
RefCell::new(std::collections::HashMap::new());
}
fn with_page_active<R>(gate: Rc<dyn Fn() -> bool>, f: impl FnOnce() -> R) -> R {
NAV_PAGE_ACTIVE.with(|s| s.borrow_mut().push(gate));
let r = f();
NAV_PAGE_ACTIVE.with(|s| {
s.borrow_mut().pop();
});
r
}
pub(crate) fn with_document_active<R>(gate: Rc<dyn Fn() -> bool>, f: impl FnOnce() -> R) -> R {
struct Restore(Option<Rc<dyn Fn() -> bool>>);
impl Drop for Restore {
fn drop(&mut self) {
NAV_PAGE_ACTIVE.with(|s| {
let mut s = s.borrow_mut();
s.pop();
if let Some(previous) = self.0.take() {
s.push(previous);
}
});
}
}
let previous = NAV_PAGE_ACTIVE.with(|s| {
let mut s = s.borrow_mut();
let previous = s.pop();
s.push(gate);
previous
});
let _restore = Restore(previous);
f()
}
fn with_nav_host<R>(cx: Option<NavHostCx>, f: impl FnOnce() -> R) -> R {
NAV_HOST_CX.with(|s| s.borrow_mut().push(cx));
let r = f();
NAV_HOST_CX.with(|s| {
s.borrow_mut().pop();
});
r
}
fn current_nav_host() -> Option<NavHostCx> {
NAV_HOST_CX.with(|s| s.borrow().last().cloned().flatten())
}
fn nav_page(
host: RNode,
props: &day_spec::props::NavPageProps,
sizes: &Rc<RefCell<std::collections::HashMap<RNode, Size>>>,
) -> RNode {
let mut cx = BuildCx::new(host);
let page = cx.native(
kinds::NAV_PAGE,
props,
Rc::new(PassThrough),
Flex::default(),
Boundary::Yes,
);
let sizes = sizes.clone();
cx.on(page, move |ev| {
if let Event::FrameChanged(sz) = ev {
let changed = sizes.borrow().get(&page) != Some(sz);
if changed {
sizes.borrow_mut().insert(page, *sz);
with_tree(|t| {
t.mark_needs_measure(page);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
});
page
}
fn list_layer_gate(
visible: Option<Signal<bool>>,
detail_open: Option<AnyBinding<bool>>,
pres: Option<Signal<day_spec::props::NavPresentation>>,
) -> Rc<dyn Fn() -> bool> {
Rc::new(move || {
let shown = visible.is_none_or(|v| v.try_get().unwrap_or(false));
let side_by_side = pres.is_some_and(|p| p.try_get().is_some_and(|p| p.is_split()));
shown && (side_by_side || detail_open.is_none_or(|d| !d.try_get().unwrap_or(false)))
})
}
#[derive(Clone)]
struct PageContext {
chrome: day_core::Chrome,
gate: Option<Rc<dyn Fn() -> bool>>,
column: day_spec::ToolbarColumn,
window: RNode,
}
impl PageContext {
fn current() -> PageContext {
PageContext {
chrome: day_core::current_chrome(),
gate: day_core::current_page_gate(),
column: day_core::current_page_column(),
window: day_core::current_page_window(),
}
}
fn enter(at: &Rc<RefCell<Option<PageContext>>>, piece: impl Piece + 'static) -> AnyPiece {
let at = at.clone();
AnyPiece::new(piece_fn(move |cx| {
let ctx = at.borrow().clone();
match ctx {
Some(PageContext {
chrome: day_core::Chrome::Page(page),
gate,
column,
window,
}) => day_core::with_page_in(page, gate, column, window, || piece.build(cx)),
_ => piece.build(cx),
}
}))
}
}
fn within_page_gate(gate: Rc<dyn Fn() -> bool>) -> Rc<dyn Fn() -> bool> {
match day_core::current_page_gate() {
Some(outer) => Rc::new(move || outer() && gate()),
None => gate,
}
}
fn destination_gate<K: Route, S: Binding<K> + 'static>(
selection: S,
key: String,
) -> Rc<dyn Fn() -> bool> {
Rc::new(move || selection.read().key() == key)
}
#[derive(Clone, Default)]
struct NavLayer {
order: Rc<Cell<u64>>,
depth: usize,
}
fn register_route_surface(
push: impl Fn(&str) -> bool + 'static,
pop: impl Fn(bool) -> bool + 'static,
current: impl Fn() -> String + 'static,
enter: impl Fn(&str) -> bool + 'static,
segments: impl Fn() -> Vec<String> + 'static,
) {
register_route_surface_in_layer(
Scope::current()
.use_context::<NavLayer>()
.unwrap_or_default(),
push,
pop,
current,
enter,
segments,
);
}
fn register_route_surface_in_layer(
layer: NavLayer,
push: impl Fn(&str) -> bool + 'static,
pop: impl Fn(bool) -> bool + 'static,
current: impl Fn() -> String + 'static,
enter: impl Fn(&str) -> bool + 'static,
segments: impl Fn() -> Vec<String> + 'static,
) {
let depth = layer.depth + NAV_HOST_CX.with(|s| s.borrow().len());
let gates: Vec<Rc<dyn Fn() -> bool>> = NAV_PAGE_ACTIVE.with(|s| s.borrow().clone());
let token = day_core::register_nav(day_core::NavController {
depth,
layer: Box::new(move || layer.order.get()),
push: Box::new(push),
pop: Box::new(pop),
current: Box::new(current),
enter: Box::new(enter),
segments: Box::new(segments),
active: Box::new(move || gates.iter().all(|g| g())),
});
Scope::current().on_cleanup(move || day_core::unregister_nav(token));
}
fn note_routed_one_of_n(kind: &str) {
let depth = NAV_HOST_CX.with(|s| s.borrow().len());
let count = ROUTED_ONE_OF_N.with(|m| {
let mut m = m.borrow_mut();
let c = m.entry(depth).or_insert(0);
*c += 1;
*c
});
if count > 1 {
warn_sibling_selectors(kind);
}
Scope::current().on_cleanup(move || {
ROUTED_ONE_OF_N.with(|m| {
if let Some(c) = m.borrow_mut().get_mut(&depth) {
*c = c.saturating_sub(1);
}
});
});
}
#[cfg(debug_assertions)]
fn warn_sibling_selectors(kind: &str) {
log::warn!(
"two routed one-of-N surfaces ({kind}) are mounted at the same navigation level. \
Their keys both flow into current_route(), so you'll see `section/childA/childB` and \
`navigate(\"child\")` is ambiguous. Mark all but the primary one `.local()` \
(docs/navigation.md)."
);
}
#[cfg(not(debug_assertions))]
fn warn_sibling_selectors(_kind: &str) {}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum NavStyle {
#[default]
Automatic,
Tabs,
Sidebar,
}
struct ResidentPage {
key: String,
scope: Scope,
node: RNode,
}
type DestFn<K> = Rc<dyn Fn(&K) -> AnyPiece>;
type ListPred<K> = Rc<dyn Fn(&K) -> bool>;
type SuggestFn = Rc<dyn Fn(&str) -> Vec<String>>;
pub const SEARCH_ITEM_ID: &str = day_spec::SEARCH_ITEM_ID;
type DerivedRows<K> = (
Vec<String>,
Vec<K>,
Vec<String>,
Vec<Option<String>>,
Vec<Option<String>>,
Vec<Option<String>>,
Vec<Option<day_spec::Color>>,
Vec<Vec<day_spec::MenuItem>>,
Vec<Option<String>>,
Vec<Option<day_spec::Color>>,
);
struct ShownRows {
keys: Vec<String>,
titles: Vec<String>,
icons: Vec<Option<String>>,
badges: Vec<Option<String>>,
badge_icons: Vec<Option<String>>,
badge_tints: Vec<Option<day_spec::Color>>,
sections: Vec<Option<String>>,
tints: Vec<Option<day_spec::Color>>,
menus: Vec<Vec<day_spec::MenuItem>>,
}
impl<K: Route> NavRows<K> {
fn shown(&self) -> ShownRows {
ShownRows {
keys: self.keys.iter().map(|k| k.key()).collect(),
titles: self.titles.clone(),
icons: self.icons.clone(),
badges: self.badges.clone(),
badge_icons: self.badge_icons.clone(),
badge_tints: self.badge_tints.clone(),
sections: self.sections.clone(),
tints: self.tints.clone(),
menus: self.menus.clone(),
}
}
}
#[derive(Clone)]
#[doc(hidden)]
pub struct SectionHeader {
id: Option<String>,
title: TextSource,
}
#[doc(hidden)]
pub struct SelItem<K> {
key: K,
title: TextSource,
icon: Option<String>,
tint: Option<day_spec::Color>,
menu: Vec<day_spec::MenuItem>,
badge: Option<TextSource>,
badge_icon: Option<String>,
badge_tint: Option<day_spec::Color>,
section: Option<SectionHeader>,
immersive: bool,
build: Option<PageFn>,
}
type PageFn = Rc<dyn Fn() -> AnyPiece>;
pub struct NavItem<K = String> {
key: K,
title: TextSource,
icon: Option<String>,
icon_tint: Option<day_spec::Color>,
menu: Vec<day_spec::MenuItem>,
badge: Option<TextSource>,
badge_icon: Option<String>,
badge_tint: Option<day_spec::Color>,
section: Option<SectionHeader>,
immersive: bool,
page: Option<PageFn>,
}
pub fn item<M, K, I: Into<K>>(key: I, title: impl IntoText<M>) -> NavItem<K> {
NavItem {
key: key.into(),
icon_tint: None,
menu: Vec::new(),
title: title.into_text(),
icon: None,
badge: None,
badge_icon: None,
badge_tint: None,
section: None,
immersive: false,
page: None,
}
}
impl<K> NavItem<K> {
pub fn page<P: Piece>(mut self, build: impl Fn() -> P + 'static) -> Self {
self.page = Some(Rc::new(move || AnyPiece::new(build())));
self
}
pub fn icon(mut self, icon: impl Into<day_spec::ImageName>) -> Self {
self.icon = Some(icon.into().as_str().to_owned());
self
}
pub fn icon_tint(mut self, color: day_spec::Color) -> Self {
self.icon_tint = Some(color);
self
}
pub fn context_menu(
mut self,
entries: impl IntoIterator<Item = crate::menus::MenuEntry>,
) -> Self {
self.menu = crate::menus::lower_menu_scoped(entries.into_iter().collect());
self
}
pub fn badge<M>(mut self, badge: impl IntoText<M>) -> Self {
self.badge = Some(badge.into_text());
self
}
pub fn badge_icon(mut self, icon: impl Into<day_spec::ImageName>) -> Self {
self.badge_icon = Some(icon.into().to_string());
self
}
pub fn badge_tint(mut self, color: day_spec::Color) -> Self {
self.badge_tint = Some(color);
self
}
pub fn section<M>(mut self, title: impl IntoText<M>) -> Self {
self.section = Some(SectionHeader {
id: None,
title: title.into_text(),
});
self
}
pub fn section_id<M>(mut self, id: impl Into<String>, title: impl IntoText<M>) -> Self {
self.section = Some(SectionHeader {
id: Some(id.into()),
title: title.into_text(),
});
self
}
pub fn immersive(mut self) -> Self {
self.immersive = true;
self
}
fn into_source(self) -> ItemSource<K> {
ItemSource::Static(SelItem {
key: self.key,
title: self.title,
icon: self.icon,
tint: self.icon_tint,
menu: self.menu,
badge: self.badge,
badge_icon: self.badge_icon,
badge_tint: self.badge_tint,
section: self.section,
immersive: self.immersive,
build: self.page,
})
}
}
pub struct NavItems<K> {
derive: Box<dyn Fn() -> Vec<SelItem<K>>>,
}
pub fn nav_items<T: Clone + 'static, K: 'static>(
items: impl Fn() -> Vec<T> + 'static,
map: impl Fn(&T) -> NavItem<K> + 'static,
) -> NavItems<K> {
NavItems {
derive: Box::new(move || {
items()
.iter()
.map(|t| match map(t).into_source() {
ItemSource::Static(it) => it,
ItemSource::Dynamic(_) => unreachable!("an item maps to one row"),
})
.collect()
}),
}
}
pub trait NavContent<K> {
fn push_sources(self, out: &mut Vec<ItemSource<K>>);
}
impl<K> NavContent<K> for NavItem<K> {
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
out.push(self.into_source());
}
}
impl<K: 'static> NavContent<K> for NavItems<K> {
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
out.push(ItemSource::Dynamic(self.derive));
}
}
impl<K, C: NavContent<K>> NavContent<K> for Option<C> {
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
if let Some(c) = self {
c.push_sources(out);
}
}
}
impl<K, C: NavContent<K>> NavContent<K> for Vec<C> {
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
for c in self {
c.push_sources(out);
}
}
}
impl<K, C: NavContent<K>, const N: usize> NavContent<K> for [C; N] {
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
for c in self {
c.push_sources(out);
}
}
}
impl<K> NavContent<K> for () {
fn push_sources(self, _out: &mut Vec<ItemSource<K>>) {}
}
macro_rules! impl_nav_content_tuple {
($($name:ident),+) => {
impl<K, $($name: NavContent<K>),+> NavContent<K> for ($($name,)+) {
#[allow(non_snake_case)]
fn push_sources(self, out: &mut Vec<ItemSource<K>>) {
let ($($name,)+) = self;
$($name.push_sources(out);)+
}
}
};
}
impl_nav_content_tuple!(A);
impl_nav_content_tuple!(A, B);
impl_nav_content_tuple!(A, B, C);
impl_nav_content_tuple!(A, B, C, D);
impl_nav_content_tuple!(A, B, C, D, E);
impl_nav_content_tuple!(A, B, C, D, E, F);
impl_nav_content_tuple!(A, B, C, D, E, F, G);
impl_nav_content_tuple!(A, B, C, D, E, F, G, H);
impl_nav_content_tuple!(A, B, C, D, E, F, G, H, I);
impl_nav_content_tuple!(A, B, C, D, E, F, G, H, I, J);
impl_nav_content_tuple!(A, B, C, D, E, F, G, H, I, J, L);
impl_nav_content_tuple!(A, B, C, D, E, F, G, H, I, J, L, N);
#[allow(clippy::large_enum_variant)]
#[doc(hidden)]
pub enum ItemSource<K> {
Static(SelItem<K>),
Dynamic(Box<dyn Fn() -> Vec<SelItem<K>>>),
}
#[allow(clippy::large_enum_variant)]
enum MetaSource<K> {
Static(K, TextSource, RowMeta, bool),
Dynamic(Box<dyn Fn() -> Vec<SelItem<K>>>),
}
#[derive(Clone, Default)]
struct RowMeta {
icon: Option<String>,
tint: Option<day_spec::Color>,
menu: Vec<day_spec::MenuItem>,
badge: Option<TextSource>,
badge_icon: Option<String>,
badge_tint: Option<day_spec::Color>,
section: Option<SectionHeader>,
}
#[derive(Clone)]
struct NavReorder<K> {
eligible: Rc<dyn Fn(&K) -> bool>,
moved: Rc<dyn Fn(K, K)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavDrag {
Fixed,
Reorder,
Regroup,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NavDrop<K> {
pub moved: K,
pub section: Option<String>,
pub before: Option<K>,
}
#[derive(Clone)]
struct NavRegroup<K> {
drag: Rc<dyn Fn(&K) -> NavDrag>,
dropped: Rc<dyn Fn(NavDrop<K>)>,
}
#[derive(Clone)]
enum SidebarDrag<K> {
Reorder(NavReorder<K>),
Regroup(NavRegroup<K>),
}
impl<K> SidebarDrag<K> {
fn drag(&self, key: &K) -> NavDrag {
match self {
SidebarDrag::Reorder(r) if (r.eligible)(key) => NavDrag::Reorder,
SidebarDrag::Reorder(_) => NavDrag::Fixed,
SidebarDrag::Regroup(g) => (g.drag)(key),
}
}
}
struct RegroupRows<K> {
keys: Vec<K>,
opens: Vec<bool>,
ids: Vec<Option<String>>,
}
impl<K: Route> RegroupRows<K> {
fn from_nav(rows: NavRows<K>) -> Self {
RegroupRows {
opens: rows.sections.iter().map(Option::is_some).collect(),
ids: rows.section_ids,
keys: rows.keys,
}
}
fn from_sidebar(rows: &[SidebarRow<K>]) -> Self {
let mut r = RegroupRows {
keys: Vec::new(),
opens: Vec::new(),
ids: Vec::new(),
};
let mut header = None;
for row in rows {
match &row.key {
None => header = Some(row.section_id.clone()),
Some(key) => {
r.keys.push(key.clone());
r.opens.push(header.is_some());
r.ids.push(header.take().flatten());
}
}
}
r
}
fn display(&self) -> Vec<Option<usize>> {
let mut rows = Vec::with_capacity(self.keys.len() * 2);
for (i, opens) in self.opens.iter().enumerate() {
if *opens {
rows.push(None);
}
rows.push(Some(i));
}
rows
}
fn section_of(&self, i: usize) -> usize {
(1..=i).rev().find(|s| self.opens[*s]).unwrap_or(0)
}
fn drop_at(
&self,
regroup: &NavRegroup<K>,
from: usize,
section: usize,
before: Option<usize>,
) -> Result<Option<NavDrop<K>>, &'static str> {
let len = self.keys.len();
let drag = |i: usize| (regroup.drag)(&self.keys[i]);
if from >= len || drag(from) == NavDrag::Fixed {
return Err("regroup denied: the row is fixed or out of range");
}
if section >= len || (section > 0 && !self.opens[section]) {
return Err("regroup denied: no section starts at that row");
}
let end = (section + 1..len).find(|i| self.opens[*i]).unwrap_or(len);
if before.is_some_and(|b| b < section || b >= end || b == from) {
return Err("regroup denied: the row it lands before is not in that section");
}
if (section..end).any(|i| drag(i) == NavDrag::Fixed) {
return Err("regroup denied: a fixed row holds that section");
}
if self.section_of(from) == section {
if drag(from) == NavDrag::Regroup {
return Err("regroup denied: the row keeps its section's sorted order");
}
let next = (from + 1 < end).then_some(from + 1);
if before == next {
return Ok(None);
}
}
Ok(Some(NavDrop {
moved: self.keys[from].clone(),
section: if self.opens[section] {
self.ids[section].clone()
} else {
None
},
before: before.map(|b| self.keys[b].clone()),
}))
}
fn display_drop(
&self,
regroup: &NavRegroup<K>,
from: usize,
to: usize,
) -> Result<Option<NavDrop<K>>, &'static str> {
let rows = self.display();
let Some(Some(moved)) = rows.get(from).copied() else {
return Err("regroup denied: the row is a section header or out of range");
};
if to >= rows.len() {
return Err("regroup denied: the target is out of range");
}
let mut order: Vec<usize> = (0..rows.len()).filter(|r| *r != from).collect();
order.insert(to, from);
let section = match order[..to].iter().rev().find(|r| rows[**r].is_none()) {
Some(header) => rows.get(header + 1).copied().flatten(),
None => rows[0],
}
.ok_or("regroup denied: no section above the first header")?;
let before = order.get(to + 1).and_then(|r| rows[*r]);
self.drop_at(regroup, moved, section, before)
}
}
#[derive(Clone, PartialEq)]
struct SidebarRow<K> {
id: String,
parent: Option<String>,
key: Option<K>,
section_id: Option<String>,
title: String,
icon: Option<String>,
tint: Option<day_spec::Color>,
badge: Option<String>,
badge_icon: Option<String>,
badge_tint: Option<day_spec::Color>,
menu: Vec<day_spec::MenuItem>,
}
fn sidebar_rows<K: Route>(rows: NavRows<K>) -> Vec<SidebarRow<K>> {
let mut result = Vec::new();
let mut parent = None;
for (i, key) in rows.keys.into_iter().enumerate() {
if let Some(title) = &rows.sections[i] {
let id = rows.section_ids[i].clone().unwrap_or_else(|| key.key());
let header = format!("header:{id}");
parent = Some(header.clone());
result.push(SidebarRow {
id: header,
parent: None,
key: None,
section_id: rows.section_ids[i].clone(),
title: title.clone(),
icon: None,
tint: None,
badge: None,
badge_icon: None,
badge_tint: None,
menu: vec![],
});
}
result.push(SidebarRow {
id: format!("item:{}", key.key()),
parent: parent.clone(),
key: Some(key),
section_id: None,
title: rows.titles[i].clone(),
icon: rows.icons[i].clone(),
tint: rows.tints[i],
badge: rows.badges[i].clone(),
badge_icon: rows.badge_icons[i].clone(),
badge_tint: rows.badge_tints[i],
menu: rows.menus[i].clone(),
});
}
result
}
fn sidebar_image<K: Route>(slot: ItemSlot<SidebarRow<K>, String>, trailing: bool) -> AnyPiece {
when(
move || {
slot.field(|r| {
let name = if trailing { &r.badge_icon } else { &r.icon };
name.as_deref()
.is_some_and(|name| std::path::Path::new(name).is_absolute())
})
},
move || sidebar_image_view(slot, trailing, false),
)
.otherwise(move || sidebar_image_view(slot, trailing, true))
.any()
}
fn sidebar_image_view<K: Route>(
slot: ItemSlot<SidebarRow<K>, String>,
trailing: bool,
template: bool,
) -> AnyPiece {
piece_fn(move |cx| {
let tint_of = move || slot.field(|r| if trailing { r.badge_tint } else { r.tint });
let seed = day_reactive::untrack(tint_of);
let image = image(move || {
day_spec::ImageSource::Named(slot.field(|r| {
if trailing {
r.badge_icon.clone()
} else {
r.icon.clone()
}
.unwrap_or_default()
}))
})
.initial_tint(seed);
let node = if template { image.template() } else { image }.build(cx);
bind(tint_of, move |tint| {
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::ImagePatch::Tint(*tint)),
false,
)
});
});
node
})
.frame(20.0, 20.0)
.any()
}
fn sidebar_progress<K: Route>(
slot: ItemSlot<SidebarRow<K>, String>,
progress: Option<IconProgressSource<K>>,
) -> AnyPiece {
let activity = Rc::new(move || {
let key = slot.field(|row| row.key.clone())?;
progress.as_ref()?()
.into_iter()
.find(|(k, _)| k.key() == key.key())
.map(|(_, fraction)| fraction)
});
let (visible, determinate, drawn) = (activity.clone(), activity.clone(), activity);
when(
move || visible().is_some(),
move || {
let (determinate, drawn) = (determinate.clone(), drawn.clone());
when(
move || determinate().flatten().is_some(),
move || {
let drawn = drawn.clone();
canvas(move |draw, size| {
let fraction = drawn().flatten().unwrap_or(0.0).clamp(0.0, 1.0);
let color = slot
.field(|r| r.tint)
.unwrap_or(day_spec::Color::hex(0x0A84FF));
let rect = day_spec::Rect::new(
1.0,
1.0,
(size.width - 2.0).max(0.0),
(size.height - 2.0).max(0.0),
);
draw.stroke(
day_spec::Shape::Arc {
rect,
start_deg: -90.0,
sweep_deg: 360.0 * fraction,
},
color,
2.0,
);
})
.frame(20.0, 20.0)
},
)
.otherwise(|| spinner().frame(20.0, 20.0))
},
)
.any()
}
fn section_sidebar<K: Route, S: Binding<K>>(
rows: Rc<SelItems<K>>,
selection: S,
collapsed: AnyBinding<std::collections::HashSet<String>>,
changed: Option<Rc<dyn Fn(String, bool)>>,
progress: Option<IconProgressSource<K>>,
) -> AnyPiece {
use crate::structure::{branches, tree};
let snapshot = Signal::new(day_reactive::untrack(|| sidebar_rows(rows.derive())));
bind(
move || sidebar_rows(rows.derive()),
move |v| snapshot.set_if_changed(v.clone()),
);
let open = Signal::new(std::collections::HashSet::new());
bind(
move || {
let closed = collapsed.get();
snapshot
.get()
.iter()
.filter(|r| r.key.is_none())
.filter(|r| !closed.contains(r.id.strip_prefix("header:").unwrap_or(&r.id)))
.map(|r| r.id.clone())
.collect::<std::collections::HashSet<_>>()
},
move |v| open.set_if_changed(v.clone()),
);
let selected = selection.clone();
tree(
branches(
move || snapshot.get(),
|r: &SidebarRow<K>| r.id.clone(),
|r| r.parent.clone(),
),
move |slot| {
let progress = progress.clone();
piece_fn(move |cx| {
let node = when(
move || slot.field(|r| r.key.is_none()),
move || {
row((label(move || slot.field(|r| r.title.clone()))
.font(day_spec::Font::Caption)
.single_line()
.grow_w(),))
.align(VAlign::Center)
.grow()
.any()
},
)
.otherwise(move || {
let progress = progress.clone();
row((
when(
move || slot.field(|r| r.icon.is_some()),
move || {
sidebar_image(slot, false)
.overlay(sidebar_progress(slot, progress.clone()))
},
),
label(move || slot.field(|r| r.title.clone()))
.single_line()
.grow_w(),
label(move || slot.field(|r| r.badge.clone().unwrap_or_default()))
.font(day_spec::Font::Caption),
when(
move || slot.field(|r| r.badge_icon.is_some()),
move || sidebar_image(slot, true),
),
))
.spacing(8.0)
.align(VAlign::Center)
.grow()
.any()
})
.grow()
.build(cx);
with_tree(|t| {
t.set_context_menu_fn(node, Rc::new(move |_| slot.field(|r| r.menu.clone())))
});
node
})
},
)
.section_headers()
.row_height(day_spec::props::RowHeight::Uniform(32.0))
.expanded(open)
.on_expansion(move |id, expanded| {
let Some(id) = id.strip_prefix("header:") else {
return;
};
collapsed.update(|s| {
if expanded {
s.remove(id);
} else {
s.insert(id.to_owned());
}
});
if let Some(f) = &changed {
f(id.to_owned(), expanded);
}
})
.selected(move || vec![format!("item:{}", selected.read().key())])
.on_selection(move |ids| {
let Some(row) = snapshot
.get()
.into_iter()
.find(|r| ids.contains(&r.id) && r.key.is_some())
else {
return;
};
if let Some(key) = row.key {
day_core::note_navigation(&key.key(), Some(&row.title));
selection.write(key);
}
})
.type_ahead(move |id| {
snapshot
.get()
.iter()
.find(|r| &r.id == id)
.map(|r| r.title.clone())
.unwrap_or_default()
})
.row_id(|id| day_core::id!(sidebar, id))
.id(day_core::id!(nav_sections))
.grow()
.any()
}
fn script_nav_reorder<K: Route>(
keys: &[K],
sections: &[Option<String>],
reorder: &NavReorder<K>,
from: usize,
to: usize,
) -> Result<usize, &'static str> {
let mut rows: Vec<Option<usize>> = Vec::with_capacity(keys.len() * 2);
for i in 0..keys.len() {
if sections.get(i).is_some_and(Option::is_some) {
rows.push(None);
}
rows.push(Some(i));
}
let movable = |r: usize| {
rows.get(r)
.copied()
.flatten()
.is_some_and(|i| (reorder.eligible)(&keys[i]))
};
if !movable(from) || to >= rows.len() {
return Err("reorder denied: the row is fixed or the target is out of range");
}
let (lo, hi) = (from.min(to), from.max(to));
if !(lo..=hi).all(movable) {
return Err("reorder denied: a fixed row or a section header is in the way");
}
if let (Some(Some(a)), Some(Some(b))) = (rows.get(from), rows.get(to))
&& a != b
{
(reorder.moved)(keys[*a].clone(), keys[*b].clone());
}
Ok(to)
}
fn reorder_sidebar<K: Route, S: Binding<K>>(
rows: Rc<SelItems<K>>,
selection: S,
drag: SidebarDrag<K>,
progress: Option<IconProgressSource<K>>,
) -> AnyPiece {
let snapshot = Signal::new(day_reactive::untrack(|| sidebar_rows(rows.derive())));
bind(
move || sidebar_rows(rows.derive()),
move |value| snapshot.set_if_changed(value.clone()),
);
let source = Rc::new(move || snapshot.get());
let selection_revision = Signal::new(0u64);
let (read, guard, commit, selected) = (
source.clone(),
source.clone(),
source.clone(),
source.clone(),
);
let selected_binding = selection.clone();
let tapped_selection = selection.clone();
list(
items(move || read(), |r: &SidebarRow<K>| r.id.clone()),
move |slot| {
let activity = progress.clone();
let tapped_selection = tapped_selection.clone();
piece_fn(move |cx| {
let node = row((when(
move || slot.field(|r| r.key.is_none()),
move || {
label(move || slot.field(|r| r.title.clone()))
.font(day_spec::Font::Caption)
.padding(day_spec::Insets::symmetric(8.0, 0.0))
.grow_w()
},
)
.otherwise(move || {
let activity = activity.clone();
row((
when(
move || slot.field(|r| r.icon.is_some()),
move || {
sidebar_image(slot, false)
.overlay(sidebar_progress(slot, activity.clone()))
},
),
label(move || slot.field(|r| r.title.clone()))
.single_line()
.grow_w(),
label(move || slot.field(|r| r.badge.clone().unwrap_or_default()))
.font(day_spec::Font::Caption),
when(
move || slot.field(|r| r.badge_icon.is_some()),
move || sidebar_image(slot, true),
),
))
.spacing(8.0)
.align(VAlign::Center)
.padding(day_spec::Insets::symmetric(8.0, 0.0))
.grow_w()
}),))
.align(VAlign::Center)
.grow()
.id_of(move || day_core::id!(sidebar, slot.field(|r| r.id.clone())))
.on_tap(move || {
if let Some(key) = slot.field(|r| r.key.clone()) {
let title = slot.field(|r| r.title.clone());
day_core::note_navigation(&key.key(), Some(&title));
tapped_selection.write(key);
}
})
.build(cx);
with_tree(|t| {
t.set_context_menu_fn(node, Rc::new(move |_| slot.field(|r| r.menu.clone())))
});
node
})
},
)
.row_height(day_spec::props::RowHeight::Uniform(36.0))
.on_select(move |id| {
selection_revision.update(|n| *n = n.wrapping_add(1));
if let Some(row) = source().into_iter().find(|r| r.id == id)
&& let Some(key) = row.key
{
day_core::note_navigation(&key.key(), Some(&row.title));
selection.write(key);
}
})
.selected_rows(move || {
selection_revision.get();
selected()
.iter()
.position(|r| {
r.key
.as_ref()
.is_some_and(|k| k.key() == selected_binding.read().key())
})
.into_iter()
.collect()
})
.reorderable(match drag {
SidebarDrag::Reorder(reorder) => crate::Reorder::new(move |from, to| {
let rows = commit();
if let (Some(a), Some(b)) = (
rows.get(from).and_then(|r| r.key.clone()),
rows.get(to).and_then(|r| r.key.clone()),
) {
(reorder.moved)(a, b);
}
})
.guard(move |from, to| {
let rows = guard();
if from >= rows.len()
|| to >= rows.len()
|| rows[from.min(to)..=from.max(to)]
.iter()
.any(|r| r.key.as_ref().is_none_or(|k| !(reorder.eligible)(k)))
{
ReorderVerdict::Deny
} else {
ReorderVerdict::Allow
}
}),
SidebarDrag::Regroup(regroup) => {
let check = regroup.clone();
crate::Reorder::new(move |from, to| {
let rows = RegroupRows::from_sidebar(&commit());
if let Ok(Some(drop)) = rows.display_drop(®roup, from, to) {
(regroup.dropped)(drop);
}
})
.guard(move |from, to| {
match RegroupRows::from_sidebar(&guard()).display_drop(&check, from, to) {
Ok(_) => ReorderVerdict::Allow,
Err(_) => ReorderVerdict::Deny,
}
})
}
})
.id(day_core::id!(nav_reorder_list))
.grow()
.any()
}
struct NavRows<K> {
keys: Vec<K>,
titles: Vec<String>,
icons: Vec<Option<String>>,
badges: Vec<Option<String>>,
badge_icons: Vec<Option<String>>,
badge_tints: Vec<Option<day_spec::Color>>,
sections: Vec<Option<String>>,
section_ids: Vec<Option<String>>,
tints: Vec<Option<day_spec::Color>>,
menus: Vec<Vec<day_spec::MenuItem>>,
}
struct SelItems<K> {
static_builders: std::collections::HashMap<String, PageFn>,
dynamic_builders: RefCell<std::collections::HashMap<String, PageFn>>,
meta: Rc<Vec<MetaSource<K>>>,
destination: Option<DestFn<K>>,
}
impl<K: Route> SelItems<K> {
fn from_sources(sources: Vec<ItemSource<K>>, destination: Option<DestFn<K>>) -> Self {
let mut static_builders = std::collections::HashMap::new();
let mut meta = Vec::new();
for src in sources {
match src {
ItemSource::Static(it) => {
if let Some(b) = it.build {
static_builders.insert(it.key.key(), b);
}
meta.push(MetaSource::Static(
it.key,
it.title,
RowMeta {
icon: it.icon,
tint: it.tint,
menu: it.menu,
badge: it.badge,
badge_icon: it.badge_icon,
badge_tint: it.badge_tint,
section: it.section,
},
it.immersive,
));
}
ItemSource::Dynamic(f) => {
meta.push(MetaSource::Dynamic(f));
}
}
}
SelItems {
static_builders,
dynamic_builders: RefCell::new(std::collections::HashMap::new()),
meta: Rc::new(meta),
destination,
}
}
fn derive(&self) -> NavRows<K> {
let mut r = NavRows {
keys: Vec::new(),
titles: Vec::new(),
icons: Vec::new(),
badges: Vec::new(),
badge_icons: Vec::new(),
badge_tints: Vec::new(),
sections: Vec::new(),
section_ids: Vec::new(),
tints: Vec::new(),
menus: Vec::new(),
};
let mut push = |k: K, title: String, m: &RowMeta| {
r.keys.push(k);
r.titles.push(title);
r.icons.push(m.icon.clone());
r.badges.push(
m.badge
.as_ref()
.map(|b| b.resolve())
.filter(|b| !b.is_empty()),
);
r.badge_icons.push(m.badge_icon.clone());
r.badge_tints.push(m.badge_tint);
r.sections
.push(m.section.as_ref().map(|s| s.title.resolve()));
r.section_ids
.push(m.section.as_ref().and_then(|s| s.id.clone()));
r.tints.push(m.tint);
r.menus.push(m.menu.clone());
};
for ms in self.meta.iter() {
match ms {
MetaSource::Static(k, t, m, _) => push(k.clone(), t.resolve(), m),
MetaSource::Dynamic(f) => {
for it in f() {
if let Some(b) = it.build {
self.dynamic_builders.borrow_mut().insert(it.key.key(), b);
}
let m = RowMeta {
icon: it.icon,
tint: it.tint,
menu: it.menu,
badge: it.badge,
badge_icon: it.badge_icon,
badge_tint: it.badge_tint,
section: it.section,
};
let title = it.title.resolve();
push(it.key, title, &m);
}
}
}
}
r
}
fn immersive_of(&self, key: &str) -> bool {
self.meta.iter().any(|ms| match ms {
MetaSource::Static(k, _, _, imm) => *imm && k.key() == key,
MetaSource::Dynamic(f) => {
day_reactive::untrack(|| f().iter().any(|it| it.immersive && it.key.key() == key))
}
})
}
fn static_title(&self, key: &str) -> Option<TextSource> {
self.meta.iter().find_map(|ms| match ms {
MetaSource::Static(k, t, _, _) if k.key() == key => Some(t.clone()),
_ => None,
})
}
fn build_page(&self, key: &K) -> AnyPiece {
let k = key.key();
let dynamic = self.dynamic_builders.borrow().get(&k).cloned();
if let Some(b) = self.static_builders.get(&k) {
b()
} else if let Some(b) = dynamic {
b()
} else if let Some(d) = &self.destination {
d(key)
} else {
piece_fn(|cx| cx.layout_only(Rc::new(PassThrough), Flex::default(), Boundary::No)).any()
}
}
}
type IconProgressSource<K> = Rc<dyn Fn() -> Vec<(K, Option<f64>)>>;
type DocumentEvent = Rc<dyn Fn(&Event)>;
type RefreshSource = (Reactive<bool>, Rc<dyn Fn()>);
pub struct Nav<S: Binding<K>, K: Route = String> {
selection: S,
documents: Option<day_spec::props::DocumentTabsConfig>,
document_event: Option<DocumentEvent>,
icon_progress: Option<IconProgressSource<K>>,
refresh: Option<RefreshSource>,
style: NavStyle,
title: TextSource,
header: Option<Box<dyn FnOnce() -> AnyPiece>>,
footer: Option<Box<dyn FnOnce() -> AnyPiece>>,
sources: Vec<ItemSource<K>>,
destination: Option<DestFn<K>>,
routed: bool,
restore: Option<String>,
retain_selection: Option<ListPred<K>>,
reorder_items: Option<SidebarDrag<K>>,
collapsed_sections: Option<AnyBinding<std::collections::HashSet<String>>>,
on_section_expansion: Option<Rc<dyn Fn(String, bool)>>,
toolbar: Vec<crate::ToolbarSource>,
sidebar_toggle: bool,
search: Option<SearchSpec>,
presentation: Option<day_spec::props::NavPresentation>,
content_list: Option<Rc<dyn Fn() -> AnyPiece>>,
content_list_width: f64,
content_list_pred: Option<ListPred<K>>,
detail_visible: Option<AnyBinding<bool>>,
detail_title: Option<TextSource>,
}
pub struct Search {
query: AnyBinding<String>,
prompt: Option<TextSource>,
placement: day_spec::props::SearchPlacement,
reveal: SearchReveal,
scopes: Vec<TextSource>,
scope: Option<AnyBinding<usize>>,
suggestions: Option<SuggestFn>,
submit: Option<SubmitFn>,
available: Option<crate::Reactive<bool>>,
}
type SubmitFn = Rc<dyn Fn(&str)>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SearchReveal {
#[default]
Always,
Scrolls,
OnPull,
}
impl Search {
pub fn new(query: impl Binding<String>) -> Search {
Search {
query: AnyBinding::new(query),
prompt: None,
placement: day_spec::props::SearchPlacement::Automatic,
reveal: SearchReveal::Always,
scopes: Vec::new(),
scope: None,
suggestions: None,
submit: None,
available: None,
}
}
pub fn prompt<M>(mut self, prompt: impl IntoText<M>) -> Search {
self.prompt = Some(prompt.into_text());
self
}
pub fn placement(mut self, placement: day_spec::props::SearchPlacement) -> Search {
self.placement = placement;
self
}
pub fn reveal(mut self, reveal: SearchReveal) -> Search {
self.reveal = reveal;
self
}
pub fn scopes<M>(
mut self,
scope: impl Binding<usize>,
titles: impl IntoIterator<Item = impl IntoText<M>>,
) -> Search {
self.scopes = titles.into_iter().map(IntoText::into_text).collect();
self.scope = Some(AnyBinding::new(scope));
self
}
pub fn suggestions(mut self, f: impl Fn(&str) -> Vec<String> + 'static) -> Search {
self.suggestions = Some(Rc::new(f));
self
}
pub fn on_submit(mut self, f: impl Fn(&str) + 'static) -> Search {
self.submit = Some(Rc::new(f));
self
}
pub fn available<M>(mut self, v: impl crate::IntoReactive<bool, M>) -> Search {
self.available = Some(v.into_reactive());
self
}
}
pub(crate) fn install_page_search(search: Search) {
let chrome = day_core::current_chrome();
let active = day_core::current_page_gate();
let column = day_core::current_page_column();
let query = search.query;
let text = query.get_untracked();
let prompt_source = search.prompt.clone();
let prompt: Rc<RefCell<String>> = Rc::new(RefCell::new(
prompt_source
.as_ref()
.map(TextSource::initial)
.unwrap_or_default(),
));
let suggestions = search
.suggestions
.as_ref()
.map(|f| day_reactive::untrack(|| f(&text)))
.unwrap_or_default();
let shown: Rc<RefCell<String>> = Rc::new(RefCell::new(text.clone()));
let action = {
let shown = shown.clone();
let submit = search.submit.clone();
day_core::register_toolbar_value(Rc::new(move |v: &day_spec::ToolbarValue| match v {
day_spec::ToolbarValue::Text(t) => {
*shown.borrow_mut() = t.clone();
query.set(t.clone());
}
day_spec::ToolbarValue::Submit(t) => {
if let Some(f) = &submit {
f(t);
}
}
_ => {}
}))
};
let item_prompt = prompt.clone();
let item = move |text: String, suggestions: Vec<String>| day_spec::ToolbarItem {
id: day_core::id!(day::search).as_str().to_owned(),
kind: day_spec::ToolbarItemKind::Search {
text,
placeholder: item_prompt.borrow().clone(),
suggestions,
},
label: item_prompt.borrow().clone(),
tooltip: None,
icon: None,
enabled: true,
action,
placement: day_spec::ToolbarPlacement::Secondary,
label_style: day_spec::LabelStyle::Automatic,
prominent: false,
column,
};
let available = search.available.clone();
let offered = available.as_ref().is_none_or(|v| v.get_untracked());
let offered_now = Rc::new(Cell::new(offered));
let item = Rc::new(item);
let token = day_core::register_contribution_gated(
chrome,
if offered {
vec![item(text.clone(), suggestions.clone())]
} else {
Vec::new()
},
active.clone(),
);
if let Some(crate::Reactive::Dyn(f)) = available {
let (shown, suggest, query) = (shown.clone(), search.suggestions.clone(), query);
let (item, offered_now) = (item.clone(), offered_now.clone());
bind_seeded(
offered,
move || f(),
move |on: &bool| {
offered_now.set(*on);
let items = if *on {
let text = shown.borrow().clone();
let list = suggest
.as_ref()
.map(|f| day_reactive::untrack(|| f(&query.get_untracked())))
.unwrap_or_default();
vec![item(text, list)]
} else {
Vec::new()
};
day_core::update_contribution(token, items);
},
);
}
if let Some(TextSource::Dyn(read)) = prompt_source {
let (shown, suggest, item) = (shown.clone(), search.suggestions.clone(), item.clone());
let seed = prompt.borrow().clone();
bind_seeded(
seed,
move || read(),
move |p: &String| {
*prompt.borrow_mut() = p.clone();
if offered_now.get() {
let text = shown.borrow().clone();
let list = suggest
.as_ref()
.map(|f| day_reactive::untrack(|| f(&query.get_untracked())))
.unwrap_or_default();
day_core::update_contribution(token, vec![item(text, list)]);
}
},
);
}
if let Some(gate) = active {
day_reactive::Effect::new(move || {
let _ = gate();
day_core::chrome_changed();
});
}
Scope::current().on_cleanup(move || day_core::unregister_contribution(token));
bind_seeded(
text,
move || query.get(),
move |t: &String| {
if *shown.borrow() == *t {
return;
}
*shown.borrow_mut() = t.clone();
day_core::patch_chrome(
chrome,
day_spec::ToolbarPatch::Text {
item: SEARCH_ITEM_ID.to_string(),
text: t.clone(),
},
);
},
);
if let Some(f) = search.suggestions {
bind_seeded(
suggestions,
move || f(&query.get()),
move |list| {
day_core::patch_chrome(
chrome,
day_spec::ToolbarPatch::Suggestions {
item: SEARCH_ITEM_ID.to_string(),
list: list.clone(),
},
)
},
);
}
}
pub struct ContentList<K = String> {
build: Rc<dyn Fn() -> AnyPiece>,
width: f64,
pred: Option<ListPred<K>>,
detail_visible: Option<AnyBinding<bool>>,
detail_title: Option<TextSource>,
}
impl<K> ContentList<K> {
pub fn new<P: Piece>(build: impl Fn() -> P + 'static) -> ContentList<K> {
ContentList {
build: Rc::new(move || AnyPiece::new(build())),
width: day_spec::NAV_LIST_WIDTH,
pred: None,
detail_visible: None,
detail_title: None,
}
}
pub fn width(mut self, width: f64) -> ContentList<K> {
self.width = width;
self
}
pub fn only_for(mut self, pred: impl Fn(&K) -> bool + 'static) -> ContentList<K> {
self.pred = Some(Rc::new(pred));
self
}
pub fn detail_visible(mut self, visible: impl Binding<bool>) -> ContentList<K> {
self.detail_visible = Some(AnyBinding::new(visible));
self
}
pub fn detail_for<T: Clone + 'static>(
self,
selected: impl Binding<Option<T>>,
) -> ContentList<K> {
self.detail_visible(Presence(AnyBinding::new(selected)))
}
pub fn detail_title<M>(mut self, t: impl IntoText<M>) -> ContentList<K> {
self.detail_title = Some(t.into_text());
self
}
}
#[derive(Clone)]
struct Presence<T: 'static>(AnyBinding<Option<T>>);
impl<T: Clone + 'static> Binding<bool> for Presence<T> {
fn read(&self) -> bool {
self.0.get().is_some()
}
fn peek(&self) -> bool {
self.0.get_untracked().is_some()
}
fn write(&self, open: bool) {
if !open && self.peek() {
self.0.set(None);
}
}
fn write_teardown(&self, _open: bool) {}
}
pub struct Sections {
collapsed: AnyBinding<std::collections::HashSet<String>>,
on_change: Option<Rc<dyn Fn(String, bool)>>,
}
impl Sections {
pub fn collapsible(collapsed: impl Binding<std::collections::HashSet<String>>) -> Sections {
Sections {
collapsed: AnyBinding::new(collapsed),
on_change: None,
}
}
pub fn restored(key: impl Into<String>) -> Sections {
let key = key.into();
let saved = day_core::nav_store_load(&key).unwrap_or_default();
let mut rest = saved.as_str();
let mut ids = std::collections::HashSet::new();
while let Some((len, tail)) = rest.split_once(':') {
let Some(len) = len.parse::<usize>().ok() else {
break;
};
let Some(id) = tail.get(..len) else {
break;
};
ids.insert(id.to_owned());
rest = &tail[len..];
}
let state = Signal::new(ids);
bind(
move || state.get(),
move |ids| {
let mut ids: Vec<_> = ids.iter().collect();
ids.sort();
let saved = ids
.iter()
.map(|id| format!("{}:{id}", id.len()))
.collect::<String>();
day_core::nav_store_save(&key, &saved);
},
);
Sections::collapsible(state)
}
pub fn on_change(mut self, f: impl Fn(String, bool) + 'static) -> Sections {
self.on_change = Some(Rc::new(f));
self
}
}
struct SearchSpec {
query: AnyBinding<String>,
prompt: Option<TextSource>,
placement: day_spec::props::SearchPlacement,
hides_on_scroll: bool,
initially_hidden: bool,
scopes: Vec<TextSource>,
scope: Option<AnyBinding<usize>>,
suggestions: Option<SuggestFn>,
submit: Option<SubmitFn>,
}
impl SearchSpec {
fn resolve(requested: day_spec::props::SearchPlacement) -> day_spec::props::SearchPlacement {
use day_spec::props::SearchPlacement as P;
use day_spec::{Cap, Support};
let can = |cap| with_tree(|t| t.capability(cap)) != Support::Unsupported;
match requested {
P::Inline => P::Inline,
P::Toolbar if can(Cap::ToolbarSearch) => P::Toolbar,
P::Automatic if can(Cap::Toolbar) && can(Cap::ToolbarSearch) => P::Toolbar,
P::Toolbar | P::Automatic => P::Inline,
}
}
fn lower(&self) -> day_spec::props::SearchProps {
let text = self.query.get_untracked();
day_spec::props::SearchProps {
suggestions: self
.suggestions
.as_ref()
.map(|f| day_reactive::untrack(|| f(&text)))
.unwrap_or_default(),
text,
prompt: self
.prompt
.as_ref()
.map(TextSource::initial)
.unwrap_or_default(),
placement: Self::resolve(self.placement),
hides_on_scroll: self.hides_on_scroll,
initially_hidden: self.initially_hidden,
scopes: self.scopes.iter().map(TextSource::initial).collect(),
scope: self.scope.map(|s| s.get_untracked()).unwrap_or(0),
}
}
fn install(&self, host: RNode, chrome: day_core::Chrome, seed: &day_spec::props::SearchProps) {
use day_spec::props::{SearchPatch, SearchPlacement as P};
let placement = Self::resolve(self.placement);
let query = self.query;
let shown: Rc<RefCell<String>> = Rc::new(RefCell::new(seed.text.clone()));
if placement == P::Toolbar {
let g = shown.clone();
let submit = self.submit.clone();
let action = day_core::register_toolbar_value(Rc::new(
move |v: &day_spec::ToolbarValue| match v {
day_spec::ToolbarValue::Text(t) => {
*g.borrow_mut() = t.clone();
query.set(t.clone());
}
day_spec::ToolbarValue::Submit(t) => {
if let Some(f) = &submit {
f(t);
}
}
_ => {}
},
));
let suggestions = seed.suggestions.clone();
let item = move |text: String, prompt: String| day_spec::ToolbarItem {
id: day_core::id!(day::search).as_str().to_owned(),
kind: day_spec::ToolbarItemKind::Search {
text,
placeholder: prompt.clone(),
suggestions: suggestions.clone(),
},
label: prompt,
tooltip: None,
icon: None,
enabled: true,
action,
placement: day_spec::ToolbarPlacement::Secondary,
label_style: day_spec::LabelStyle::Automatic,
prominent: false,
column: day_spec::ToolbarColumn::Detail,
};
let token = day_core::register_contribution(
chrome,
vec![item(seed.text.clone(), seed.prompt.clone())],
);
if let Some(TextSource::Dyn(read)) = self.prompt.clone() {
let shown = shown.clone();
bind_seeded(
seed.prompt.clone(),
move || read(),
move |p: &String| {
let text = shown.borrow().clone();
day_core::update_contribution(token, vec![item(text, p.clone())]);
},
);
}
Scope::current().on_cleanup(move || day_core::unregister_contribution(token));
} else {
day_core::register_search_host(host);
Scope::current().on_cleanup(move || day_core::unregister_search_host(host));
}
bind_seeded(
seed.text.clone(),
move || query.get(),
move |t: &String| {
if *shown.borrow() == *t {
return; }
*shown.borrow_mut() = t.clone();
match placement {
P::Toolbar => day_core::patch_chrome(
chrome,
day_spec::ToolbarPatch::Text {
item: SEARCH_ITEM_ID.to_string(),
text: t.clone(),
},
),
_ => {
let p = SearchPatch::Text(t.clone());
with_tree(|tr| tr.patch(host, Box::new(p), false));
}
}
},
);
if let Some(sig) = self.scope {
let seed_scope = seed.scope;
bind_seeded(
seed_scope,
move || sig.get(),
move |i| {
let p = SearchPatch::Scope(*i);
with_tree(|tr| tr.patch(host, Box::new(p), false));
},
);
}
if let Some(f) = self.suggestions.clone() {
bind_seeded(
seed.suggestions.clone(),
move || f(&query.get()),
move |list| match placement {
P::Toolbar => day_core::patch_chrome(
chrome,
day_spec::ToolbarPatch::Suggestions {
item: SEARCH_ITEM_ID.to_string(),
list: list.clone(),
},
),
_ => {
let p = SearchPatch::Suggestions(list.clone());
with_tree(|t| t.patch(host, Box::new(p), false));
}
},
);
}
}
}
pub fn nav<K: Route, S: Binding<K>>(selection: S) -> Nav<S, K> {
Nav {
selection,
documents: None,
document_event: None,
icon_progress: None,
refresh: None,
style: NavStyle::default(),
sidebar_toggle: true,
title: TextSource::Static(String::new()),
header: None,
footer: None,
sources: Vec::new(),
destination: None,
routed: true,
restore: None,
retain_selection: None,
reorder_items: None,
collapsed_sections: None,
on_section_expansion: None,
toolbar: Vec::new(),
search: None,
presentation: None,
content_list: None,
content_list_width: day_spec::NAV_LIST_WIDTH,
content_list_pred: None,
detail_visible: None,
detail_title: None,
}
}
#[day_macros::builder(NavBuilder<K>)]
impl<K: Route, S: Binding<K>> Nav<S, K> {
pub fn refreshable<M>(
mut self,
refreshing: impl IntoReactive<bool, M>,
action: impl Fn() + 'static,
) -> Self {
self.refresh = Some((refreshing.into_reactive(), Rc::new(action)));
self
}
pub(crate) fn document_mode(
mut self,
config: day_spec::props::DocumentTabsConfig,
event: impl Fn(&Event) + 'static,
) -> Self {
self.documents = Some(config);
self.document_event = Some(Rc::new(event));
self
}
pub fn sections(mut self, sections: Sections) -> Self {
self.collapsed_sections = Some(sections.collapsed);
self.on_section_expansion = sections.on_change;
self
}
pub fn section_id<M>(
mut self,
id: impl Into<String>,
title: impl IntoText<M>,
content: impl NavContent<K>,
) -> Self {
self.push_section(
SectionHeader {
id: Some(id.into()),
title: title.into_text(),
},
content,
);
self
}
fn push_section(&mut self, header: SectionHeader, content: impl NavContent<K>) {
let mut sources = Vec::new();
content.push_sources(&mut sources);
let mut sources = sources.into_iter();
match sources.next() {
Some(ItemSource::Static(mut it)) => {
if it.section.is_none() {
it.section = Some(header);
}
self.sources.push(ItemSource::Static(it));
}
Some(ItemSource::Dynamic(f)) => {
self.sources.push(ItemSource::Dynamic(Box::new(move || {
let mut rows = f();
if let Some(first) = rows.first_mut()
&& first.section.is_none()
{
first.section = Some(header.clone());
}
rows
})));
}
None => {}
}
self.sources.extend(sources);
}
pub fn reorder_items(
mut self,
eligible: impl Fn(&K) -> bool + 'static,
moved: impl Fn(K, K) + 'static,
) -> Self {
self.reorder_items = Some(SidebarDrag::Reorder(NavReorder {
eligible: Rc::new(eligible),
moved: Rc::new(moved),
}));
self
}
pub fn regroup_items(
mut self,
drag: impl Fn(&K) -> NavDrag + 'static,
dropped: impl Fn(NavDrop<K>) + 'static,
) -> Self {
self.reorder_items = Some(SidebarDrag::Regroup(NavRegroup {
drag: Rc::new(drag),
dropped: Rc::new(dropped),
}));
self
}
pub fn retain_selection_when(mut self, keep: impl Fn(&K) -> bool + 'static) -> Self {
self.retain_selection = Some(Rc::new(keep));
self
}
pub fn icon_progress(mut self, progress: impl Fn() -> Vec<(K, Option<f64>)> + 'static) -> Self {
self.icon_progress = Some(Rc::new(progress));
self
}
pub fn style(mut self, style: NavStyle) -> Self {
self.style = style;
self
}
pub fn title<M>(mut self, t: impl IntoText<M>) -> Self {
self.title = t.into_text();
self
}
pub fn section<M>(mut self, title: impl IntoText<M>, content: impl NavContent<K>) -> Self {
self.push_section(
SectionHeader {
id: None,
title: title.into_text(),
},
content,
);
self
}
pub fn header<P: Piece>(mut self, build: impl FnOnce() -> P + 'static) -> Self {
self.header = Some(Box::new(move || AnyPiece::new(build())));
self
}
pub fn footer<P: Piece>(mut self, build: impl FnOnce() -> P + 'static) -> Self {
self.footer = Some(Box::new(move || AnyPiece::new(build())));
self
}
pub fn presentation(mut self, presentation: day_spec::props::NavPresentation) -> Self {
self.presentation = Some(presentation);
self
}
pub fn content_list(mut self, list: ContentList<K>) -> Self {
self.content_list = Some(list.build);
self.content_list_width = list.width;
self.content_list_pred = list.pred;
self.detail_visible = list.detail_visible;
self.detail_title = list.detail_title;
self
}
pub fn item<M, P: Piece>(
self,
key: impl Into<K>,
title: impl IntoText<M>,
build: impl Fn() -> P + 'static,
) -> Self {
self.items(crate::item(key, title).page(build))
}
pub fn items(mut self, content: impl NavContent<K>) -> Self {
content.push_sources(&mut self.sources);
self
}
pub fn destination<P: Piece>(mut self, build: impl Fn(&K) -> P + 'static) -> Self {
self.destination = Some(Rc::new(move |k| AnyPiece::new(build(k))));
self
}
pub fn local(mut self) -> Self {
self.routed = false;
self
}
pub fn restore(mut self, key: impl Into<String>) -> Self {
self.restore = Some(key.into());
self
}
pub fn toolbar<M>(mut self, content: impl crate::ToolbarContent<M>) -> Self {
self.toolbar.push(content.into_source());
self
}
pub fn no_sidebar_toggle(mut self) -> Self {
self.sidebar_toggle = false;
self
}
pub fn searchable(mut self, search: Search) -> Self {
let (hides_on_scroll, initially_hidden) = match search.reveal {
SearchReveal::Always => (false, false),
SearchReveal::Scrolls => (true, false),
SearchReveal::OnPull => (true, true),
};
self.search = Some(SearchSpec {
query: search.query,
prompt: search.prompt,
placement: search.placement,
hides_on_scroll,
initially_hidden,
scopes: search.scopes,
scope: search.scope,
suggestions: search.suggestions,
submit: search.submit,
});
self
}
}
impl<K: Route, S: Binding<K>> Piece for Nav<S, K> {
fn build(self, cx: &mut BuildCx) -> RNode {
build_selector(self, cx)
}
}
fn restore_selection<K: Route, S: Binding<K>>(
restore: &Option<String>,
selection: &S,
items: &[K],
) {
if let Some(key) = restore.as_deref()
&& !day_core::has_launch_deeplink()
&& let Some(saved) = day_core::nav_store_load(key)
&& let Some(k) = K::from_key(&saved)
&& (saved.is_empty() || items.iter().any(|x| x.key() == saved))
{
selection.write(k);
}
}
fn persist_selection<K: Route, S: Binding<K>>(restore: Option<String>, selection: &S) {
if let Some(key) = restore {
let s = selection.clone();
bind(
move || s.read().key(),
move |k: &String| day_core::nav_store_save(&key, k),
);
}
}
struct GatedDetail<K: Route> {
open: AnyBinding<bool>,
list: Rc<dyn Fn() -> AnyPiece>,
items: Rc<SelItems<K>>,
key: K,
title: String,
retitle: Option<TextSource>,
detail_title: Option<TextSource>,
host_cx: NavHostCx,
toolbar: Vec<crate::ToolbarSource>,
}
impl<K: Route> Clone for GatedDetail<K> {
fn clone(&self) -> Self {
GatedDetail {
open: self.open,
list: self.list.clone(),
items: self.items.clone(),
key: self.key.clone(),
title: self.title.clone(),
retitle: self.retitle.clone(),
detail_title: self.detail_title.clone(),
host_cx: self.host_cx.clone(),
toolbar: self.toolbar.clone(),
}
}
}
fn gated_detail_piece<K: Route>(
cfg: GatedDetail<K>,
pres: Signal<day_spec::props::NavPresentation>,
window: RNode,
) -> AnyPiece {
let (c1, c2) = (cfg.clone(), cfg);
AnyPiece::new(
when(
move || {
pres.get().rows_are_chrome()
&& !day_core::window_size_class(window).is_some_and(|c| c.prefers_split())
},
move || gated_detail_nested(c1.clone()),
)
.otherwise(move || gated_detail_merged(c2.clone())),
)
}
fn gated_detail_nested<K: Route>(cfg: GatedDetail<K>) -> impl Piece {
piece_fn(move |cx| {
use day_spec::props::{NavPageProps, NavPresentation, NavProps, Pane};
let sizes: Rc<RefCell<std::collections::HashMap<RNode, Size>>> = Rc::default();
let host = cx.native(
kinds::NAV,
&NavProps {
documents: None,
title: cfg.title.clone(),
presentation: NavPresentation::Stack,
adaptive: false,
sidebar_toggle: false,
search: None,
list_width: None,
list_visible: true,
},
Rc::new(NavLayout::stack(sizes.clone())),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::Yes,
);
let owners: Rc<RefCell<Vec<PopOwner>>> = Rc::default();
let target = NavHostCx {
host,
sizes: sizes.clone(),
owners: owners.clone(),
split: Rc::new(Cell::new(false)),
guards: Rc::default(),
};
let root_page = nav_page(
host,
&NavPageProps {
title: cfg.title.clone(),
pane: Pane::Detail,
},
&sizes,
);
let covered = within_page_gate(list_layer_gate(None, Some(cfg.open), None));
with_nav_host(Some(target.clone()), || {
day_core::with_page_gated(
root_page,
Some(covered),
day_spec::ToolbarColumn::List,
|| {
let mut pcx = BuildCx::new(root_page);
let _ = (cfg.list)().grow().build(&mut pcx);
},
);
});
for source in cfg.toolbar.clone() {
crate::contribute(day_core::Chrome::Page(root_page), source);
}
{
let owners = owners.clone();
cx.on(host, move |ev| match ev {
Event::NavBack { already_popped } => {
let top = owners.borrow().last().cloned();
if let Some(f) = top {
f(*already_popped);
}
}
Event::RouteRequested(route) => {
let _ = day_core::navigate(route);
}
_ => {}
});
}
wire_gated_detail(&cfg, target);
host
})
}
fn gated_detail_merged<K: Route>(cfg: GatedDetail<K>) -> impl Piece {
piece_fn(move |cx| {
let target = cfg.host_cx.clone();
let covered = within_page_gate(list_layer_gate(None, Some(cfg.open), None));
let page = match day_core::current_chrome() {
day_core::Chrome::Page(p) => p,
day_core::Chrome::Window(w) => w,
};
let node = with_nav_host(Some(target.clone()), || {
day_core::with_page_gated(page, Some(covered), day_spec::ToolbarColumn::List, || {
(cfg.list)().grow().build(cx)
})
});
wire_gated_detail(&cfg, target);
node
})
}
fn wire_gated_detail<K: Route>(cfg: &GatedDetail<K>, target: NavHostCx) {
use day_spec::props::{NavPageProps, NavPatch, Pane};
struct Layer {
scope: Scope,
page: RNode,
}
let layer: Rc<RefCell<Option<Layer>>> = Rc::default();
let native_popped: Rc<Cell<usize>> = Rc::default();
let open = cfg.open;
let owner_scope = Scope::current();
let owner: PopOwner = {
let native_popped = native_popped.clone();
Rc::new(move |already_popped: bool| {
if already_popped {
native_popped.set(native_popped.get() + 1);
}
open.set(false);
})
};
let detail_src = cfg.detail_title.clone();
let root_title = cfg.title.clone();
let push = {
let (layer, target, owner) = (layer.clone(), target.clone(), owner.clone());
let (items, key) = (cfg.items.clone(), cfg.key.clone());
let (detail_src, root_title) = (detail_src.clone(), root_title.clone());
move || {
if layer.borrow().is_some() {
return;
}
let title = detail_src
.as_ref()
.map(TextSource::initial)
.unwrap_or_else(|| root_title.clone());
let page = nav_page(
target.host,
&NavPageProps {
title: title.clone(),
pane: Pane::Detail,
},
&target.sizes,
);
target.owners.borrow_mut().push(owner.clone());
let scope = owner_scope.enter(Scope::child);
let tc = target.clone();
scope.enter(|| {
let content = items.build_page(&key);
with_nav_host(Some(tc), || {
day_core::with_page(page, || {
let mut c = BuildCx::new(page);
let _ = content.grow().build(&mut c);
});
});
});
let immersive = items.immersive_of(&key.key());
with_tree(|t| {
t.patch(
target.host,
Box::new(NavPatch::Pushed { title, immersive }),
false,
);
t.mark_layout_dirty();
t.layout_if_needed();
});
*layer.borrow_mut() = Some(Layer { scope, page });
}
};
let pop = {
let (layer, target, native_popped) = (layer.clone(), target.clone(), native_popped.clone());
move || {
let taken = layer.borrow_mut().take();
let Some(l) = taken else {
return;
};
l.scope.dispose();
if native_popped.get() > 0 {
native_popped.set(native_popped.get() - 1);
} else {
with_tree(|t| t.patch(target.host, Box::new(NavPatch::Popped), false));
}
target.owners.borrow_mut().pop();
target.sizes.borrow_mut().remove(&l.page);
with_tree(|t| {
t.remove_subtree(l.page);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
};
{
let (push, pop) = (push, pop.clone());
bind(
move || open.get(),
move |v: &bool| if *v { push() } else { pop() },
);
}
{
let (retitle, host) = (cfg.retitle.clone(), target.host);
let top_title = move || {
let root = || {
retitle
.as_ref()
.map_or_else(|| root_title.clone(), TextSource::resolve)
};
if open.get() {
detail_src.as_ref().map_or_else(root, TextSource::resolve)
} else {
root()
}
};
let seed = day_reactive::untrack(&top_title);
bind_seeded(seed, top_title, move |t: &String| {
with_tree(|tr| tr.patch(host, Box::new(NavPatch::Title(t.clone())), false));
});
}
{
let (o_push, o_pop) = (open, open);
register_route_surface(
move |k| {
if k.is_empty() && o_push.peek() {
o_push.set(false);
true
} else {
false
}
},
move |_| {
if o_pop.peek() {
o_pop.set(false);
true
} else {
false
}
},
String::new,
|_| false,
Vec::new,
);
}
owner_scope.on_cleanup(move || {
let alive = with_tree(|t| t.node_kind(target.host).is_some());
if alive {
pop();
} else if let Some(l) = layer.borrow_mut().take() {
l.scope.dispose();
}
});
}
fn build_selector<K: Route, S: Binding<K>>(sel: Nav<S, K>, cx: &mut BuildCx) -> RNode {
use day_spec::props::{
NavMenuPatch, NavMenuProps, NavPageProps, NavPatch, NavPresentation, NavProps, Pane,
};
let icon_progress = sel.icon_progress;
let progress_revision = Signal::new(0u64);
let window = day_core::window_being_built();
let can_split =
with_tree(|t| t.capability(day_spec::Cap::NavSplit)) == day_spec::Support::Native;
let can_tabs =
with_tree(|t| t.capability(day_spec::Cap::NavTabs)) != day_spec::Support::Unsupported;
let adaptive_tabs = with_tree(|t| t.capability(day_spec::Cap::NavTabsAdaptive))
!= day_spec::Support::Unsupported;
let represent = with_tree(|t| t.capability(day_spec::Cap::NavRepresent));
let we_drive = represent == day_spec::Support::Native;
let toolkit_drives = represent == day_spec::Support::Emulated;
let list_cap = with_tree(|t| t.capability(day_spec::Cap::NavContentList));
let size_decides = we_drive || toolkit_drives;
let requested = sel.presentation;
let style = match sel.style {
NavStyle::Automatic if !can_tabs => NavStyle::Sidebar,
s => s,
};
let resolve = move |class: Option<day_spec::SizeClass>| -> NavPresentation {
if let Some(p) = requested {
return match p {
NavPresentation::Split if !can_split => NavPresentation::Stack,
NavPresentation::Tabs | NavPresentation::Rail if !can_tabs => {
if can_split {
NavPresentation::Split
} else {
NavPresentation::Stack
}
}
p => p,
};
}
match style {
NavStyle::Tabs => NavPresentation::Tabs,
NavStyle::Sidebar => match class {
_ if !can_split => NavPresentation::Stack,
Some(c) if size_decides && !c.prefers_split() => NavPresentation::Stack,
_ => NavPresentation::Split,
},
NavStyle::Automatic => {
let compact = if adaptive_tabs {
NavPresentation::Tabs
} else {
NavPresentation::Stack
};
if !size_decides {
return if can_split {
NavPresentation::Split
} else {
compact
};
}
if !can_split {
return compact;
}
match class.map(|c| c.width) {
Some(day_spec::WidthClass::Compact) => compact,
Some(day_spec::WidthClass::Medium) => NavPresentation::Rail,
_ => NavPresentation::Split,
}
}
}
};
let presentation = resolve(day_core::window_size_class_untracked(window));
let adaptive = requested.is_none();
let lowered =
if toolkit_drives && adaptive && can_tabs && adaptive_tabs && style != NavStyle::Sidebar {
NavPresentation::Tabs
} else if toolkit_drives && adaptive && can_split && style != NavStyle::Tabs {
NavPresentation::Split
} else {
presentation
};
let presentation = if lowered == NavPresentation::Tabs && toolkit_drives && adaptive {
NavPresentation::Tabs
} else {
presentation
};
let native_list = sel.content_list.is_some()
&& list_cap != day_spec::Support::Unsupported
&& lowered != NavPresentation::Tabs;
let merged_list = native_list && list_cap == day_spec::Support::Emulated;
let split = presentation.is_split();
let presentation_cell = Rc::new(Cell::new(presentation));
let presentation_sig = Signal::new(presentation);
let split_cell = Rc::new(Cell::new(split));
let sidebar_cell: Rc<Cell<Option<RNode>>> = Rc::new(Cell::new(None));
let list_cell: Rc<Cell<Option<RNode>>> = Rc::new(Cell::new(None));
let list_shown = Rc::new(Cell::new(true));
let list_in_stack = Rc::new(Cell::new(false));
let selection = sel.selection;
let routed = sel.routed;
let restore = sel.restore;
let title_s = sel.title.initial();
let host_toolbar = sel.toolbar;
let gated_toolbar = host_toolbar.clone();
let sidebar_toggle =
sel.sidebar_toggle && matches!(sel.style, NavStyle::Sidebar | NavStyle::Automatic);
let detail_title = sel.detail_title;
let search_spec = sel.search;
let search_seed = search_spec.as_ref().map(SearchSpec::lower);
let search = search_seed.clone();
let items = Rc::new(SelItems::from_sources(sel.sources, sel.destination));
let rows0 = day_reactive::untrack(|| items.derive());
let shown0 = rows0.shown();
let (typed0, titles0, icons0) = (rows0.keys, rows0.titles, rows0.icons);
restore_selection(&restore, &selection, &typed0);
let typed: Rc<RefCell<Vec<K>>> = Rc::new(RefCell::new(typed0));
let titles: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(titles0));
let initial_list_visible = match sel.content_list_pred.as_ref() {
Some(pred) if native_list => {
let restored = selection.peek();
let first = if restored.key().is_empty() {
typed.borrow().first().cloned()
} else {
Some(restored)
};
first.as_ref().is_none_or(|k| pred(k))
}
_ => true,
};
list_shown.set(initial_list_visible);
let sizes: Rc<RefCell<std::collections::HashMap<RNode, Size>>> = Rc::default();
let host = cx.native(
kinds::NAV,
&NavProps {
documents: sel.documents.clone(),
title: title_s.clone(),
presentation: lowered,
adaptive,
sidebar_toggle,
search,
list_width: native_list.then_some(sel.content_list_width),
list_visible: initial_list_visible,
},
Rc::new(NavLayout {
sizes: sizes.clone(),
presentation: presentation_cell.clone(),
sidebar: sidebar_cell.clone(),
list: list_cell.clone(),
list_width: sel.content_list_width,
list_visible: list_shown.clone(),
}),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::Yes,
);
if let Some(event) = sel.document_event.clone() {
cx.on(host, move |ev| event(ev));
}
let owners: Rc<RefCell<Vec<PopOwner>>> = Rc::default();
let host_cx = NavHostCx {
host,
sizes: sizes.clone(),
owners: owners.clone(),
split: split_cell.clone(),
guards: Rc::default(),
};
let root_page = nav_page(
host,
&NavPageProps {
title: title_s.clone(),
pane: Pane::Sidebar,
},
&sizes,
);
sidebar_cell.set(Some(root_page));
if let Some((spec, seed)) = search_spec.as_ref().zip(search_seed.as_ref()) {
spec.install(host, day_core::Chrome::Page(root_page), seed);
let query = spec.query;
let scope_sig = spec.scope;
let submit = spec.submit.clone();
cx.on(host, move |ev| match ev {
Event::SearchChanged(text) => query.set(text.clone()),
Event::SearchSubmitted(text) => {
if let Some(f) = &submit {
f(text);
}
}
Event::SearchScopeChanged(i) => {
if let Some(s) = scope_sig {
s.set(*i);
}
}
_ => {}
});
}
day_core::with_page_in(
root_page,
None,
day_spec::ToolbarColumn::Sidebar,
window,
|| {
if sidebar_toggle
&& with_tree(|t| t.capability(day_spec::Cap::Toolbar))
!= day_spec::Support::Unsupported
{
crate::contribute(
day_core::Chrome::Page(root_page),
crate::ToolbarSource::Fixed(vec![crate::sidebar_toggle_item(host)]),
);
}
for source in host_toolbar {
crate::contribute(day_core::Chrome::Page(root_page), source);
}
},
);
let menu_holder: Rc<Cell<Option<RNode>>> = Rc::new(Cell::new(None));
let nav_reorder = day_core::capability(day_spec::Cap::NavReorder);
let native_reorder = sel
.reorder_items
.clone()
.filter(|_| nav_reorder == day_spec::Support::Native && sel.collapsed_sections.is_none());
let movable_of = native_reorder.clone();
{
let (mh, ks, s, ts) = (
menu_holder.clone(),
typed.clone(),
selection.clone(),
titles.clone(),
);
let (titles_init, icons_init) = (titles.borrow().clone(), icons0.clone());
let selected_init = {
let key = selection.peek().key().to_string();
typed
.borrow()
.iter()
.position(|k| k.key() == key)
.filter(|_| !key.is_empty())
};
let (badges_init, sections_init) = (rows0.badges.clone(), rows0.sections.clone());
let (badge_icons_init, badge_tints_init) =
(rows0.badge_icons.clone(), rows0.badge_tints.clone());
let tints_init = rows0.tints.clone();
let menus_init = rows0.menus.clone();
let reorders_natively = native_reorder.is_some();
let rows_script = items.clone();
let movable_init: Vec<bool> = movable_of
.as_ref()
.map(|r| {
typed
.borrow()
.iter()
.map(|k| r.drag(k) != NavDrag::Fixed)
.collect()
})
.unwrap_or_default();
let regroup_init = matches!(movable_of, Some(SidebarDrag::Regroup(_)));
let sorted_init: Vec<bool> = match &movable_of {
Some(r @ SidebarDrag::Regroup(_)) => typed
.borrow()
.iter()
.map(|k| r.drag(k) == NavDrag::Regroup)
.collect(),
_ => Vec::new(),
};
let document_keys = if sel.documents.is_some() {
typed.borrow().iter().map(Route::key).collect()
} else {
Vec::new()
};
let document_event = sel.document_event.clone();
let refresh = sel.refresh;
let list_pred_click = sel.content_list_pred.clone();
let pres_click = presentation_cell.clone();
let detail_visible_click = sel.detail_visible;
let menu_piece = if let Some(collapsed) = sel.collapsed_sections {
section_sidebar(
items.clone(),
selection.clone(),
collapsed,
sel.on_section_expansion,
icon_progress.clone(),
)
} else if let Some(reorder) = sel
.reorder_items
.filter(|_| nav_reorder == day_spec::Support::Emulated)
{
reorder_sidebar(
items.clone(),
selection.clone(),
reorder,
icon_progress.clone(),
)
} else {
piece_fn(move |mcx| {
let node = mcx.native(
kinds::NAV_MENU,
&NavMenuProps {
document_keys,
items: titles_init,
icons: icons_init,
badges: badges_init,
badge_icons: badge_icons_init,
badge_tints: badge_tints_init,
sections: sections_init,
tints: tints_init,
menus: menus_init,
movable: movable_init,
regroup: regroup_init,
sorted: sorted_init,
selected: selected_init,
},
Rc::new(LeafLayout),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::No,
);
mh.set(Some(node));
match native_reorder.clone() {
Some(SidebarDrag::Reorder(reorder)) => {
let rows = rows_script.clone();
day_core::set_scripted_reorder(node, move |from, to| {
let NavRows { keys, sections, .. } =
day_reactive::untrack(|| rows.derive());
script_nav_reorder(&keys, §ions, &reorder, from, to)
});
}
Some(SidebarDrag::Regroup(regroup)) => {
let rows = rows_script.clone();
day_core::set_scripted_reorder(node, move |from, to| {
let rows =
RegroupRows::from_nav(day_reactive::untrack(|| rows.derive()));
if let Some(drop) = rows.display_drop(®roup, from, to)? {
(regroup.dropped)(drop);
}
Ok(to)
});
}
None => {}
}
if let Some((refreshing, action)) = refresh {
let refreshing = Rc::new(refreshing);
let state = refreshing.clone();
bind(
move || state.get(),
move |busy| {
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::RefreshPatch { refreshing: *busy }),
false,
)
});
},
);
mcx.on(node, move |event| {
if matches!(event, Event::RefreshRequested) {
if !day_reactive::untrack(|| refreshing.get()) {
action();
}
let busy = day_reactive::untrack(|| refreshing.get());
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::RefreshPatch { refreshing: busy }),
false,
)
});
}
});
}
if let Some(event) = document_event {
mcx.on(node, move |ev| event(ev));
}
if let Some(SidebarDrag::Regroup(regroup)) = native_reorder.clone() {
let rows = rows_script.clone();
mcx.on(node, move |ev| {
if let Event::NavRegroup {
from,
section,
before,
} = *ev
{
let rows =
RegroupRows::from_nav(day_reactive::untrack(|| rows.derive()));
if let Ok(Some(drop)) = rows.drop_at(®roup, from, section, before) {
(regroup.dropped)(drop);
}
}
});
}
if let Some(SidebarDrag::Reorder(reorder)) = native_reorder {
let keys = ks.clone();
mcx.on(node, move |ev| {
if let Event::ListReorder { from, to } = *ev {
let pair = {
let keys = keys.borrow();
keys.get(from).cloned().zip(keys.get(to).cloned())
};
if let Some((moved, target)) = pair
&& (reorder.eligible)(&moved)
&& (reorder.eligible)(&target)
{
(reorder.moved)(moved, target);
}
}
});
}
mcx.on(node, move |ev| {
if let Event::SelectionChanged(i) = ev
&& let Some(k) = ks.borrow().get(*i as usize)
{
let label = ts.borrow().get(*i as usize).cloned();
day_core::note_navigation(&k.key(), label.as_deref());
s.write(k.clone());
if merged_list
&& pres_click.get() == NavPresentation::Stack
&& detail_visible_click.is_some_and(|visible| !visible.peek())
&& list_pred_click.as_ref().is_none_or(|p| p(k))
{
with_tree(|t| {
t.patch(host, Box::new(NavPatch::ListInStack(true)), false)
});
}
}
});
node
})
.id(if reorders_natively {
day_core::id!(nav_reorder_list)
} else {
day_core::id!(nav_menu)
})
.any()
};
let content: AnyPiece = match sel.header {
Some(h) => column((h(), menu_piece))
.spacing(4.0)
.align(HAlign::Leading)
.any(),
None => column((menu_piece,))
.spacing(4.0)
.align(HAlign::Leading)
.any(),
};
let content = match sel.footer {
Some(footer) => column((content.grow(), footer())).grow().any(),
None => content,
};
with_nav_host(Some(host_cx.clone()), || {
let mut pcx = BuildCx::new(root_page);
let _ = content.build(&mut pcx);
});
}
let list_build = sel.content_list.clone();
let list_pred = sel.content_list_pred.clone();
let detail_visible = sel.detail_visible;
let list_visible_sig: Rc<Cell<Option<Signal<bool>>>> = Rc::new(Cell::new(None));
let apply_list_visible: Rc<dyn Fn(&K)> = {
let (shown, pred) = (list_shown.clone(), list_pred.clone());
let mirror = list_visible_sig.clone();
Rc::new(move |key: &K| {
if !native_list {
return;
}
let want = pred.as_ref().is_none_or(|p| p(key));
if want != shown.get() {
shown.set(want);
if let Some(sig) = mirror.get() {
sig.set(want);
}
with_tree(|t| t.patch(host, Box::new(NavPatch::ListVisible(want)), false));
}
})
};
if native_list && let Some(build_list) = list_build.clone() {
let page = nav_page(
host,
&NavPageProps {
title: title_s.clone(),
pane: Pane::List,
},
&sizes,
);
list_cell.set(Some(page));
let visible = Signal::new(initial_list_visible);
list_visible_sig.set(Some(visible));
let (dv, pres_sig) = (detail_visible, presentation_sig);
let gate: Rc<dyn Fn() -> bool> = Rc::new(move || {
visible.try_get().unwrap_or(false)
&& (pres_sig.try_get().is_some_and(|p| p.is_split())
|| dv.is_none_or(|d| !d.try_get().unwrap_or(false)))
});
with_nav_host(None, || {
day_core::with_page_gated(page, Some(gate), day_spec::ToolbarColumn::List, || {
let mut pcx = BuildCx::new(page);
let _ = build_list().build(&mut pcx);
});
});
}
let compose: Option<DestFn<K>> = if let (Some(list), true) = (list_build.clone(), !native_list)
{
let pred = list_pred.clone();
let width = sel.content_list_width;
let dv = detail_visible;
let items_c = items.clone();
let (typed_c, titles_c) = (typed.clone(), titles.clone());
let pres = presentation_sig;
let win = window;
let host_cx_g = host_cx.clone();
let detail_title_g = detail_title.clone();
Some(Rc::new(move |key: &K| {
if pred.as_ref().is_some_and(|p| !p(key)) {
return items_c.build_page(key);
}
let (l1, l2) = (list.clone(), list.clone());
let (i1, i2) = (items_c.clone(), items_c.clone());
let (k1, k2) = (key.clone(), key.clone());
let (hc, dt) = (host_cx_g.clone(), detail_title_g.clone());
let (tk, tt) = (typed_c.clone(), titles_c.clone());
let ba = gated_toolbar.clone();
let page: Rc<RefCell<Option<PageContext>>> = Rc::default();
let at_build = page.clone();
let arms = when(
move || {
let p = pres.get();
p.is_split()
|| (p.rows_are_chrome()
&& day_core::window_size_class(win).is_some_and(|c| c.prefers_split()))
},
{
let page = page.clone();
move || {
let (l, i, k) = (l1.clone(), i1.clone(), k1.clone());
PageContext::enter(
&page,
row((l().width(width).grow_h(), i.build_page(&k).grow())).grow(),
)
}
},
)
.otherwise(move || {
let (l, i, k) = (l2.clone(), i2.clone(), k2.clone());
PageContext::enter(
&page,
match dv {
Some(d) => {
let retitle = i.static_title(&k.key());
let title = retitle
.as_ref()
.map(TextSource::initial)
.or_else(|| {
let at = tk.borrow().iter().position(|x| x.key() == k.key())?;
tt.borrow().get(at).cloned()
})
.unwrap_or_else(|| k.title());
gated_detail_piece(
GatedDetail {
open: d,
list: l,
items: i,
key: k,
title,
retitle,
detail_title: dt.clone(),
host_cx: hc.clone(),
toolbar: ba.clone(),
},
pres,
win,
)
}
None => i.build_page(&k),
},
)
});
AnyPiece::new(piece_fn(move |cx| {
at_build.replace(Some(PageContext::current()));
arms.build(cx)
}))
}))
} else {
None
};
let sync_menu = {
let mh = menu_holder.clone();
move |idx: Option<usize>| {
if let Some(m) = mh.get() {
with_tree(|t| t.patch(m, Box::new(NavMenuPatch::Selected(idx)), false));
}
with_tree(|t| t.set_probe_selected(host, idx));
}
};
let resident: Rc<RefCell<Vec<ResidentPage>>> = Rc::default();
let current: Rc<RefCell<Option<String>>> = Rc::default();
let nav_scope = Scope::current();
let show = std::rc::Rc::new({
let (items, resident, current, sizes, typed_s, titles_s) = (
items.clone(),
resident.clone(),
current.clone(),
sizes.clone(),
typed.clone(),
titles.clone(),
);
let (sync_menu, owners, host_cx, selection, pres) = (
sync_menu.clone(),
owners.clone(),
host_cx.clone(),
selection.clone(),
presentation_cell.clone(),
);
let (list_pred_s, list_in_stack_s) = (list_pred.clone(), list_in_stack.clone());
let apply_list_visible_s = apply_list_visible.clone();
let compose_s = compose.clone();
let detail_title_s = detail_title.clone();
move |key: &str| {
if current.borrow().as_deref() == Some(key) {
return;
}
let chrome = pres.get().rows_are_chrome();
let gated =
merged_list && pres.get() == NavPresentation::Stack && detail_visible.is_some();
if !chrome {
if let Some(p) = resident.borrow_mut().pop() {
p.scope.dispose();
with_tree(|t| t.patch(host, Box::new(NavPatch::Popped), false));
owners.borrow_mut().pop();
sizes.borrow_mut().remove(&p.node);
with_tree(|t| {
t.remove_subtree(p.node);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
*current.borrow_mut() = None;
if key.is_empty() {
if list_in_stack_s.get() {
list_in_stack_s.set(false);
with_tree(|t| t.patch(host, Box::new(NavPatch::ListInStack(false)), false));
owners.borrow_mut().pop();
}
sync_menu(None);
return;
}
let idx = typed_s.borrow().iter().position(|k| k.key() == key);
let Some(idx) = idx else {
sync_menu(None);
return;
};
let typed_key_now = typed_s.borrow()[idx].clone();
apply_list_visible_s(&typed_key_now);
if let Some(i) = resident.borrow().iter().position(|p| p.key == key) {
with_tree(|t| {
t.patch(host, Box::new(NavPatch::Select(i)), false);
t.mark_layout_dirty();
t.layout_if_needed();
});
*current.borrow_mut() = Some(key.to_string());
sync_menu(Some(idx));
return;
}
let typed_key = typed_key_now;
let title_now = titles_s.borrow()[idx].clone();
let want = native_list && list_pred_s.as_ref().is_none_or(|p| p(&typed_key));
let interposed = gated && want;
if native_list {
if interposed && !list_in_stack_s.get() {
list_in_stack_s.set(true);
with_tree(|t| t.patch(host, Box::new(NavPatch::ListInStack(true)), false));
let s = selection.clone();
owners.borrow_mut().push(Rc::new(move |_already_popped| {
if let Some(root) = K::from_key("") {
s.write(root);
}
}) as PopOwner);
} else if gated && !want && list_in_stack_s.get() {
list_in_stack_s.set(false);
with_tree(|t| t.patch(host, Box::new(NavPatch::ListInStack(false)), false));
owners.borrow_mut().pop();
}
if interposed && !detail_visible.expect("gated implies Some").peek() {
with_tree(|t| t.patch(host, Box::new(NavPatch::ListInStack(true)), false));
*current.borrow_mut() = Some(key.to_string());
sync_menu(Some(idx));
return;
}
}
let (page_title, retitle) = match &detail_title_s {
Some(dt) if want => (dt.initial(), Some(dt.clone())),
_ => (title_now.clone(), items.static_title(key)),
};
let page = nav_page(
host,
&NavPageProps {
title: page_title.clone(),
pane: Pane::Detail,
},
&sizes,
);
if !chrome {
let owner: PopOwner = if interposed && let Some(dv) = detail_visible {
Rc::new(move |_already_popped| {
dv.set(false);
})
} else {
let s = selection.clone();
Rc::new(move |_already_popped| {
if let Some(root) = K::from_key("") {
s.write(root);
}
})
};
owners.borrow_mut().push(owner);
let immersive = items.immersive_of(&typed_key.key());
with_tree(|t| {
t.patch(
host,
Box::new(NavPatch::Pushed {
title: page_title.clone(),
immersive,
}),
false,
);
});
}
let scope = nav_scope.enter(Scope::child);
scope.enter(|| {
let content = match &compose_s {
Some(c) => c(&typed_key),
None => items.build_page(&typed_key),
};
let inner = if chrome { None } else { Some(host_cx.clone()) };
let sel_gate = Some(destination_gate(selection.clone(), key.to_string()));
let build = || {
with_nav_host(inner, || {
day_core::with_page_in(
page,
sel_gate.clone(),
day_spec::ToolbarColumn::Detail,
window,
|| {
let mut c = BuildCx::new(page);
let _ = content.build(&mut c);
},
);
});
};
if chrome {
with_page_active(destination_gate(selection.clone(), key.to_string()), build);
} else {
build();
}
});
resident.borrow_mut().push(ResidentPage {
key: key.to_string(),
scope,
node: page,
});
let at = resident.borrow().len() - 1;
if chrome {
with_tree(|t| t.patch(host, Box::new(NavPatch::Select(at)), false));
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
if let Some(rt) = retitle {
scope.enter(|| {
rt.bind_to(host, |t| Box::new(NavPatch::Title(t)), false);
});
}
*current.borrow_mut() = Some(key.to_string());
sync_menu(Some(idx));
}
});
if (split || presentation.rows_are_chrome() || native_list)
&& selection.peek().key().is_empty()
&& let Some(k) = typed.borrow().first().cloned()
{
selection.write(k);
}
let build_all = {
let (show, typed_a, sel_a) = (show.clone(), typed.clone(), selection.clone());
Rc::new(move || {
let keys: Vec<String> = typed_a.borrow().iter().map(|k| k.key()).collect();
for k in keys {
show(&k);
}
show(&sel_a.peek().key());
})
};
if presentation.rows_are_chrome() {
build_all();
}
{
let (s, show) = (selection.clone(), show.clone());
bind(move || s.read().key(), move |key: &String| show(key));
}
if let Some(dv) = detail_visible {
let (show_dv, current_dv, resident_dv, sizes_dv, owners_dv) = (
show.clone(),
current.clone(),
resident.clone(),
sizes.clone(),
owners.clone(),
);
let (pres_dv, list_in_stack_dv) = (presentation_cell.clone(), list_in_stack.clone());
bind(
move || dv.get(),
move |v: &bool| {
let gated_now = merged_list
&& pres_dv.get() == NavPresentation::Stack
&& list_in_stack_dv.get();
if !gated_now {
return;
}
if *v {
let key = current_dv.borrow().clone();
if resident_dv.borrow().is_empty()
&& let Some(k) = key
{
*current_dv.borrow_mut() = None;
show_dv(&k);
}
} else if let Some(p) = resident_dv.borrow_mut().pop() {
p.scope.dispose();
with_tree(|t| t.patch(host, Box::new(NavPatch::Popped), false));
owners_dv.borrow_mut().pop();
sizes_dv.borrow_mut().remove(&p.node);
with_tree(|t| {
t.remove_subtree(p.node);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
},
);
}
let beside: Rc<Cell<bool>> = Rc::default();
if let Some(dv) = detail_visible
&& list_build.is_some()
&& !native_list
{
let (hc, s, pred, beside) = (
host_cx.clone(),
selection.clone(),
list_pred.clone(),
beside.clone(),
);
bind(
move || {
let k = s.read();
dv.get()
&& presentation_sig.get().is_split()
&& !k.key().is_empty()
&& pred.as_ref().is_none_or(|p| p(&k))
},
move |want: &bool| {
if *want != beside.get() {
beside.set(*want);
hc.vote_guard(*want);
}
},
);
}
let reconcile = {
let (pc, sel_r, typed_r, split_r) = (
presentation_cell.clone(),
selection.clone(),
typed.clone(),
split_cell.clone(),
);
let build_all_r = build_all.clone();
let (resident_r, current_r, sizes_r, owners_r) = (
resident.clone(),
current.clone(),
sizes.clone(),
owners.clone(),
);
let (list_pred_r, list_in_stack_r, show_r) =
(list_pred.clone(), list_in_stack.clone(), show.clone());
let apply_list_visible_r = apply_list_visible.clone();
Rc::new(move |next: NavPresentation| {
let was_chrome = pc.get().rows_are_chrome();
pc.set(next);
split_r.set(next.is_split());
presentation_sig.set(next);
if was_chrome && !next.rows_are_chrome() {
let shown = current_r.borrow().clone();
let gone: Vec<ResidentPage> = {
let mut r = resident_r.borrow_mut();
let (keep, drop): (Vec<_>, Vec<_>) =
r.drain(..).partition(|p| Some(&p.key) == shown.as_ref());
*r = keep;
drop
};
for p in gone {
p.scope.dispose();
sizes_r.borrow_mut().remove(&p.node);
with_tree(|t| t.remove_subtree(p.node));
}
owners_r.borrow_mut().clear();
if let Some(k) = shown {
let s = sel_r.clone();
let owner: PopOwner = Rc::new(move |_already_popped| {
if let Some(root) = K::from_key("") {
s.write(root);
}
});
owners_r.borrow_mut().push(owner);
let _ = k;
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
}
if !was_chrome && next.rows_are_chrome() {
build_all_r();
}
if (next.is_split() || next.rows_are_chrome() || native_list)
&& sel_r.peek().key().is_empty()
&& let Some(k) = typed_r.borrow().first().cloned()
{
sel_r.write(k);
}
if native_list
&& let Some(k) = typed_r
.borrow()
.iter()
.find(|x| Some(&x.key()) == current_r.borrow().as_ref())
.cloned()
{
apply_list_visible_r(&k);
}
if merged_list && let Some(dv) = detail_visible {
if next == NavPresentation::Stack {
let key = current_r.borrow().clone();
let want = key.as_ref().is_some_and(|k| {
typed_r
.borrow()
.iter()
.find(|x| &x.key() == k)
.is_some_and(|tk| list_pred_r.as_ref().is_none_or(|p| p(tk)))
});
if want {
if !list_in_stack_r.get() {
list_in_stack_r.set(true);
with_tree(|t| {
t.patch(host, Box::new(NavPatch::ListInStack(true)), false)
});
}
owners_r.borrow_mut().clear();
let s = sel_r.clone();
owners_r.borrow_mut().push(Rc::new(move |_already_popped| {
if let Some(root) = K::from_key("") {
s.write(root);
}
}) as PopOwner);
if dv.peek() {
owners_r.borrow_mut().push(Rc::new(move |_already_popped| {
dv.set(false);
}) as PopOwner);
} else if let Some(p) = resident_r.borrow_mut().pop() {
p.scope.dispose();
with_tree(|t| t.patch(host, Box::new(NavPatch::Popped), false));
sizes_r.borrow_mut().remove(&p.node);
with_tree(|t| {
t.remove_subtree(p.node);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
} else if next.is_split() {
list_in_stack_r.set(false);
let key = current_r.borrow().clone();
if resident_r.borrow().is_empty()
&& let Some(k) = key
{
*current_r.borrow_mut() = None;
show_r(&k);
} else if !resident_r.borrow().is_empty() {
owners_r.borrow_mut().clear();
let s = sel_r.clone();
owners_r.borrow_mut().push(Rc::new(move |_already_popped| {
if let Some(root) = K::from_key("") {
s.write(root);
}
}) as PopOwner);
}
}
}
})
};
if requested.is_none() && can_split && we_drive {
let rec = reconcile.clone();
bind_seeded(
presentation,
move || resolve(day_core::window_size_class(window)),
move |next: &NavPresentation| {
with_tree(|t| {
t.patch(host, Box::new(NavPatch::Presentation(*next)), false);
t.mark_layout_dirty();
t.layout_if_needed();
});
rec(*next);
},
);
} else if toolkit_drives {
let rec = reconcile.clone();
cx.on(host, move |ev| {
if let Event::NavPresentationChanged(next) = ev {
rec(*next);
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
}
});
}
let icon_progress_enabled = icon_progress.is_some();
let documents = sel.documents.is_some();
let retain_selection = sel.retain_selection;
{
let (items_e, typed_e, titles_e, mh_e, sel_e) = (
items.clone(),
typed.clone(),
titles.clone(),
menu_holder.clone(),
selection.clone(),
);
let (show_e, pres_e) = (show.clone(), presentation_cell.clone());
let last_rows: RefCell<Option<ShownRows>> = RefCell::new(Some(shown0));
let (resident_e, current_e, sizes_e) = (resident.clone(), current.clone(), sizes.clone());
let movable_e = movable_of.clone();
bind(
move || {
let NavRows {
keys: k,
titles: t,
icons: i,
badges: b,
badge_icons: bi,
badge_tints: bt,
section_ids: _,
sections: sc,
tints: tn,
menus: mn,
} = items_e.derive();
(
k.iter().map(|x| x.key()).collect::<Vec<_>>(),
k,
t,
i,
b,
sc,
tn,
mn,
bi,
bt,
)
},
move |(key_strs, keys, ts, ics, bs, scs, tns, mns, bis, bts): &DerivedRows<K>| {
let rows_changed = {
let mut last = last_rows.borrow_mut();
let changed = last.as_ref().and_then(|l: &ShownRows| {
(l.keys == *key_strs && l.sections == *scs && l.menus == *mns).then(|| {
(0..key_strs.len())
.filter(|&i| {
l.titles[i] != ts[i]
|| l.icons[i] != ics[i]
|| l.tints[i] != tns[i]
|| l.badges[i] != bs[i]
|| l.badge_icons[i] != bis[i]
|| l.badge_tints[i] != bts[i]
})
.collect::<Box<[usize]>>()
})
});
*last = Some(ShownRows {
keys: key_strs.clone(),
titles: ts.clone(),
icons: ics.clone(),
badges: bs.clone(),
badge_icons: bis.clone(),
badge_tints: bts.clone(),
sections: scs.clone(),
tints: tns.clone(),
menus: mns.clone(),
});
changed
};
*typed_e.borrow_mut() = keys.clone();
*titles_e.borrow_mut() = ts.clone();
let selected_key = sel_e.peek();
let cur = selected_key.key();
let retain = !cur.is_empty()
&& !key_strs.contains(&cur)
&& retain_selection
.as_ref()
.is_some_and(|keep| keep(&selected_key));
{
let stale: Vec<ResidentPage> = {
let mut r = resident_e.borrow_mut();
let (keep, drop): (Vec<_>, Vec<_>) = r
.drain(..)
.partition(|p| key_strs.contains(&p.key) || (retain && p.key == cur));
*r = keep;
drop
};
if !stale.is_empty() {
if current_e
.borrow()
.as_ref()
.is_some_and(|c| stale.iter().any(|p| &p.key == c))
{
*current_e.borrow_mut() = None;
}
for p in stale {
p.scope.dispose();
sizes_e.borrow_mut().remove(&p.node);
with_tree(|t| t.remove_subtree(p.node));
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
let still = retain || cur.is_empty() || key_strs.iter().any(|k| k == &cur);
if !still && let Some(root) = K::from_key("") {
sel_e.write(root);
}
let mut cur2 = sel_e.peek().key();
let pres_now = pres_e.get();
if (pres_now.is_split()
|| pres_now.rows_are_chrome()
|| (native_list && pres_now != NavPresentation::Stack))
&& cur2.is_empty()
&& let Some(k) = keys.first().cloned()
{
sel_e.write(k.clone());
cur2 = k.key();
}
let selected = key_strs.iter().position(|k| k == &cur2);
if let Some(m) = mh_e.get() {
with_tree(|t| {
t.patch(
m,
Box::new(day_spec::props::NavMenuPatch::Items {
rows_changed: rows_changed.clone(),
document_keys: if documents {
key_strs.clone()
} else {
Vec::new()
}
.into_boxed_slice(),
items: ts.clone(),
icons: ics.clone(),
badges: bs.clone(),
badge_icons: bis.clone(),
badge_tints: bts.clone(),
sections: scs.clone(),
tints: tns.clone(),
menus: mns.clone(),
movable: movable_e
.as_ref()
.map(|r| {
keys.iter().map(|k| r.drag(k) != NavDrag::Fixed).collect()
})
.unwrap_or_default(),
sorted: match &movable_e {
Some(r @ SidebarDrag::Regroup(_)) => {
keys.iter().map(|k| r.drag(k) == NavDrag::Regroup).collect()
}
_ => Vec::new(),
},
selected,
}),
false,
);
});
}
if icon_progress_enabled {
progress_revision.update(|n| *n = n.wrapping_add(1));
}
if pres_now.rows_are_chrome() {
for key in key_strs {
let missing = !resident_e.borrow().iter().any(|p| &p.key == key);
if missing {
show_e(key);
}
}
let order: Vec<usize> = key_strs
.iter()
.filter_map(|key| resident_e.borrow().iter().position(|p| &p.key == key))
.collect();
if order.len() == resident_e.borrow().len()
&& order.iter().enumerate().any(|(i, old)| i != *old)
{
let mut old: Vec<Option<ResidentPage>> =
resident_e.borrow_mut().drain(..).map(Some).collect();
*resident_e.borrow_mut() =
order.iter().map(|i| old[*i].take().unwrap()).collect();
with_tree(|t| t.patch(host, Box::new(NavPatch::Reorder(order)), false));
*current_e.borrow_mut() = None;
}
}
show_e(&cur2);
},
);
}
if let Some(progress) = icon_progress {
let mh = menu_holder.clone();
let keys = typed.clone();
bind(
move || {
let revision = progress_revision.get();
let active = progress();
let keys = keys.borrow();
let values = active
.into_iter()
.filter_map(|(key, fraction)| {
keys.iter().position(|k| k.key() == key.key()).map(|row| {
(
row,
fraction
.filter(|f| f.is_finite())
.map(|f| f.clamp(0.0, 1.0)),
)
})
})
.collect::<Vec<_>>();
(revision, values)
},
move |(_, values)| {
if let Some(menu) = mh.get() {
with_tree(|t| {
t.patch(
menu,
Box::new(NavMenuPatch::IconProgress(values.clone())),
false,
)
});
}
},
);
}
{
let owners = owners.clone();
let (beside, dv_back) = (beside.clone(), detail_visible);
cx.on(host, move |ev| match ev {
Event::NavBack { already_popped } => {
if !*already_popped
&& beside.get()
&& owners.borrow().len() <= 1
&& let Some(dv) = dv_back
{
dv.set(false);
} else {
let top = owners.borrow().last().cloned();
if let Some(f) = top {
f(*already_popped);
}
}
}
Event::RouteRequested(route) => {
let _ = day_core::navigate(route);
}
_ => {}
});
}
let (tp_push, s_push) = (typed.clone(), selection.clone());
let s_pop = selection.clone();
let (dv_pop, lis_pop, pres_pop) = (
detail_visible,
list_in_stack.clone(),
presentation_cell.clone(),
);
let (has_list_pop, pred_pop) = (list_build.is_some(), list_pred.clone());
let s_cur = selection.clone();
let (tp_enter, s_enter) = (typed.clone(), selection.clone());
let s_seg = selection.clone();
let pick =
|tp: &Rc<RefCell<Vec<K>>>, k: &str| tp.borrow().iter().find(|x| x.key() == k).cloned();
let pick_push = pick;
let pick_enter = pick;
if routed {
note_routed_one_of_n("sidebar");
register_route_surface(
move |k| {
if k.is_empty() {
if let Some(root) = K::from_key("") {
s_push.write(root);
true
} else {
false }
} else if let Some(key) = pick_push(&tp_push, k) {
s_push.write(key);
true
} else {
false
}
},
move |_| {
let current = s_pop.peek();
let pres = pres_pop.get();
if current.key().is_empty() {
false
} else if let Some(dv) = dv_pop
&& dv.peek()
&& has_list_pop
&& pred_pop.as_ref().is_none_or(|p| p(¤t))
&& (pres.is_split()
|| (merged_list && pres == NavPresentation::Stack && lis_pop.get()))
{
dv.set(false);
true
} else if let Some(root) = K::from_key("") {
s_pop.write(root);
true
} else {
false
}
},
move || s_cur.peek().key(),
move |k| {
if let Some(key) = pick_enter(&tp_enter, k) {
s_enter.write(key);
true
} else {
false
}
},
move || {
let k = s_seg.peek().key();
if k.is_empty() { Vec::new() } else { vec![k] }
},
);
}
persist_selection(restore, &selection);
host
}
struct StackEntry<K> {
key: K,
scope: Scope,
page: RNode,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BackResponse {
Proceed,
Handled,
}
#[derive(Clone)]
pub struct BackRequest {
pop: Rc<dyn Fn()>,
}
impl BackRequest {
pub fn proceed(&self) {
(self.pop)();
}
}
pub struct NavStack<S: Binding<Vec<K>>, K: Route = String> {
path: S,
title: TextSource,
root: AnyPiece,
destination: Rc<dyn Fn(&K) -> AnyPiece>,
on_back: Option<Rc<dyn Fn(BackRequest) -> BackResponse>>,
restore: Option<String>,
toolbar: Vec<crate::ToolbarSource>,
}
pub fn nav_stack<K: Route, S: Binding<Vec<K>>>(path: S, root: impl Piece) -> NavStack<S, K> {
NavStack {
path,
title: TextSource::Static(String::new()),
root: AnyPiece::new(root),
destination: Rc::new(|_| {
piece_fn(|cx| cx.layout_only(Rc::new(PassThrough), Flex::default(), Boundary::No)).any()
}),
on_back: None,
restore: None,
toolbar: Vec::new(),
}
}
#[day_macros::builder(NavStackBuilder<K>)]
impl<K: Route, S: Binding<Vec<K>>> NavStack<S, K> {
pub fn title<M>(mut self, t: impl IntoText<M>) -> Self {
self.title = t.into_text();
self
}
pub fn toolbar<M>(mut self, content: impl crate::ToolbarContent<M>) -> Self {
self.toolbar.push(content.into_source());
self
}
pub fn destination<P: Piece>(mut self, build: impl Fn(&K) -> P + 'static) -> Self {
self.destination = Rc::new(move |k| AnyPiece::new(build(k)));
self
}
pub fn on_back(mut self, guard: impl Fn(BackRequest) -> BackResponse + 'static) -> Self {
self.on_back = Some(Rc::new(guard));
self
}
pub fn restore(mut self, key: impl Into<String>) -> Self {
self.restore = Some(key.into());
self
}
}
impl<K: Route, S: Binding<Vec<K>>> Piece for NavStack<S, K> {
fn build(self, cx: &mut BuildCx) -> RNode {
use day_spec::props::{NavPageProps, NavPatch, NavPresentation, NavProps, Pane};
let NavStack {
path,
title,
root,
destination: dest,
on_back,
restore,
toolbar,
} = self;
let title_s = title.initial();
let outer_gate = day_core::current_page_gate();
let shown_depth = Signal::new(0usize);
let gate = move |at: usize| -> Option<Rc<dyn Fn() -> bool>> {
let outer = outer_gate.clone();
Some(Rc::new(move || {
outer.as_ref().is_none_or(|g| g()) && shown_depth.get() == at
}))
};
if let Some(key) = restore.as_deref()
&& !day_core::has_launch_deeplink()
&& let Some(saved) = day_core::nav_store_load(key)
{
let (segments, _) = day_core::parse_route(&saved);
let parsed: Option<Vec<K>> = segments.iter().map(|s| K::from_key(s)).collect();
if let Some(v) = parsed {
path.write(v);
}
}
let merge = current_nav_host().filter(|c| !c.split.get());
let entries: Rc<RefCell<Vec<StackEntry<K>>>> = Rc::default();
let native_popped: Rc<Cell<usize>> = Rc::new(Cell::new(0));
let host: RNode;
let sizes: Rc<RefCell<std::collections::HashMap<RNode, Size>>>;
let owners: Rc<RefCell<Vec<PopOwner>>>;
let host_cx: NavHostCx;
let ret_node: RNode;
let merged: bool;
if let Some(ctx) = merge {
host = ctx.host;
sizes = ctx.sizes.clone();
owners = ctx.owners.clone();
host_cx = ctx;
let hc = host_cx.clone();
ret_node = with_nav_host(Some(hc), || root.build(cx));
for source in toolbar {
crate::contribute(day_core::current_chrome(), source);
}
merged = true;
} else {
sizes = Rc::default();
host = cx.native(
kinds::NAV,
&NavProps {
documents: None,
title: title_s.clone(),
presentation: NavPresentation::Stack,
adaptive: false,
sidebar_toggle: false,
search: None,
list_width: None,
list_visible: true,
},
Rc::new(NavLayout::stack(sizes.clone())),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::Yes,
);
owners = Rc::default();
host_cx = NavHostCx {
host,
sizes: sizes.clone(),
owners: owners.clone(),
split: Rc::new(Cell::new(false)),
guards: Rc::default(),
};
let root_page = nav_page(
host,
&NavPageProps {
title: title_s,
pane: Pane::Detail,
},
&sizes,
);
let hc = host_cx.clone();
with_nav_host(Some(hc), || {
day_core::with_page_gated(
root_page,
gate(0),
day_spec::ToolbarColumn::Detail,
|| {
let mut pcx = BuildCx::new(root_page);
let _ = root.build(&mut pcx);
},
);
});
for source in toolbar {
crate::contribute(day_core::Chrome::Page(root_page), source);
}
ret_node = host;
merged = false;
}
let nav_scope = Scope::current();
let raw_pop: Rc<dyn Fn()> = {
let p = path.clone();
Rc::new(move || {
let mut v = p.peek();
if v.pop().is_some() {
p.write(v);
}
})
};
let depth = {
let p = path.clone();
move || p.peek().len()
};
let run_back: Rc<dyn Fn()> = {
let (raw_pop, depth, guard) = (raw_pop.clone(), depth.clone(), on_back.clone());
Rc::new(move || {
if depth() == 0 {
return; }
match &guard {
Some(g) => {
let req = BackRequest {
pop: raw_pop.clone(),
};
if g(req) == BackResponse::Proceed {
raw_pop();
}
}
None => raw_pop(),
}
})
};
let stack_owner: PopOwner = {
let (native_popped, raw_pop, run_back) =
(native_popped.clone(), raw_pop.clone(), run_back.clone());
Rc::new(move |already_popped: bool| {
if already_popped {
native_popped.set(native_popped.get() + 1);
raw_pop();
} else {
run_back();
}
})
};
let guard_armed_sent = Rc::new(Cell::new(false));
let has_guard = on_back.is_some();
let reconcile = {
let (entries, sizes, dest, native_popped, owners, host_cx, stack_owner) = (
entries.clone(),
sizes.clone(),
dest.clone(),
native_popped.clone(),
owners.clone(),
host_cx.clone(),
stack_owner.clone(),
);
let guard_armed_sent = guard_armed_sent.clone();
move |want: &Vec<K>| {
let common = {
let ents = entries.borrow();
let mut i = 0;
while i < ents.len() && i < want.len() && ents[i].key == want[i] {
i += 1;
}
i
};
while entries.borrow().len() > common {
let e = entries.borrow_mut().pop().unwrap();
if native_popped.get() > 0 {
native_popped.set(native_popped.get() - 1);
} else {
with_tree(|t| t.patch(host, Box::new(NavPatch::Popped), false));
}
e.scope.dispose();
sizes.borrow_mut().remove(&e.page);
with_tree(|t| t.remove_subtree(e.page));
owners.borrow_mut().pop();
}
for (at, key) in want.iter().enumerate().skip(common) {
let title = key.title();
let page = nav_page(
host,
&NavPageProps {
title: title.clone(),
pane: Pane::Detail,
},
&sizes,
);
let scope = nav_scope.enter(Scope::child);
let hc = host_cx.clone();
scope.enter(|| {
let content = (dest)(key);
with_nav_host(Some(hc), || {
day_core::with_page_gated(
page,
gate(at + 1),
day_spec::ToolbarColumn::Detail,
|| {
let mut c = BuildCx::new(page);
let _ = content.build(&mut c);
},
);
});
});
with_tree(|t| {
t.patch(
host,
Box::new(NavPatch::Pushed {
title,
immersive: false,
}),
false,
)
});
owners.borrow_mut().push(stack_owner.clone());
entries.borrow_mut().push(StackEntry {
key: key.clone(),
scope,
page,
});
}
shown_depth.set(want.len());
if has_guard {
let armed = !entries.borrow().is_empty();
if armed != guard_armed_sent.get() {
guard_armed_sent.set(armed);
host_cx.vote_guard(armed);
}
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
}
};
{
let p = path.clone();
bind(move || p.read(), move |want: &Vec<K>| reconcile(want));
}
{
let p = path.clone();
let owners = owners.clone();
let stack_owner = stack_owner.clone();
bind(
move || p.read().last().map(Route::title),
move |title: &Option<String>| {
if let Some(title) = title
&& owners
.borrow()
.last()
.is_some_and(|owner| Rc::ptr_eq(owner, &stack_owner))
{
with_tree(|t| {
t.patch(host, Box::new(NavPatch::Title(title.clone())), false)
});
}
},
);
}
if !merged && let TextSource::Dyn(_) = &title {
let (p, src) = (path.clone(), title.clone());
bind_seeded(
(src.initial(), true),
move || (src.resolve(), p.read().is_empty()),
move |(t, at_root): &(String, bool)| {
if *at_root {
with_tree(|tr| tr.patch(host, Box::new(NavPatch::Title(t.clone())), false));
}
},
);
}
if let Some(key) = restore {
let p = path.clone();
bind(
move || {
let keys: Vec<String> = p.read().iter().map(|k| k.key()).collect();
day_core::encode_route(&keys, &[])
},
move |s: &String| day_core::nav_store_save(&key, s),
);
}
if !merged {
let owners_h = owners.clone();
cx.on(host, move |ev| match ev {
Event::NavBack { already_popped } => {
let top = owners_h.borrow().last().cloned();
if let Some(f) = top {
f(*already_popped);
}
}
Event::RouteRequested(route) => {
let _ = day_core::navigate(route);
}
_ => {}
});
}
if merged {
let (entries_c, sizes_c, owners_c, native_popped_c) = (
entries.clone(),
sizes.clone(),
owners.clone(),
native_popped.clone(),
);
let (guard_c, host_cx_c) = (guard_armed_sent.clone(), host_cx.clone());
nav_scope.on_cleanup(move || {
let alive = with_tree(|t| t.node_kind(host).is_some());
if alive && guard_c.replace(false) {
host_cx_c.vote_guard(false);
}
loop {
let e = entries_c.borrow_mut().pop();
let Some(e) = e else { break };
if alive {
if native_popped_c.get() > 0 {
native_popped_c.set(native_popped_c.get() - 1);
} else {
with_tree(|t| t.patch(host, Box::new(NavPatch::Popped), false));
}
sizes_c.borrow_mut().remove(&e.page);
with_tree(|t| t.remove_subtree(e.page));
owners_c.borrow_mut().pop();
}
}
if alive {
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
}
});
}
let p_push = path.clone();
let p_cur = path.clone();
let p_enter = path.clone();
let p_seg = path.clone();
register_route_surface(
move |k| {
if k.is_empty() {
let mut v = p_push.peek();
if v.is_empty() {
return false; }
v.clear();
p_push.write(v);
true
} else {
false
}
},
{
let (run_back, depth) = (run_back.clone(), depth.clone());
move |_| {
if depth() == 0 {
return false; }
run_back();
true
}
},
move || p_cur.peek().last().map(|k| k.key()).unwrap_or_default(),
move |k| {
let Some(parsed) = K::from_key(k) else {
return false; };
let mut v = p_enter.peek();
v.push(parsed);
p_enter.write(v);
true
},
move || p_seg.peek().iter().map(|k| k.key()).collect(),
);
ret_node
}
}
type CoverBackground<R> = Rc<dyn Fn(&R) -> day_spec::Color>;
pub struct Cover<S, R: Route> {
sheet: bool,
half: bool,
open: S,
build: Rc<dyn Fn(&R) -> AnyPiece>,
background: Option<CoverBackground<R>>,
routed: bool,
_marker: std::marker::PhantomData<R>,
}
pub fn cover<R: Route, S: Binding<Option<R>>, P: Piece>(
open: S,
build: impl Fn(&R) -> P + 'static,
) -> Cover<S, R> {
Cover {
sheet: false,
half: false,
open,
build: Rc::new(move |r| AnyPiece::new(build(r))),
background: None,
routed: true,
_marker: std::marker::PhantomData,
}
}
#[day_macros::builder(CoverBuilder<R>)]
impl<S: Binding<Option<R>>, R: Route> Cover<S, R> {
pub fn sheet(mut self) -> Self {
self.sheet = true;
self
}
pub fn half_sheet(mut self) -> Self {
self.sheet = true;
self.half = true;
self
}
pub fn background(mut self, f: impl Fn(&R) -> day_spec::Color + 'static) -> Self {
self.background = Some(Rc::new(f));
self
}
pub fn unrouted(mut self) -> Self {
self.routed = false;
self
}
}
impl<S: Binding<Option<R>>, R: Route> Piece for Cover<S, R> {
fn build(self, cx: &mut BuildCx) -> RNode {
use day_spec::props::{CoverPatch, CoverProps};
let Cover {
sheet,
half,
open,
build,
background,
routed,
..
} = self;
let composed_sheet = sheet
&& !half
&& day_core::capability(day_spec::Cap::CoverSheet) == day_spec::Support::Unsupported;
let window = day_core::window_being_built();
let size: Rc<RefCell<Option<Size>>> = Rc::default();
let node = cx.native(
kinds::COVER,
&CoverProps { sheet, half },
Rc::new(day_core::CoverLayout { size: size.clone() }),
Flex::default(),
Boundary::Yes,
);
struct Presented<R> {
key: R,
scope: Scope,
}
let current: Rc<RefCell<Option<Presented<R>>>> = Rc::default();
let closing: Rc<Cell<bool>> = Rc::default();
let owner_scope = Scope::current();
let layer = NavLayer {
order: Rc::new(Cell::new(0)),
depth: owner_scope.use_context::<NavLayer>().map_or(0, |l| l.depth),
};
let dispose_content = {
let current = current.clone();
move || {
let presented = current.borrow_mut().take();
while with_tree(|t| t.child_count(node)) > 0 {
match with_tree(|t| t.first_child(node)) {
Some(c) => with_tree(|t| t.remove_subtree(c)),
None => break,
}
}
if let Some(p) = presented {
p.scope.dispose();
}
}
};
let reconcile = {
let (current, closing, dispose_content) =
(current.clone(), closing.clone(), dispose_content.clone());
let layer = layer.clone();
move |want: &Option<R>| match want {
Some(r) => {
let already =
!closing.get() && current.borrow().as_ref().is_some_and(|p| p.key == *r);
if already {
return;
}
dispose_content();
closing.set(false);
layer.order.set(NEXT_COVER_LAYER.with(|next| {
let order = next.get();
next.set(order + 1);
order
}));
let scope = owner_scope.enter(Scope::child);
scope.provide(NavLayer {
order: layer.order.clone(),
depth: layer.depth + 1,
});
let card = composed_sheet
&& !day_core::window_size_class_untracked(window)
.is_some_and(|c| c.width == day_spec::WidthClass::Compact);
scope.enter(|| {
let content = (build)(r);
let mut c = BuildCx::new(node);
if card {
let _ = day_core::ComposedSheet { content }.build(&mut c);
} else {
let _ = content.build(&mut c);
}
});
*current.borrow_mut() = Some(Presented {
key: r.clone(),
scope,
});
let bg = if card {
Some(day_spec::Color::CLEAR)
} else {
background.as_ref().map(|f| f(r))
};
with_tree(|t| {
t.patch(
node,
Box::new(CoverPatch::Present {
background: bg,
dismiss_disabled: day_core::shield::dismiss_disabled(),
}),
false,
);
t.mark_needs_measure(node);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
None => {
layer.order.set(0);
if current.borrow().is_some() && !closing.get() {
closing.set(true);
with_tree(|t| t.patch(node, Box::new(CoverPatch::Dismiss), false));
}
}
}
};
{
let o = open.clone();
bind(move || o.read(), move |want: &Option<R>| reconcile(want));
}
{
let current = current.clone();
bind(
day_core::shield::dismiss_disabled,
move |disabled: &bool| {
if current.borrow().is_some() {
with_tree(|t| {
t.patch(
node,
Box::new(CoverPatch::DismissDisabled(*disabled)),
false,
)
});
}
},
);
}
{
let (o, size, closing, dispose_content) = (
open.clone(),
size.clone(),
closing.clone(),
dispose_content.clone(),
);
cx.on(node, move |ev| match ev {
Event::FrameChanged(sz) => {
if *size.borrow() != Some(*sz) {
*size.borrow_mut() = Some(*sz);
with_tree(|t| {
t.mark_needs_measure(node);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
Event::NavBack { .. } => {
if !day_core::shield::dismiss_disabled() && o.peek().is_some() {
o.write(None);
}
}
Event::CoverHidden if closing.get() => {
closing.set(false);
dispose_content();
}
_ => {}
});
}
if !routed {
return node;
}
let o_push = open.clone();
let o_pop = open.clone();
let o_cur = open.clone();
let o_enter = open.clone();
let o_seg = open;
let push = move |k: &str, sig: &S| match R::from_key(k) {
Some(r) => {
sig.write(Some(r));
true
}
None => false,
};
let push2 = push;
register_route_surface_in_layer(
layer,
move |k| push(k, &o_push),
move |_| {
if o_pop.peek().is_some() {
o_pop.write(None);
true
} else {
false
}
},
move || o_cur.peek().map(|r| r.key()).unwrap_or_default(),
move |k| push2(k, &o_enter),
move || o_seg.peek().map(|r| vec![r.key()]).unwrap_or_default(),
);
node
}
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::Piece;
use std::rc::Rc;
use day_core::conformance::{Case, Drive};
use day_core::{Boundary, Flex, RNode, piece_fn};
use day_reactive::Signal;
use day_spec::props::NavPresentation;
use day_spec::{Cap, Size, kinds};
use crate::*;
fn stack(path: Signal<Vec<String>>) -> impl Piece {
nav_stack(
path,
column((
label(move || format!("depth {}", path.get().len())).id(day_core::id!(depth)),
button("Open")
.action(move || path.update(|p| p.push("detail".into())))
.id(day_core::id!(open)),
)),
)
.title("Root")
.destination(|key: &String| {
label(format!("Page {key}")).id(day_core::id!(page, key.to_owned()))
})
}
#[day_macros::test(day_core)]
fn nav_stack_push_pop() -> Case {
Case::new()
.proves(kinds::NAV)
.proves(kinds::NAV_PAGE)
.page(|| stack(Signal::new(Vec::new())))
.drive(|d: Drive| async move {
d.assert_text("depth", "depth 0").await?;
d.tap("open").await?;
d.assert_text("page:detail", "Page detail").await?;
d.wait_idle().await?;
d.nav_back().await?;
d.assert_missing("page:detail").await?;
d.assert_text("depth", "depth 0").await
})
}
#[day_macros::test(day_core)]
fn nav_stack_follows_path() -> Case {
Case::new()
.proves(kinds::NAV)
.page(|| stack(Signal::new(vec!["first".into(), "second".into()])))
.drive(|d: Drive| async move {
d.assert_text("page:second", "Page second").await?;
d.wait_idle().await?;
d.nav_back().await?;
d.assert_missing("page:second").await?;
d.assert_text("page:first", "Page first").await?;
d.wait_idle().await?;
d.nav_back().await?;
d.assert_missing("page:first").await?;
d.assert_text("depth", "depth 0").await
})
}
#[day_macros::test(day_core)]
fn nav_sidebar_selects() -> Case {
Case::new()
.proves(kinds::NAV)
.proves(kinds::NAV_MENU)
.page(|| {
nav(Signal::new("alpha".to_string()))
.item("alpha", "Alpha", || {
label("Page Alpha").id(day_core::id!(page_alpha))
})
.item("beta", "Beta", || {
label("Page Beta").id(day_core::id!(page_beta))
})
})
.drive(|d: Drive| async move {
d.navigate("beta").await?;
d.assert_text("page_beta", "Page Beta").await?;
d.assert_route("beta").await?;
d.wait_idle().await?;
d.navigate("alpha").await?;
d.assert_text("page_alpha", "Page Alpha").await
})
}
#[day_macros::test(day_core)]
fn nav_tabs_switch() -> Case {
Case::new()
.proves(kinds::NAV)
.page(|| {
nav(Signal::new("alpha".to_string()))
.presentation(NavPresentation::Tabs)
.item("alpha", "Alpha", || {
label("Tab Alpha").id(day_core::id!(tab_alpha))
})
.item("beta", "Beta", || {
label("Tab Beta").id(day_core::id!(tab_beta))
})
})
.drive(|d: Drive| async move {
d.assert_text("tab_alpha", "Tab Alpha").await?;
d.navigate("beta").await?;
d.assert_text("tab_beta", "Tab Beta").await?;
d.assert_route("beta").await
})
}
fn covered(open: Signal<Option<String>>, sheet: bool) -> impl Piece {
let c = cover(open, move |key: &String| {
column((
label(format!("Cover {key}")).id(day_core::id!(cover_text)),
button("Close")
.action(move || open.set(None))
.id(day_core::id!(close)),
))
});
column((
label(move || format!("open {}", open.get().unwrap_or_default()))
.id(day_core::id!(state)),
button("Show")
.action(move || open.set(Some("panel".into())))
.id(day_core::id!(show)),
if sheet { c.sheet().any() } else { c.any() },
))
}
#[day_macros::test(day_core)]
fn cover_presents_and_dismisses() -> Case {
Case::new()
.proves_cap(Cap::Cover)
.proves(kinds::COVER)
.page(|| covered(Signal::new(None), false))
.drive(|d: Drive| async move {
d.assert_missing("cover_text").await?;
d.tap("show").await?;
d.assert_text("cover_text", "Cover panel").await?;
d.wait_idle().await?;
d.tap("close").await?;
d.assert_missing("cover_text").await?;
d.assert_text("state", "open ").await
})
}
#[day_macros::test(day_core)]
fn cover_sheet_back_dismisses() -> Case {
Case::new()
.proves(kinds::COVER)
.page(|| covered(Signal::new(None), true))
.drive(|d: Drive| async move {
d.tap("show").await?;
d.assert_text("cover_text", "Cover panel").await?;
d.assert_text("state", "open panel").await?;
d.wait_idle().await?;
d.nav_back().await?;
d.assert_missing("cover_text").await?;
d.assert_text("state", "open ").await
})
}
pub(crate) struct HeightProbe(pub(crate) Rc<std::cell::Cell<f64>>);
impl day_core::Layout for HeightProbe {
fn measure(
&self,
_cx: &mut dyn day_core::LayoutOps,
_children: &[RNode],
p: day_geometry::Proposal,
) -> Size {
Size::new(p.width.unwrap_or(0.0), p.height.unwrap_or(0.0))
}
fn place(
&self,
_cx: &mut dyn day_core::LayoutOps,
_children: &[RNode],
bounds: day_geometry::Rect,
) {
self.0.set(bounds.size.height);
}
}
pub(crate) fn height_probe(into: Rc<std::cell::Cell<f64>>) -> impl Piece {
piece_fn(move |cx| {
cx.layout_only(
Rc::new(HeightProbe(into)),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::No,
)
})
}
thread_local! {
static HALF_HEIGHTS: (Rc<std::cell::Cell<f64>>, Rc<std::cell::Cell<f64>>) =
(Rc::default(), Rc::default());
}
fn half_covered(open: Signal<Option<String>>) -> impl Piece {
let (page_h, cover_h) = HALF_HEIGHTS.with(|h| (h.0.clone(), h.1.clone()));
page_h.set(0.0);
cover_h.set(0.0);
let sheet = cover(open, move |key: &String| {
column((
label(format!("Cover {key}")).id(day_core::id!(cover_text)),
height_probe(cover_h.clone()),
button("Close")
.action(move || open.set(None))
.id(day_core::id!(close)),
))
})
.half_sheet();
zstack((
height_probe(page_h),
column((
label(move || format!("open {}", open.get().unwrap_or_default()))
.id(day_core::id!(state)),
button("Show")
.action(move || open.set(Some("panel".into())))
.id(day_core::id!(show)),
sheet,
)),
))
}
pub(crate) fn is_half(page_h: f64, probe_h: f64) -> bool {
page_h > 0.0 && probe_h > page_h * 0.15 && probe_h <= page_h * 0.6
}
pub(crate) async fn check_half(
d: &Drive,
what: &str,
heights: &(Rc<std::cell::Cell<f64>>, Rc<std::cell::Cell<f64>>),
) -> day_core::conformance::TestResult {
let mut got = (0.0, 0.0);
for _ in 0..40 {
got = (heights.0.get(), heights.1.get());
if is_half(got.0, got.1) {
break;
}
d.pause(0.05).await?;
d.wait_idle().await?;
}
d.check(
is_half(got.0, got.1),
&format!("{what} {} of page {}: between 15% and 60%", got.1, got.0),
)
}
#[day_macros::test(day_core)]
fn cover_half_sheet_presents_over_content() -> Case {
Case::new()
.proves(kinds::COVER)
.page(|| half_covered(Signal::new(None)))
.drive(|d: Drive| async move {
d.assert_missing("cover_text").await?;
d.tap("show").await?;
d.assert_text("cover_text", "Cover panel").await?;
d.wait_idle().await?;
let heights = HALF_HEIGHTS.with(|h| h.clone());
check_half(&d, "half sheet content", &heights).await?;
d.assert_text("state", "open panel").await?;
d.tap("close").await?;
d.assert_missing("cover_text").await?;
d.assert_text("state", "open ").await
})
}
#[day_macros::test(day_core)]
fn cover_half_sheet_back_dismisses() -> Case {
Case::new()
.proves(kinds::COVER)
.page(|| half_covered(Signal::new(None)))
.drive(|d: Drive| async move {
d.tap("show").await?;
d.assert_text("cover_text", "Cover panel").await?;
d.wait_idle().await?;
d.nav_back().await?;
d.assert_missing("cover_text").await?;
d.assert_text("state", "open ").await
})
}
thread_local! {
static SHEET_HEIGHTS: (Rc<std::cell::Cell<f64>>, Rc<std::cell::Cell<f64>>) =
(Rc::default(), Rc::default());
}
#[day_macros::test(day_core)]
fn cover_sheet_composes_a_card_on_wide() -> Case {
Case::new()
.proves(kinds::COVER)
.requires_support("composed sheet", || {
if day_core::capability(Cap::CoverSheet) == day_spec::Support::Unsupported {
day_spec::Support::Native
} else {
day_spec::Support::Unsupported
}
})
.page(|| {
let (page_h, sheet_h) = SHEET_HEIGHTS.with(|h| (h.0.clone(), h.1.clone()));
page_h.set(0.0);
sheet_h.set(0.0);
let open = Signal::new(None::<String>);
let sheet = cover(open, move |key: &String| {
column((
label(format!("Cover {key}")).id(day_core::id!(cover_text)),
height_probe(sheet_h.clone()),
button("Close")
.action(move || open.set(None))
.id(day_core::id!(close)),
))
})
.sheet()
.unrouted();
zstack((
height_probe(page_h),
column((
label(move || format!("open {}", open.get().unwrap_or_default()))
.id(day_core::id!(state)),
button("Show")
.action(move || open.set(Some("panel".into())))
.id(day_core::id!(show)),
sheet,
)),
))
})
.drive(|d: Drive| async move {
let _restore = crate::inspector::conformance::RestoreSizeClass;
day_core::override_size_class(day_spec::SizeClass {
width: day_spec::WidthClass::Expanded,
height: day_spec::HeightClass::Expanded,
});
d.wait_idle().await?;
d.tap("show").await?;
d.assert_text("cover_text", "Cover panel").await?;
let heights = SHEET_HEIGHTS.with(|h| h.clone());
let mut got = (0.0, 0.0);
for _ in 0..40 {
got = (heights.0.get(), heights.1.get());
if got.0 > 0.0 && got.1 > 0.0 {
break;
}
d.pause(0.05).await?;
d.wait_idle().await?;
}
d.check(
got.0 > 0.0 && got.1 > 0.0 && got.1 < got.0,
&format!("sheet content {} under a page {} tall", got.1, got.0),
)?;
d.assert_text("state", "open panel").await?;
d.tap("close").await?;
d.assert_missing("cover_text").await?;
d.assert_text("state", "open ").await
})
}
}