use std::cell::RefCell;
use std::rc::Rc;
use day_reactive::{AnyBinding, Binding, Scope, bind, bind_seeded};
use day_spec::{Icon, Symbol, ToolbarItem, ToolbarItemKind, ToolbarPatch, ToolbarValue};
use crate::{IntoReactive, IntoText, MenuEntry, Reactive, TextSource};
#[derive(Clone)]
pub struct ToolbarEntry {
id: String,
kind: Kind,
label: Option<TextSource>,
tooltip: Option<TextSource>,
icon: Option<Icon>,
enabled: Reactive<bool>,
action: Option<Rc<dyn Fn()>>,
placement: day_spec::ToolbarPlacement,
label_style: day_spec::LabelStyle,
prominent: bool,
}
#[derive(Clone)]
enum Kind {
Button,
Segmented(Vec<Segment>, AnyBinding<usize>),
Toggle(AnyBinding<bool>),
CommandToggle(Rc<dyn Fn() -> bool>),
Menu(Vec<MenuEntry>),
Label,
Separator,
}
fn entry(id: day_core::ElementId, kind: Kind) -> ToolbarEntry {
ToolbarEntry {
id: id.as_str().to_owned(),
kind,
label: None,
tooltip: None,
icon: None,
enabled: Reactive::Const(true),
action: None,
placement: day_spec::ToolbarPlacement::Automatic,
label_style: day_spec::LabelStyle::Automatic,
prominent: false,
}
}
pub fn toolbar_button<M>(id: day_core::ElementId, label: impl IntoText<M>) -> ToolbarEntry {
ToolbarEntry {
label: Some(label.into_text()),
..entry(id, Kind::Button)
}
}
pub fn toolbar_segmented(
id: day_core::ElementId,
segments: Vec<Segment>,
selected: impl Binding<usize>,
) -> ToolbarEntry {
ToolbarEntry {
label: None,
..entry(id, Kind::Segmented(segments, AnyBinding::new(selected)))
}
}
#[derive(Clone)]
pub struct Segment {
label: TextSource,
icon: Option<day_spec::Icon>,
}
pub fn segment<M>(label: impl IntoText<M>) -> Segment {
Segment {
label: label.into_text(),
icon: None,
}
}
impl Segment {
pub fn icon(mut self, s: day_spec::Symbol) -> Segment {
self.icon = Some(day_spec::Icon::Symbol(s));
self
}
pub fn image(mut self, name: impl Into<String>) -> Segment {
self.icon = Some(day_spec::Icon::Image(name.into()));
self
}
}
pub fn toolbar_toggle<M>(
id: day_core::ElementId,
label: impl IntoText<M>,
on: impl Binding<bool>,
) -> ToolbarEntry {
ToolbarEntry {
label: Some(label.into_text()),
..entry(id, Kind::Toggle(AnyBinding::new(on)))
}
}
pub fn toolbar_menu<M>(
id: day_core::ElementId,
label: impl IntoText<M>,
items: Vec<MenuEntry>,
) -> ToolbarEntry {
ToolbarEntry {
label: Some(label.into_text()),
..entry(id, Kind::Menu(items))
}
}
pub fn toolbar_label<M>(id: day_core::ElementId, text: impl IntoText<M>) -> ToolbarEntry {
ToolbarEntry {
label: Some(text.into_text()),
..entry(id, Kind::Label)
}
}
pub fn toolbar_separator(id: day_core::ElementId) -> ToolbarEntry {
entry(id, Kind::Separator)
}
impl ToolbarEntry {
pub(crate) fn command_checked(mut self, checked: Rc<dyn Fn() -> bool>) -> Self {
self.kind = Kind::CommandToggle(checked);
self
}
pub fn id(mut self, id: day_core::ElementId) -> Self {
self.id = id.as_str().to_owned();
self
}
pub fn action(mut self, f: impl Fn() + 'static) -> ToolbarEntry {
self.action = Some(Rc::new(f));
self
}
pub fn icon(mut self, symbol: Symbol) -> ToolbarEntry {
self.icon = Some(Icon::Symbol(symbol));
self
}
pub fn image(mut self, name: impl Into<day_spec::ImageName>) -> ToolbarEntry {
self.icon = Some(Icon::Image(name.into().as_str().to_string()));
self
}
pub fn tooltip<M>(mut self, t: impl IntoText<M>) -> ToolbarEntry {
self.tooltip = Some(t.into_text());
self
}
pub fn enabled<M>(mut self, on: impl IntoReactive<bool, M>) -> ToolbarEntry {
self.enabled = on.into_reactive();
self
}
pub fn placement(mut self, placement: day_spec::ToolbarPlacement) -> ToolbarEntry {
self.placement = placement;
self
}
pub fn label_style(mut self, style: day_spec::LabelStyle) -> ToolbarEntry {
self.label_style = style;
self
}
pub fn prominent(mut self) -> ToolbarEntry {
self.prominent = true;
self
}
}
#[derive(Clone)]
pub enum ToolbarSource {
Fixed(Vec<ToolbarEntry>),
Derived(Rc<dyn Fn() -> Vec<ToolbarEntry>>),
}
pub trait ToolbarContent<M> {
fn into_source(self) -> ToolbarSource;
}
pub struct OneMark;
pub struct ManyMark;
pub struct DerivedMark;
impl ToolbarContent<OneMark> for ToolbarEntry {
fn into_source(self) -> ToolbarSource {
ToolbarSource::Fixed(vec![self])
}
}
impl ToolbarContent<ManyMark> for Vec<ToolbarEntry> {
fn into_source(self) -> ToolbarSource {
ToolbarSource::Fixed(self)
}
}
impl<const N: usize> ToolbarContent<ManyMark> for [ToolbarEntry; N] {
fn into_source(self) -> ToolbarSource {
ToolbarSource::Fixed(Vec::from(self))
}
}
impl<F: Fn() -> Vec<ToolbarEntry> + 'static> ToolbarContent<DerivedMark> for F {
fn into_source(self) -> ToolbarSource {
ToolbarSource::Derived(Rc::new(self))
}
}
pub fn sidebar_toggle_item(host: day_core::RNode) -> ToolbarEntry {
toolbar_button(
day_core::id!(day::sidebar_toggle),
day_localization::t(day_localization::key::day_toggle_sidebar),
)
.icon(Symbol::Sidebar)
.placement(day_spec::ToolbarPlacement::Navigation)
.action(move || {
day_core::toggle_sidebar(host);
})
}
pub fn contribute(chrome: day_core::Chrome, content: ToolbarSource) {
let active = day_core::current_page_gate();
let gate = active.clone();
let column = day_core::current_page_column();
let token = match content {
ToolbarSource::Fixed(entries) => {
let texts: Vec<(Option<TextSource>, Option<TextSource>)> = entries
.iter()
.map(|e| (e.label.clone(), e.tooltip.clone()))
.collect();
let token = day_core::register_contribution_gated(
chrome,
lower(entries, chrome, column),
active,
);
let live = |t: &Option<TextSource>| matches!(t, Some(TextSource::Dyn(_)));
if texts.iter().any(|(l, t)| live(l) || live(t)) {
let resolve = move || -> Vec<(String, Option<String>)> {
texts
.iter()
.map(|(l, t)| {
(
l.as_ref().map(TextSource::resolve).unwrap_or_default(),
t.as_ref().map(TextSource::resolve),
)
})
.collect()
};
let seed = day_reactive::untrack(&resolve);
bind_seeded(seed, resolve, move |now: &Vec<(String, Option<String>)>| {
day_core::relabel_contribution(token, now)
});
}
token
}
ToolbarSource::Derived(builder) => {
let token = day_core::register_contribution_gated(chrome, Vec::new(), active);
let pass: Rc<RefCell<Option<Scope>>> = Rc::new(RefCell::new(None));
let outer = Scope::child();
outer.enter(|| {
day_reactive::Effect::new(move || {
let _ = day_localization::locale().get();
let next = outer.create_child();
let items = next.enter(|| lower(builder(), chrome, column));
if let Some(old) = pass.borrow_mut().replace(next) {
old.dispose();
}
day_core::update_contribution(token, items);
});
});
token
}
};
if let Some(gate) = gate {
day_reactive::Effect::new(move || {
let _ = gate();
day_core::chrome_changed();
});
}
Scope::current().on_cleanup(move || day_core::unregister_contribution(token));
}
fn lower(
entries: Vec<ToolbarEntry>,
chrome: day_core::Chrome,
column: day_spec::ToolbarColumn,
) -> Vec<ToolbarItem> {
entries
.into_iter()
.map(|e| {
let ToolbarEntry {
id,
kind,
label,
tooltip,
icon,
enabled,
action,
placement,
label_style,
prominent,
} = e;
let label = label.map(|t| t.initial()).unwrap_or_default();
let extra = action;
let (kind, action_id) = match kind {
Kind::Button => (
ToolbarItemKind::Button,
extra
.clone()
.map(day_core::register_menu_action)
.unwrap_or(0),
),
Kind::Menu(items) => (
ToolbarItemKind::Menu {
items: crate::lower_menu(items),
},
extra
.clone()
.map(day_core::register_menu_action)
.unwrap_or(0),
),
Kind::Toggle(on) => {
let seed = on.get_untracked();
let item = id.clone();
let extra = extra.clone();
let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
if let ToolbarValue::On(next) = v {
on.set(*next);
if let Some(f) = &extra {
f();
}
}
}));
bind_seeded(
seed,
move || on.get(),
move |v: &bool| {
day_core::patch_chrome(
chrome,
ToolbarPatch::On {
item: item.clone(),
on: *v,
},
);
},
);
(ToolbarItemKind::Toggle { on: seed }, act)
}
Kind::CommandToggle(read) => {
let seed = day_reactive::untrack(|| read());
let item = id.clone();
let after = read.clone();
let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
if let ToolbarValue::On(wanted) = v {
if *wanted != day_reactive::untrack(|| after())
&& let Some(f) = &extra
{
f();
}
day_core::patch_chrome(
chrome,
ToolbarPatch::On {
item: item.clone(),
on: day_reactive::untrack(|| after()),
},
);
}
}));
let item = id.clone();
bind_seeded(
seed,
move || read(),
move |on: &bool| {
day_core::patch_chrome(
chrome,
ToolbarPatch::On {
item: item.clone(),
on: *on,
},
);
},
);
(ToolbarItemKind::Toggle { on: seed }, act)
}
Kind::Segmented(segments, sel) => {
let seed = sel.get_untracked().min(segments.len().saturating_sub(1));
let item = id.clone();
let extra = extra.clone();
let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
if let ToolbarValue::Selected(next) = v {
sel.set(*next);
if let Some(f) = &extra {
f();
}
}
}));
bind_seeded(
seed,
move || sel.get(),
move |v: &usize| {
day_core::patch_chrome(
chrome,
ToolbarPatch::Selected {
item: item.clone(),
index: *v,
},
);
},
);
(
ToolbarItemKind::Segmented {
segments: segments
.into_iter()
.map(|s| day_spec::ToolbarSegment {
title: s.label.initial(),
icon: s.icon,
})
.collect(),
selected: seed,
},
act,
)
}
Kind::Label => (ToolbarItemKind::Label, 0),
Kind::Separator => (ToolbarItemKind::Separator, 0),
};
let enabled_now = enabled.get_untracked();
if let Reactive::Dyn(_) = &enabled {
let item = id.clone();
bind(
move || enabled.get(),
move |on: &bool| {
day_core::patch_chrome(
chrome,
ToolbarPatch::Enabled {
item: item.clone(),
on: *on,
},
);
},
);
}
let enabled = enabled_now;
ToolbarItem {
id,
kind,
label,
tooltip: tooltip.map(|t| t.initial()),
icon,
enabled,
action: action_id,
placement,
label_style,
prominent,
column,
}
})
.collect()
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::conformance::{Case, Drive};
use day_reactive::Signal;
use day_spec::Cap;
use crate::*;
#[day_macros::test(day_core)]
fn toolbar_button_runs_action() -> Case {
Case::new()
.proves_duty("edit_toolbar")
.proves_cap(Cap::Toolbar)
.proves_modifier("toolbar")
.requires(Cap::Toolbar)
.page(|| {
let count = Signal::new(0i64);
label(move || format!("count {}", count.get()))
.id(day_core::id!(count))
.toolbar([toolbar_button(day_core::id!(bump), "Bump")
.action(move || count.update(|n| *n += 1))])
})
.drive(|d: Drive| async move {
d.toolbar_press("bump").await?;
d.toolbar_press("bump").await?;
d.assert_text("count", "count 2").await
})
}
#[day_macros::test(day_core)]
fn toolbar_toggle_writes_signal() -> Case {
Case::new()
.proves_duty("update_toolbar")
.proves_modifier("toolbar")
.requires(Cap::Toolbar)
.page(|| {
let on = Signal::new(false);
label(move || format!("star {}", on.get()))
.id(day_core::id!(star))
.toolbar([toolbar_toggle(day_core::id!(star), "Star", on)])
})
.drive(|d: Drive| async move {
d.toolbar_toggle("star", true).await?;
d.assert_text("star", "star true").await?;
d.toolbar_toggle("star", false).await?;
d.assert_text("star", "star false").await
})
}
}