use std::cell::RefCell;
use std::rc::Rc;
use day_core::*;
use day_reactive::{bind, bind_seeded};
use day_spec::props::*;
use day_spec::{Event, Font, Proposal, Rect, Role, Size, kinds};
use crate::*;
#[derive(Clone, Debug, Default)]
pub struct TextBuilder {
text: String,
runs: Vec<day_spec::TextRun>,
base: Font,
}
impl TextBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn base(mut self, font: Font) -> Self {
self.base = font;
self
}
pub fn text(mut self, s: &str) -> Self {
self.text.push_str(s);
self
}
pub fn run(
mut self,
s: &str,
run: impl FnOnce(std::ops::Range<usize>) -> day_spec::TextRun,
) -> Self {
let start = self.text.len();
self.text.push_str(s);
self.runs.push(run(start..self.text.len()));
self
}
pub fn strong(self, s: &str) -> Self {
let base = self.base;
self.run(s, move |range| {
day_spec::TextRun::font(
range,
day_spec::FontSpec {
style: base,
weight: Some(day_spec::FontWeight::Bold),
..Default::default()
},
)
})
}
pub fn emphasis(self, s: &str) -> Self {
let base = self.base;
self.run(s, move |range| {
day_spec::TextRun::font(
range,
day_spec::FontSpec {
style: base,
italic: true,
..Default::default()
},
)
})
}
pub fn code(self, s: &str) -> Self {
let base = self.base;
self.run(s, move |range| {
day_spec::TextRun::font(
range,
day_spec::FontSpec {
style: base,
monospace: true,
..Default::default()
},
)
})
}
pub fn colored(self, s: &str, color: day_spec::Color) -> Self {
let base = self.base;
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base),
color: Some(color),
..day_spec::TextRun::default()
})
}
pub fn underline(self, s: &str) -> Self {
let base = self.base;
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base),
underline: day_spec::Underline::Single,
..day_spec::TextRun::default()
})
}
pub fn highlight(self, s: &str, color: day_spec::Color) -> Self {
let base = self.base;
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base),
background: Some(color),
color: Some(day_spec::props::ButtonStyleSpec::on_tint(color)),
..day_spec::TextRun::default()
})
}
pub fn sized(self, s: &str, scale: f64) -> Self {
let base = self.base;
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base).scaled(scale),
..day_spec::TextRun::default()
})
}
pub fn strikethrough(self, s: &str) -> Self {
let base = self.base;
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base),
strikethrough: true,
..day_spec::TextRun::default()
})
}
pub fn link(self, s: &str, target: &str) -> Self {
let base = self.base;
let target = target.to_string();
self.run(s, move |range| day_spec::TextRun {
range,
font: day_spec::FontSpec::from(base),
link: Some(target),
..day_spec::TextRun::default()
})
}
pub fn build(self) -> (String, Vec<day_spec::TextRun>) {
(self.text, self.runs)
}
}
pub struct Label {
pub(crate) text: TextSource,
pub(crate) font: Font,
pub(crate) font_scale: f64,
pub(crate) weight: Option<day_spec::FontWeight>,
pub(crate) italic: bool,
pub(crate) tabular: bool,
pub(crate) monospace: bool,
pub(crate) wraps: bool,
pub(crate) max_lines: u32,
pub(crate) color: Option<Reactive<day_spec::Color>>,
pub(crate) role: day_spec::props::TextRole,
pub(crate) runs: Vec<day_spec::TextRun>,
pub(crate) markdown: bool,
pub(crate) align: day_spec::props::TextAlign,
pub(crate) on_link: Option<LinkHandler>,
}
pub(crate) type LinkHandler = Rc<dyn Fn(&str)>;
pub fn label<M>(text: impl IntoText<M>) -> Label {
Label {
text: text.into_text(),
font: Font::Body,
font_scale: 1.0,
weight: None,
italic: false,
tabular: false,
monospace: false,
wraps: true,
max_lines: 0,
color: None,
role: Default::default(),
runs: Vec::new(),
markdown: false,
align: day_spec::props::TextAlign::Leading,
on_link: None,
}
}
#[day_macros::builder(LabelBuilder)]
impl Label {
pub fn single_line(mut self) -> Self {
self.wraps = false;
self
}
pub fn max_lines(mut self, lines: u32) -> Self {
self.max_lines = lines;
self
}
pub fn font(mut self, f: Font) -> Self {
self.font = f;
self
}
pub fn font_scale(mut self, scale: f64) -> Self {
self.font_scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
self
}
pub fn weight(mut self, w: day_spec::FontWeight) -> Self {
self.weight = Some(w);
self
}
pub fn bold(self) -> Self {
self.weight(day_spec::FontWeight::Bold)
}
pub fn secondary(mut self) -> Self {
self.role = day_spec::props::TextRole::Secondary;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn tabular(mut self) -> Self {
self.tabular = true;
self
}
pub fn monospace(mut self) -> Self {
self.monospace = true;
self
}
pub fn runs(mut self, runs: Vec<day_spec::TextRun>) -> Self {
self.runs = runs;
self
}
pub fn runs_from(mut self, b: TextBuilder) -> Self {
let (text, runs) = b.build();
self.text = TextSource::Static(text);
self.runs = runs;
self
}
pub fn align(mut self, align: day_spec::props::TextAlign) -> Self {
self.align = align;
self
}
pub fn markdown(mut self) -> Self {
self.markdown = true;
self
}
pub fn on_link(mut self, f: impl Fn(&str) + 'static) -> Self {
self.on_link = Some(Rc::new(f));
self
}
pub fn color<M>(mut self, c: impl IntoReactive<day_spec::Color, M>) -> Self {
self.color = Some(c.into_reactive());
self
}
pub fn preset(mut self, preset: &crate::TextPreset) -> Self {
self.font = preset.font;
self.font_scale = preset.font_scale;
if preset.weight.is_some() {
self.weight = preset.weight;
}
self.italic |= preset.italic;
self.tabular |= preset.tabular;
self.monospace |= preset.monospace;
if preset.secondary {
self.role = day_spec::props::TextRole::Secondary;
}
if preset.single_line {
self.wraps = false;
}
if let Some(c) = preset.color {
self.color = Some(Reactive::Const(c));
}
self
}
}
impl Piece for Label {
fn build(self, cx: &mut BuildCx) -> RNode {
let (initial, runs) = if self.markdown {
crate::markdown::parse(&self.text.initial(), self.font)
} else {
(self.text.initial(), self.runs.clone())
};
let runs = match day_spec::runs_are_valid(&initial, &runs) {
Ok(()) => runs,
Err(why) => {
log::warn!("label runs ignored — {why}; the text renders unstyled");
Vec::new()
}
};
let node = cx.leaf(
kinds::LABEL,
&LabelProps {
align: self.align,
text: initial,
font: day_spec::FontSpec {
style: self.font,
scale: self.font_scale,
weight: self.weight,
italic: self.italic,
tabular: self.tabular,
monospace: self.monospace,
},
color: self.color.as_ref().map(|c| c.get_untracked()),
role: self.role,
wraps: self.wraps,
max_lines: self.max_lines,
runs,
},
Flex::default(),
);
let could_link =
self.markdown || self.on_link.is_some() || self.runs.iter().any(|r| r.link.is_some());
if could_link {
let handler = self.on_link.clone();
cx.on(node, move |ev| {
if let Event::LinkActivated(url) = ev {
match &handler {
Some(f) => f(url),
None => open_link(url),
}
}
});
}
let font = self.font;
let md = self.markdown;
self.text.bind_to(
node,
move |t| {
if md {
let (text, runs) = crate::markdown::parse(&t, font);
Box::new(LabelPatch::Runs(text, runs))
} else {
Box::new(LabelPatch::Text(t))
}
},
true,
);
if let Some(Reactive::Dyn(f)) = self.color {
bind(
move || f(),
move |c: &day_spec::Color| {
with_tree(|t| t.patch(node, Box::new(LabelPatch::Color(Some(*c))), false));
},
);
}
node
}
}
const LINK_BLUE: day_spec::Color = day_spec::Color::rgb(0.0, 0.478, 1.0);
pub fn open_link(target: &str) {
if let Some(route) = target.strip_prefix('#') {
let _ = day_core::navigate(route);
} else {
day_core::open_url(target);
}
}
pub struct Link {
label: Label,
url: String,
}
pub fn link<M>(text: impl IntoText<M>, url: impl Into<String>) -> Link {
Link {
label: label(text),
url: url.into(),
}
}
#[day_macros::builder(LinkBuilder)]
impl Link {
pub fn font(mut self, f: Font) -> Self {
self.label = self.label.font(f);
self
}
pub fn color(mut self, c: day_spec::Color) -> Self {
self.label = self.label.color(c);
self
}
pub fn bold(mut self) -> Self {
self.label = self.label.bold();
self
}
}
impl Piece for Link {
fn build(self, cx: &mut BuildCx) -> RNode {
let url = self.url;
let mut label = self.label;
if day_core::capability(day_spec::Cap::TextLinks) == day_spec::Support::Native {
let source = label.text.clone();
let font = day_spec::FontSpec {
style: label.font,
scale: label.font_scale,
weight: label.weight,
italic: label.italic,
..Default::default()
};
let color = label.color.as_ref().map(|c| c.get_untracked());
let target = url.clone();
let runs = move |text: &str| {
vec![day_spec::TextRun {
range: 0..text.len(),
font,
color,
link: Some(target.clone()),
..Default::default()
}]
};
let initial = source.initial();
label.runs = runs(&initial);
label.text = TextSource::Static(initial);
let node = label.build(cx);
source.bind_to(
node,
move |text| {
let spans = runs(&text);
Box::new(LabelPatch::Runs(text, spans))
},
true,
);
cx.on(node, move |ev| {
if matches!(ev, Event::Tap(_)) {
open_link(&url);
}
});
node
} else {
if label.color.is_none() {
label = label.color(LINK_BLUE);
}
label
.on_tap(move || open_link(&url))
.a11y(|b| b.role(Role::Button))
.build(cx)
}
}
}
pub struct Button {
title: TextSource,
icon: Reactive<Option<day_spec::Icon>>,
icon_only: bool,
action: Option<Rc<dyn Fn()>>,
native_style: day_spec::props::ButtonStyleSpec,
tint: Option<Reactive<day_spec::Color>>,
enabled: Reactive<bool>,
selected: Reactive<bool>,
}
pub fn button<M>(title: impl IntoText<M>) -> Button {
Button {
title: title.into_text(),
icon: Reactive::Const(None),
icon_only: false,
action: None,
native_style: day_spec::props::ButtonStyleSpec::Automatic,
tint: None,
enabled: true.into_reactive(),
selected: false.into_reactive(),
}
}
#[day_macros::builder(ButtonBuilder)]
impl Button {
pub fn icon<M>(mut self, symbol: impl IntoReactive<day_spec::Symbol, M>) -> Self {
let symbol = symbol.into_reactive();
self.icon = match symbol {
Reactive::Const(s) => Reactive::Const(Some(day_spec::Icon::Symbol(s))),
Reactive::Dyn(f) => Reactive::Dyn(Rc::new(move || Some(day_spec::Icon::Symbol(f())))),
};
self
}
pub fn image(mut self, name: impl Into<day_spec::ImageName>) -> Self {
self.icon = Reactive::Const(Some(day_spec::Icon::Image(name.into().as_str().to_owned())));
self
}
pub fn icon_only(mut self) -> Self {
self.icon_only = true;
self
}
pub fn action(mut self, f: impl Fn() + 'static) -> Self {
self.action = Some(Rc::new(f));
self
}
pub fn bordered(mut self) -> Self {
self.native_style = day_spec::props::ButtonStyleSpec::Bordered;
self
}
pub fn enabled<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.enabled = v.into_reactive();
self
}
pub fn prominent(mut self) -> Self {
self.native_style = day_spec::props::ButtonStyleSpec::Prominent;
self
}
pub fn tint<M>(mut self, color: impl IntoReactive<day_spec::Color, M>) -> Self {
self.tint = Some(color.into_reactive());
self
}
pub fn compact(mut self) -> Self {
self.native_style = day_spec::props::ButtonStyleSpec::Compact;
self
}
pub fn plain(mut self) -> Self {
self.native_style = day_spec::props::ButtonStyleSpec::Plain;
self
}
pub fn selected<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.selected = v.into_reactive();
self
}
}
impl Piece for Button {
fn build(self, cx: &mut BuildCx) -> RNode {
let initial = self.title.initial();
let style = match &self.tint {
Some(c) => day_spec::props::ButtonStyleSpec::Tinted(c.get_untracked()),
None => self.native_style,
};
let node = cx.leaf(
kinds::BUTTON,
&ButtonProps {
title: initial.clone(),
icon: self.icon.get_untracked(),
icon_only: self.icon_only,
enabled: self.enabled.get_untracked(),
style,
selected: self.selected.get_untracked(),
},
Flex::default(),
);
if let selected @ Reactive::Dyn(_) = self.selected {
bind(
move || selected.get(),
move |on: &bool| {
with_tree(|t| t.patch(node, Box::new(ButtonPatch::Selected(*on)), false));
},
);
}
if let Some(c @ Reactive::Dyn(_)) = self.tint.clone() {
bind(
move || c.get(),
move |col: &day_spec::Color| {
with_tree(|t| {
t.patch(
node,
Box::new(ButtonPatch::Style(
day_spec::props::ButtonStyleSpec::Tinted(*col),
)),
false,
)
});
},
);
}
let enabled = self.enabled;
let enabled_gate = enabled.clone();
if let Reactive::Dyn(_) = &enabled {
bind(
move || enabled.get(),
move |e: &bool| {
with_tree(|t| t.patch(node, Box::new(ButtonPatch::Enabled(*e)), false));
},
);
}
if let Some(action) = self.action {
let gate = enabled_gate;
cx.on(node, move |ev| {
if matches!(ev, Event::Pressed) && gate.get() {
action();
}
});
}
if matches!(&self.icon, Reactive::Const(None)) {
self.title
.bind_to(node, |t| Box::new(ButtonPatch::Title(t)), true);
} else {
let icon = self.icon;
let icon_only = self.icon_only;
let seed = day_spec::props::ButtonContent {
title: initial,
icon: icon.get_untracked(),
icon_only,
};
let title = self.title;
bind_seeded(
seed,
move || day_spec::props::ButtonContent {
title: title.resolve(),
icon: icon.get(),
icon_only,
},
move |content: &day_spec::props::ButtonContent| {
with_tree(|t| {
t.patch(node, Box::new(ButtonPatch::Content(content.clone())), true)
});
},
);
}
node
}
}
pub struct Toggle<S: Binding<bool>> {
value: S,
enabled: Reactive<bool>,
}
pub fn toggle<S: Binding<bool>>(value: S) -> Toggle<S> {
Toggle {
value,
enabled: true.into_reactive(),
}
}
#[day_macros::builder(ToggleBuilder)]
impl<S: Binding<bool>> Toggle<S> {
pub fn enabled<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.enabled = v.into_reactive();
self
}
}
impl<S: Binding<bool>> Piece for Toggle<S> {
fn build(self, cx: &mut BuildCx) -> RNode {
let initial = self.value.peek();
let node = cx.leaf(
kinds::TOGGLE,
&ToggleProps {
on: initial,
enabled: self.enabled.get_untracked(),
},
Flex::default(),
);
let v = self.value.clone();
bind_seeded(
initial,
move || v.read(),
move |on: &bool| {
with_tree(|t| t.patch(node, Box::new(TogglePatch::On(*on)), false));
},
);
let enabled = self.enabled;
if let Reactive::Dyn(_) = &enabled {
bind(
move || enabled.get(),
move |e: &bool| {
with_tree(|t| t.patch(node, Box::new(TogglePatch::Enabled(*e)), false));
},
);
}
let v = self.value;
cx.on(node, move |ev| {
if let Event::ToggleChanged(on) = ev {
v.write(*on);
}
});
node
}
}
pub struct Slider<S: Binding<f64>> {
value: S,
min: f64,
max: f64,
step: Option<f64>,
enabled: Reactive<bool>,
}
pub fn slider<S: Binding<f64>>(value: S) -> Slider<S> {
Slider {
value,
min: 0.0,
max: 1.0,
step: None,
enabled: Reactive::Const(true),
}
}
#[day_macros::builder(SliderBuilder)]
impl<S: Binding<f64>> Slider<S> {
pub fn enabled<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.enabled = v.into_reactive();
self
}
pub fn range(mut self, r: std::ops::RangeInclusive<f64>) -> Self {
self.min = *r.start();
self.max = *r.end();
self
}
pub fn step(mut self, s: f64) -> Self {
self.step = Some(s);
self
}
}
impl<S: Binding<f64>> Piece for Slider<S> {
fn build(self, cx: &mut BuildCx) -> RNode {
let initial = self.value.peek();
let node = cx.leaf(
kinds::SLIDER,
&SliderProps {
value: initial,
min: self.min,
max: self.max,
step: self.step,
enabled: self.enabled.get_untracked(),
},
Flex {
grow_w: true,
..Default::default()
},
);
let enabled = self.enabled;
if let Reactive::Dyn(_) = &enabled {
bind(
move || enabled.get(),
move |e: &bool| {
with_tree(|t| t.patch(node, Box::new(SliderPatch::Enabled(*e)), false));
},
);
}
let v = self.value.clone();
bind_seeded(
initial,
move || v.read(),
move |val: &f64| {
with_tree(|t| t.patch(node, Box::new(SliderPatch::Value(*val)), false));
},
);
let v = self.value;
let (step, min, max) = (self.step, self.min, self.max);
cx.on(node, move |ev| {
let snap = |val: f64| match step {
Some(s) if s > 0.0 => (min + ((val - min) / s).round() * s).clamp(min, max),
_ => val,
};
match ev {
Event::ValueChanged(val) => v.write_preview(snap(*val)),
Event::ValueCommitted(val) => v.write_commit(snap(*val)),
_ => {}
}
});
node
}
}
pub struct TextField<S: Binding<String>> {
value: S,
placeholder: Option<TextSource>,
on_submit: Option<Rc<dyn Fn()>>,
secure: Reactive<bool>,
read_only: Reactive<bool>,
purpose: day_spec::InputPurpose,
submit_label: day_spec::SubmitLabel,
max_length: Option<u32>,
enabled: Reactive<bool>,
}
pub fn text_field<S: Binding<String>>(value: S) -> TextField<S> {
TextField {
value,
placeholder: None,
on_submit: None,
secure: Reactive::Const(false),
read_only: Reactive::Const(false),
purpose: day_spec::InputPurpose::Text,
submit_label: day_spec::SubmitLabel::Return,
max_length: None,
enabled: Reactive::Const(true),
}
}
pub fn secure_field<S: Binding<String>>(value: S) -> TextField<S> {
text_field(value)
.secure(true)
.input_purpose(day_spec::InputPurpose::Password)
}
#[day_macros::builder(TextFieldBuilder)]
impl<S: Binding<String>> TextField<S> {
pub fn enabled<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.enabled = v.into_reactive();
self
}
pub fn placeholder<M>(mut self, t: impl IntoText<M>) -> Self {
self.placeholder = Some(t.into_text());
self
}
pub fn on_submit(mut self, f: impl Fn() + 'static) -> Self {
self.on_submit = Some(Rc::new(f));
self
}
pub fn secure<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.secure = v.into_reactive();
self
}
pub fn read_only<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.read_only = v.into_reactive();
self
}
pub fn input_purpose(mut self, purpose: day_spec::InputPurpose) -> Self {
self.purpose = purpose;
self
}
pub fn submit_label(mut self, label: day_spec::SubmitLabel) -> Self {
self.submit_label = label;
self
}
pub fn max_length(mut self, characters: u32) -> Self {
self.max_length = Some(characters);
self
}
}
impl<S: Binding<String>> Piece for TextField<S> {
fn build(self, cx: &mut BuildCx) -> RNode {
let initial = self.value.peek();
let ph = self
.placeholder
.as_ref()
.map(|p| p.initial())
.unwrap_or_default();
let node = cx.leaf(
kinds::TEXT_FIELD,
&TextFieldProps {
text: initial.clone(),
placeholder: ph,
enabled: self.enabled.get_untracked(),
},
Flex {
grow_w: true,
..Default::default()
},
);
if let Reactive::Dyn(_) = &self.enabled {
let enabled = self.enabled.clone();
bind(
move || enabled.get(),
move |e: &bool| {
with_tree(|t| t.patch(node, Box::new(TextFieldPatch::Enabled(*e)), false));
},
);
}
let (secure, read_only) = (self.secure, self.read_only);
let (purpose, submit_label, max_length) =
(self.purpose, self.submit_label, self.max_length);
let traits = move |secure: bool, read_only: bool| day_spec::InputTraits {
secure,
read_only,
purpose,
submit_label,
max_length,
};
let initial_traits = traits(secure.get_untracked(), read_only.get_untracked());
let reactive_traits =
matches!(secure, Reactive::Dyn(_)) || matches!(read_only, Reactive::Dyn(_));
if initial_traits != day_spec::InputTraits::default() {
with_tree(|t| t.set_node_input_traits(node, &initial_traits));
}
if reactive_traits {
bind_seeded(
initial_traits,
move || traits(secure.get(), read_only.get()),
move |tr: &day_spec::InputTraits| {
with_tree(|t| t.set_node_input_traits(node, tr));
},
);
}
let guard: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let v = self.value.clone();
let g = guard.clone();
bind_seeded(
initial,
move || v.read(),
move |t: &String| {
let from_native = g.borrow_mut().take().as_deref() == Some(t.as_str());
with_tree(|tr| {
tr.patch(
node,
Box::new(TextFieldPatch::Text {
text: t.clone(),
from_native,
}),
false,
)
});
},
);
let v = self.value;
let submit = self.on_submit;
let last: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
{
let (v, last) = (v.clone(), last.clone());
day_reactive::Scope::current().on_cleanup(move || {
if let Some(t) = last.borrow_mut().take() {
v.write_teardown(t);
}
});
}
cx.on(node, move |ev| match ev {
Event::TextChanged(t) => {
let cut = max_length
.and_then(|max| t.char_indices().nth(max as usize))
.map(|(at, _)| t[..at].to_string());
let t = match cut {
Some(cut) => {
with_tree(|tr| {
tr.patch(
node,
Box::new(TextFieldPatch::Text {
text: cut.clone(),
from_native: false,
}),
false,
)
});
cut
}
None => t.clone(),
};
*guard.borrow_mut() = Some(t.clone());
*last.borrow_mut() = Some(t.clone());
v.write_preview(t);
}
Event::Submitted => {
if let Some(t) = last.borrow_mut().take() {
v.write_commit(t);
}
if let Some(f) = &submit {
f();
}
}
Event::FocusChanged(false) => {
if let Some(t) = last.borrow_mut().take() {
v.write_commit(t);
}
}
_ => {}
});
if let Some(p) = self.placeholder {
p.bind_to(node, |t| Box::new(TextFieldPatch::Placeholder(t)), false);
}
node
}
}
pub struct Progress {
value: Option<FractionSource>,
animating: crate::Reactive<bool>,
large: bool,
}
pub fn spinner() -> Progress {
Progress {
value: None,
animating: crate::Reactive::Const(true),
large: false,
}
}
pub fn progress<M>(fraction: impl IntoFraction<M>) -> Progress {
Progress {
value: Some(fraction.into_fraction()),
animating: crate::Reactive::Const(true),
large: false,
}
}
#[day_macros::builder(ProgressBuilder)]
impl Progress {
pub fn animating<M>(mut self, source: impl crate::IntoReactive<bool, M>) -> Self {
self.animating = source.into_reactive();
self
}
pub fn large(mut self) -> Self {
self.large = true;
self
}
}
impl Piece for Progress {
fn build(self, cx: &mut BuildCx) -> RNode {
let determinate = self.value.is_some();
let initial = self.value.as_ref().map(|f| f.initial());
let node = cx.leaf(
kinds::PROGRESS,
&ProgressProps {
value: initial,
animating: self.animating.get_untracked(),
large: self.large,
},
Flex {
grow_w: determinate,
..Default::default()
},
);
if !determinate && let crate::Reactive::Dyn(f) = self.animating {
day_reactive::bind_seeded(
f(),
move || f(),
move |on| {
day_core::with_tree(|t| {
t.patch(node, Box::new(ProgressPatch::Animating(*on)), false)
});
},
);
}
if let Some(src) = self.value {
src.bind_to(node);
}
node
}
}
pub struct Divider {
vertical: bool,
}
pub fn divider() -> Divider {
Divider { vertical: false }
}
#[day_macros::builder(DividerBuilder)]
impl Divider {
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
impl Piece for Divider {
fn build(self, cx: &mut BuildCx) -> RNode {
let props = DividerProps {
vertical: self.vertical,
};
if self.vertical {
cx.native(
kinds::DIVIDER,
&props,
Rc::new(VerticalLeaf),
Flex {
grow_h: true,
..Default::default()
},
Boundary::No,
)
} else {
cx.leaf(
kinds::DIVIDER,
&props,
Flex {
grow_w: true,
..Default::default()
},
)
}
}
}
struct VerticalLeaf;
impl Layout for VerticalLeaf {
fn measure(&self, cx: &mut dyn LayoutOps, _children: &[RNode], p: Proposal) -> Size {
let s = cx.measure_leaf(Proposal::new(p.height, p.width));
Size::new(s.height, s.width)
}
fn place(&self, _cx: &mut dyn LayoutOps, _children: &[RNode], _bounds: Rect) {}
}
pub struct Spacer;
pub fn spacer() -> Spacer {
Spacer
}
impl Piece for Spacer {
fn build(self, cx: &mut BuildCx) -> RNode {
cx.layout_only(
Rc::new(PassThrough),
Flex {
is_spacer: true,
..Default::default()
},
Boundary::No,
)
}
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::conformance::{Case, Drive, FrameExpect, NativeExpect};
use day_reactive::Signal;
use day_spec::{Cap, Font, Role, kinds};
use crate::*;
#[day_macros::test(day_core)]
fn button_press() -> Case {
let presses = Signal::new(0i64);
Case::new()
.proves(kinds::BUTTON)
.page(move || {
column((
button("Press")
.action(move || presses.update(|n| *n += 1))
.id(day_core::id!(press)),
label(move || presses.get().to_string()).id(day_core::id!(count)),
))
.spacing(8.0)
})
.shot("default")
.drive(|d: Drive| async move {
d.assert_text("press", "Press").await?;
d.tap("press").await?;
d.assert_text("count", "1").await?;
d.tap("press").await?;
d.assert_text("count", "2").await
})
}
#[day_macros::test(day_core)]
fn button_disabled() -> Case {
let presses = Signal::new(0i64);
let enabled = Signal::new(false);
Case::new()
.proves(kinds::BUTTON)
.proves_duty("set_enabled")
.page(move || {
column((
button("Off")
.enabled(move || enabled.get())
.action(move || presses.update(|n| *n += 1))
.id(day_core::id!(off)),
label(move || presses.get().to_string()).id(day_core::id!(count)),
button("Enable")
.action(move || enabled.set(true))
.id(day_core::id!(enable)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_enabled("off", false).await?;
d.assert_text("count", "0").await?;
d.tap("enable").await?;
d.assert_enabled("off", true).await?;
d.tap("off").await?;
d.assert_text("count", "1").await
})
}
#[day_macros::test(day_core)]
fn toggle_binding() -> Case {
let on = Signal::new(false);
Case::new()
.proves(kinds::TOGGLE)
.page(move || {
column((
toggle(on).id(day_core::id!(tgl_switch)),
label(move || if on.get() { "on" } else { "off" }.to_owned())
.id(day_core::id!(tgl_state)),
button("Set on")
.action(move || on.set(true))
.id(day_core::id!(tgl_set)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.toggle("tgl_switch", true).await?;
d.assert_text("tgl_state", "on").await?;
d.toggle("tgl_switch", false).await?;
d.assert_text("tgl_state", "off").await?;
d.tap("tgl_set").await?;
d.assert_on("tgl_switch", true).await
})
}
#[day_macros::test(day_core)]
fn text_field_binding() -> Case {
let name = Signal::new(String::new());
Case::new()
.proves(kinds::TEXT_FIELD)
.page(move || {
column((
text_field(name)
.placeholder("Name")
.id(day_core::id!(tf_field)),
label(move || name.get()).id(day_core::id!(tf_echo)),
button("Set")
.action(move || name.set("Ada".into()))
.id(day_core::id!(tf_set)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.input("tf_field", "Grace").await?;
d.assert_text("tf_echo", "Grace").await?;
d.tap("tf_set").await?;
d.assert_text("tf_field", "Ada").await
})
}
#[day_macros::test(day_core)]
fn text_field_secure() -> Case {
let password = Signal::new(String::new());
let shown = Signal::new(false);
Case::new()
.proves(kinds::TEXT_FIELD)
.proves_duty("set_input_traits")
.page(move || {
column((
secure_field(password)
.secure(move || !shown.get())
.placeholder("Password")
.id(day_core::id!(tfs_pass)),
toggle(shown).id(day_core::id!(tfs_show)),
label(move || password.get().chars().count().to_string())
.id(day_core::id!(tfs_len)),
))
.spacing(8.0)
})
.shot("masked")
.drive(|d: Drive| async move {
d.input("tfs_pass", "correct horse").await?;
d.assert_text("tfs_len", "13").await?;
d.focus("tfs_pass").await?;
d.toggle("tfs_show", true).await?;
d.wait_idle().await?;
d.shot("shown").await?;
d.assert_focused("tfs_pass", true).await?;
d.input("tfs_pass", "battery").await?;
d.assert_text("tfs_len", "7").await?;
d.toggle("tfs_show", false).await?;
d.wait_idle().await?;
d.assert_text("tfs_pass", "battery").await
})
}
#[day_macros::test(day_core)]
fn text_field_max_length() -> Case {
let pin = Signal::new(String::new());
Case::new()
.proves(kinds::TEXT_FIELD)
.page(move || {
column((
text_field(pin).max_length(4).id(day_core::id!(tfm_pin)),
label(move || pin.get()).id(day_core::id!(tfm_echo)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.input("tfm_pin", "123456").await?;
d.assert_text("tfm_echo", "1234").await?;
d.assert_text("tfm_pin", "1234").await
})
}
#[day_macros::test(day_core)]
fn slider_range() -> Case {
let value = Signal::new(40.0f64);
Case::new()
.proves(kinds::SLIDER)
.page(move || {
column((
slider(value)
.range(0.0..=100.0)
.step(10.0)
.id(day_core::id!(sl_slider)),
label(move || format!("{:.0}", value.get())).id(day_core::id!(sl_value)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.set_value("sl_slider", 72.0).await?;
d.assert_text("sl_value", "70").await?;
d.assert_value("sl_slider", 70.0).await
})
}
#[day_macros::test(day_core)]
fn label_heading() -> Case {
let text = Signal::new("Hello".to_owned());
Case::new()
.proves(kinds::LABEL)
.proves_duty("set_a11y")
.page(move || {
column((
label(move || text.get())
.font(Font::Headline)
.a11y(|a| a.role(Role::Heading(2)))
.id(day_core::id!(lbl_heading)),
button("Rename")
.action(move || text.set("Renamed".into()))
.id(day_core::id!(lbl_rename)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_text("lbl_heading", "Hello").await?;
d.tap("lbl_rename").await?;
d.assert_text("lbl_heading", "Renamed").await?;
d.a11y_audit(Some("lbl_heading")).await
})
}
#[day_macros::test(day_core)]
fn label_text() -> Case {
Case::new()
.proves(kinds::LABEL)
.page(|| label("Plain text").id(day_core::id!(text)))
.drive(|d: Drive| async move { d.assert_text("text", "Plain text").await })
}
#[day_macros::test(day_core)]
fn label_reactive() -> Case {
let count = Signal::new(0i64);
Case::new()
.proves(kinds::LABEL)
.proves_duty("update")
.proves_duty("read_native")
.page(move || {
column((
label(move || format!("Count {}", count.get())).id(day_core::id!(text)),
button("Add")
.action(move || count.update(|n| *n += 1))
.id(day_core::id!(add)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_text("text", "Count 0").await?;
d.tap("add").await?;
d.tap("add").await?;
d.assert_text("text", "Count 2").await
})
}
#[day_macros::test(day_core)]
fn label_runs() -> Case {
Case::new()
.proves(kinds::LABEL)
.proves_cap(Cap::TextRuns)
.requires(Cap::TextRuns)
.page(|| {
label("")
.runs_from(
TextBuilder::new()
.text("Plain, ")
.strong("strong")
.text(" and ")
.emphasis("emphasized"),
)
.id(day_core::id!(runs))
})
.shot("default")
.drive(|d: Drive| async move {
d.assert_text("runs", "Plain, strong and emphasized").await
})
}
#[day_macros::test(day_core)]
fn label_link_runs_reactive() -> Case {
let text = Signal::new("😀 [Alpha](#alpha) / [Beta](#beta)".to_string());
Case::new()
.proves(kinds::LABEL)
.requires(Cap::TextRuns)
.page(move || {
column((
label(text).markdown().id(day_core::id!(links)),
button("Replace")
.id(day_core::id!(replace))
.action(move || {
text.set("[東京](#tokyo) then [again](#again)".into());
}),
button("Plain")
.id(day_core::id!(plain))
.action(move || text.set("Plain 😀".into())),
button("Restore")
.id(day_core::id!(restore))
.action(move || text.set("[Restored](#restored)".into())),
))
})
.drive(|d: Drive| async move {
d.assert_text("links", "😀 Alpha / Beta").await?;
d.tap("replace").await?;
d.assert_text("links", "東京 then again").await?;
d.tap("plain").await?;
d.assert_text("links", "Plain 😀").await?;
d.tap("restore").await?;
d.assert_text("links", "Restored").await
})
}
#[day_macros::test(day_core)]
fn label_selectable() -> Case {
Case::new()
.proves(kinds::LABEL)
.proves_modifier("selectable")
.proves_duty("set_selectable")
.page(|| label("Copy me").selectable().id(day_core::id!(text)))
.drive(|d: Drive| async move { d.assert_text("text", "Copy me").await })
}
#[day_macros::test(day_core)]
fn button_title() -> Case {
let saved = Signal::new(false);
Case::new()
.proves(kinds::BUTTON)
.page(move || {
button(move || if saved.get() { "Saved" } else { "Save" }.to_owned())
.action(move || saved.set(true))
.id(day_core::id!(save))
})
.drive(|d: Drive| async move {
d.assert_text("save", "Save").await?;
d.tap("save").await?;
d.assert_text("save", "Saved").await
})
}
#[day_macros::test(day_core)]
fn button_a11y() -> Case {
Case::new()
.proves(kinds::BUTTON)
.proves_modifier("a11y")
.page(|| {
button("Send")
.a11y(|a| a.label("Send the message"))
.id(day_core::id!(send))
})
.drive(|d: Drive| async move { d.a11y_audit(Some("send")).await })
}
#[day_macros::test(day_core)]
fn button_a11y_survives_title_change() -> Case {
let playing = Signal::new(false);
Case::new()
.proves(kinds::BUTTON)
.proves_modifier("a11y")
.page(move || {
button(move || if playing.get() { "Pause" } else { "Play" }.to_owned())
.icon(day_spec::Symbol::Play)
.action(move || playing.set(true))
.a11y(|a| a.label("Playback"))
.id(day_core::id!(play))
})
.drive(|d: Drive| async move {
d.a11y_audit(Some("play")).await?;
d.tap("play").await?;
d.assert_text("play", "Pause").await?;
d.a11y_audit(Some("play")).await
})
}
#[day_macros::test(day_core)]
fn toggle_disabled() -> Case {
let on = Signal::new(false);
let enabled = Signal::new(false);
Case::new()
.proves(kinds::TOGGLE)
.page(move || {
column((
toggle(on)
.enabled(move || enabled.get())
.id(day_core::id!(switch)),
button("Enable")
.action(move || enabled.set(true))
.id(day_core::id!(enable)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_enabled("switch", false).await?;
d.tap("enable").await?;
d.assert_enabled("switch", true).await?;
d.toggle("switch", true).await?;
d.assert_on("switch", true).await
})
}
#[day_macros::test(day_core)]
fn toggle_disabled_constant() -> Case {
let on = Signal::new(true);
Case::new()
.proves(kinds::TOGGLE)
.page(move || toggle(on).enabled(false).id(day_core::id!(switch)))
.drive(|d: Drive| async move {
d.assert_enabled("switch", false).await?;
d.assert_on("switch", true).await
})
}
#[day_macros::test(day_core)]
fn toggle_a11y() -> Case {
let on = Signal::new(false);
Case::new()
.proves(kinds::TOGGLE)
.page(move || {
toggle(on)
.a11y(|a| a.label("Notifications"))
.id(day_core::id!(switch))
})
.drive(|d: Drive| async move { d.a11y_audit(Some("switch")).await })
}
#[day_macros::test(day_core)]
fn slider_disabled() -> Case {
let value = Signal::new(0.5f64);
let enabled = Signal::new(false);
Case::new()
.proves(kinds::SLIDER)
.page(move || {
column((
slider(value)
.enabled(move || enabled.get())
.id(day_core::id!(slider)),
button("Enable")
.action(move || enabled.set(true))
.id(day_core::id!(enable)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_enabled("slider", false).await?;
d.tap("enable").await?;
d.assert_enabled("slider", true).await?;
d.set_value("slider", 0.25).await?;
d.assert_value("slider", 0.25).await
})
}
#[day_macros::test(day_core)]
fn slider_follows_signal() -> Case {
let value = Signal::new(10.0f64);
Case::new()
.proves(kinds::SLIDER)
.page(move || {
column((
slider(value).range(0.0..=50.0).id(day_core::id!(slider)),
button("Set 40")
.action(move || value.set(40.0))
.id(day_core::id!(set)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_value("slider", 10.0).await?;
d.tap("set").await?;
d.assert_value("slider", 40.0).await
})
}
#[day_macros::test(day_core)]
fn slider_a11y() -> Case {
let value = Signal::new(0.5f64);
Case::new()
.proves(kinds::SLIDER)
.page(move || {
slider(value)
.a11y(|a| a.label("Volume"))
.id(day_core::id!(slider))
})
.drive(|d: Drive| async move { d.a11y_audit(Some("slider")).await })
}
#[day_macros::test(day_core)]
fn text_field_disabled() -> Case {
let text = Signal::new("Fixed".to_owned());
let enabled = Signal::new(false);
Case::new()
.proves(kinds::TEXT_FIELD)
.page(move || {
column((
text_field(text)
.enabled(move || enabled.get())
.id(day_core::id!(field)),
button("Enable")
.action(move || enabled.set(true))
.id(day_core::id!(enable)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_enabled("field", false).await?;
d.assert_text("field", "Fixed").await?;
d.tap("enable").await?;
d.assert_enabled("field", true).await?;
d.input("field", "Changed").await?;
d.assert_text("field", "Changed").await
})
}
#[day_macros::test(day_core)]
fn text_field_read_only() -> Case {
let text = Signal::new("Shown".to_owned());
Case::new()
.proves(kinds::TEXT_FIELD)
.proves_duty("set_input_traits")
.page(move || {
column((
text_field(text).read_only(true).id(day_core::id!(field)),
button("Replace")
.action(move || text.set("Replaced".into()))
.id(day_core::id!(replace)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_text("field", "Shown").await?;
d.assert_enabled("field", true).await?;
d.tap("replace").await?;
d.assert_text("field", "Replaced").await
})
}
#[day_macros::test(day_core)]
fn text_field_submit() -> Case {
let text = Signal::new(String::new());
let sent = Signal::new(String::new());
Case::new()
.proves(kinds::TEXT_FIELD)
.page(move || {
column((
text_field(text)
.submit_label(day_spec::SubmitLabel::Send)
.on_submit(move || sent.set(text.get_untracked()))
.id(day_core::id!(field)),
label(move || sent.get()).id(day_core::id!(sent)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.input("field", "hello").await?;
d.submit("field").await?;
d.assert_text("sent", "hello").await
})
}
#[day_macros::test(day_core)]
fn progress_value() -> Case {
let done = Signal::new(0.25f64);
Case::new()
.proves(kinds::PROGRESS)
.page(move || {
column((
progress(done).id(day_core::id!(bar)),
button("Advance")
.action(move || done.set(0.75))
.id(day_core::id!(advance)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_value("bar", 0.25).await?;
d.tap("advance").await?;
d.assert_value("bar", 0.75).await
})
}
#[day_macros::test(day_core)]
fn spinner_renders() -> Case {
Case::new()
.proves(kinds::PROGRESS)
.page(|| spinner().id(day_core::id!(spin)))
.drive(|d: Drive| async move {
d.assert_visible("spin").await?;
d.assert_native(
"spin",
NativeExpect {
visible: Some(true),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn spinner_size_and_animation() -> Case {
Case::new()
.proves(kinds::PROGRESS)
.page(|| {
let running = Signal::new(true);
column((
spinner()
.animating(running)
.id(day_core::id!(spinner_fixture::small)),
spinner()
.animating(running)
.large()
.id(day_core::id!(spinner_fixture::large)),
button("Stop fixture")
.action(move || running.set(false))
.id(day_core::id!(spinner_fixture::stop)),
))
})
.drive(|d: Drive| async move {
let sizes = match day_core::toolkit_key() {
Some("appkit") => Some((20.0, 32.0)),
Some("uikit") => Some((20.0, 37.0)),
Some("gtk") => Some((24.0, 48.0)),
Some("arkui") => Some((32.0, 64.0)),
Some("xaml" | "winui") => Some((32.0, 48.0)),
Some("dom") => Some((22.0, 48.0)),
_ => None, };
for stopped in [false, true] {
if stopped {
d.tap("spinner_fixture::stop").await?;
}
d.assert_visible("spinner_fixture::small").await?;
d.assert_visible("spinner_fixture::large").await?;
if let Some((small, large)) = sizes {
d.assert_size("spinner_fixture::small", small, small)
.await?;
d.assert_size("spinner_fixture::large", large, large)
.await?;
}
}
Ok(())
})
}
#[day_macros::test(day_core)]
fn divider_renders() -> Case {
Case::new()
.proves(kinds::DIVIDER)
.page(|| {
column((
label("Above"),
divider().id(day_core::id!(rule)),
label("Below"),
))
.spacing(8.0)
.width(200.0)
})
.drive(|d: Drive| async move {
d.assert_visible("rule").await?;
d.assert_frame(
"rule",
FrameExpect {
width: Some(200.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn vertical_divider_fills_height() -> Case {
Case::new()
.proves(kinds::DIVIDER)
.page(|| {
row((
label("Left"),
divider().vertical().id(day_core::id!(rule)),
label("Right"),
))
.spacing(8.0)
.height(120.0)
.id(day_core::id!(row))
})
.drive(|d: Drive| async move {
d.assert_visible("rule").await?;
d.assert_frame(
"rule",
FrameExpect {
height: Some(120.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn spacer_fills() -> Case {
Case::new()
.proves_modifier("width")
.page(|| {
row((
label("A").width(50.0).wrapper_id(day_core::id!(first)),
spacer(),
label("B").width(50.0).wrapper_id(day_core::id!(last)),
))
.width(300.0)
.wrapper_id(day_core::id!(row))
})
.drive(|d: Drive| async move {
d.assert_frame(
"last",
FrameExpect {
width: Some(50.0),
x: Some(250.0),
relative_to: Some("row".into()),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn link_opens_url() -> Case {
Case::new()
.proves_duty("open_url")
.page(|| link("daybrite.dev", "https://daybrite.dev/").id(day_core::id!(link)))
.drive(|d: Drive| async move {
d.assert_text("link", "daybrite.dev").await?;
d.tap("link").await?;
d.assert_opened_url("https://daybrite.dev/").await
})
}
}