use std::cell::RefCell;
use std::rc::Rc;
use day_core::*;
use day_reactive::{bind, bind_seeded};
use day_spec::{Event, kinds};
use crate::*;
pub enum PickerOptions<T: 'static> {
Fixed(Vec<(T, TextSource)>),
Derived(Rc<dyn Fn() -> Vec<(T, TextSource)>>),
}
impl<T: Clone + 'static> PickerOptions<T> {
fn resolve(&self) -> (Vec<T>, Vec<String>) {
let pairs = match self {
PickerOptions::Fixed(v) => v.clone(),
PickerOptions::Derived(f) => f(),
};
let mut values = Vec::with_capacity(pairs.len());
let mut titles = Vec::with_capacity(pairs.len());
for (v, t) in pairs {
values.push(v);
titles.push(t.resolve());
}
(values, titles)
}
}
pub trait IntoPickerOptions<T: 'static, M> {
fn into_picker_options(self) -> PickerOptions<T>;
}
pub struct FixedOptionsMark;
pub struct DerivedOptionsMark;
impl<T: 'static, X: IntoText<M>, M, const N: usize> IntoPickerOptions<T, (FixedOptionsMark, M)>
for [(T, X); N]
{
fn into_picker_options(self) -> PickerOptions<T> {
PickerOptions::Fixed(self.into_iter().map(|(v, t)| (v, t.into_text())).collect())
}
}
impl<T: 'static, X: IntoText<M>, M> IntoPickerOptions<T, (FixedOptionsMark, M)> for Vec<(T, X)> {
fn into_picker_options(self) -> PickerOptions<T> {
PickerOptions::Fixed(self.into_iter().map(|(v, t)| (v, t.into_text())).collect())
}
}
impl<T: 'static, X: IntoText<M>, M, F> IntoPickerOptions<T, (DerivedOptionsMark, M)> for F
where
F: Fn() -> Vec<(T, X)> + 'static,
{
fn into_picker_options(self) -> PickerOptions<T> {
PickerOptions::Derived(Rc::new(move || {
self()
.into_iter()
.map(|(v, t)| (v, t.into_text()))
.collect()
}))
}
}
pub trait PickerSelection<T: 'static>: Clone + 'static {
fn current(&self) -> Option<T>;
fn choose(&self, value: T);
}
#[derive(Clone)]
pub struct Required<Sel>(Sel);
impl<T: 'static, Sel: Binding<T>> PickerSelection<T> for Required<Sel> {
fn current(&self) -> Option<T> {
Some(self.0.read())
}
fn choose(&self, value: T) {
self.0.write(value);
}
}
#[derive(Clone)]
pub struct Optional<Sel>(Sel);
impl<T: 'static, Sel: Binding<Option<T>>> PickerSelection<T> for Optional<Sel> {
fn current(&self) -> Option<T> {
self.0.read()
}
fn choose(&self, value: T) {
self.0.write(Some(value));
}
}
pub struct Picker<T: Clone + PartialEq + 'static, S: PickerSelection<T>> {
options: PickerOptions<T>,
separators_before: Vec<usize>,
selected: S,
style: day_spec::props::PickerStyle,
enabled: Reactive<bool>,
}
pub fn picker<T, Sel, M>(
selected: Sel,
options: impl IntoPickerOptions<T, M>,
) -> Picker<T, Required<Sel>>
where
T: Clone + PartialEq + 'static,
Sel: Binding<T>,
{
Picker {
options: options.into_picker_options(),
separators_before: Vec::new(),
selected: Required(selected),
style: day_spec::props::PickerStyle::Menu,
enabled: Reactive::Const(true),
}
}
pub fn picker_optional<T, Sel, M>(
selected: Sel,
options: impl IntoPickerOptions<T, M>,
) -> Picker<T, Optional<Sel>>
where
T: Clone + PartialEq + 'static,
Sel: Binding<Option<T>>,
{
Picker {
options: options.into_picker_options(),
separators_before: Vec::new(),
selected: Optional(selected),
style: day_spec::props::PickerStyle::Menu,
enabled: Reactive::Const(true),
}
}
#[day_macros::builder(PickerBuilder)]
impl<T: Clone + PartialEq + 'static, S: PickerSelection<T>> Picker<T, S> {
pub fn enabled<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.enabled = v.into_reactive();
self
}
pub fn separators_before(mut self, indexes: impl IntoIterator<Item = usize>) -> Self {
self.separators_before = indexes.into_iter().collect();
self.separators_before.sort_unstable();
self.separators_before.dedup();
self
}
pub fn menu(mut self) -> Self {
self.style = day_spec::props::PickerStyle::Menu;
self
}
pub fn segmented(mut self) -> Self {
self.style = day_spec::props::PickerStyle::Segmented;
self
}
pub fn inline(mut self) -> Self {
self.style = day_spec::props::PickerStyle::Inline;
self
}
pub fn style(mut self, style: day_spec::props::PickerStyle) -> Self {
self.style = style;
self
}
}
impl<T: Clone + PartialEq + 'static, S: PickerSelection<T>> Piece for Picker<T, S> {
fn build(self, cx: &mut BuildCx) -> RNode {
let Picker {
options,
separators_before,
selected,
style,
enabled,
} = self;
let (values0, titles0) = day_reactive::untrack(|| options.resolve());
debug_assert!(
values0
.iter()
.enumerate()
.all(|(i, v)| !values0[..i].contains(v)),
"day: a picker's option values must be distinct"
);
let values: Rc<RefCell<Vec<T>>> = Rc::new(RefCell::new(values0));
let index_of = {
let values = values.clone();
move |v: Option<T>| -> Option<usize> {
let v = v?;
values.borrow().iter().position(|x| *x == v)
}
};
let initial_index = index_of(day_reactive::untrack(|| selected.current())).unwrap_or(0);
let enabled_now = enabled.get_untracked() && !titles0.is_empty();
let initial = day_spec::props::PickerProps {
options: titles0,
separators_before,
selected: initial_index,
style,
enabled: enabled_now,
};
let node = cx.leaf(kinds::PICKER, &initial, Flex::default());
let patch = move |p: day_spec::props::PickerPatch, affects_size: bool| {
with_tree(|t| t.patch(node, Box::new(p), affects_size));
};
{
let enabled = enabled.clone();
let values = values.clone();
bind_seeded(
enabled_now,
move || enabled.get() && !values.borrow().is_empty(),
move |e: &bool| patch(day_spec::props::PickerPatch::Enabled(*e), false),
);
}
let affects_size = matches!(initial.style, day_spec::props::PickerStyle::Menu);
{
let (values, sel) = (values.clone(), selected.clone());
let index_of = index_of.clone();
bind_seeded(
initial.options.clone(),
move || {
let (vs, titles) = options.resolve();
*values.borrow_mut() = vs;
titles
},
move |titles: &Vec<String>| {
patch(day_spec::props::PickerPatch::Options(titles.clone()), true);
if let Some(i) = index_of(day_reactive::untrack(|| sel.current())) {
patch(day_spec::props::PickerPatch::Selected(i), affects_size);
}
},
);
}
{
let sel = selected.clone();
let index_of = index_of.clone();
bind_seeded(
Some(initial.selected),
move || index_of(sel.current()),
move |i: &Option<usize>| {
if let Some(i) = i {
patch(day_spec::props::PickerPatch::Selected(*i), affects_size);
}
},
);
}
cx.on(node, move |ev| {
if let Event::SelectionChanged(i) = ev
&& *i >= 0
&& let Some(v) = values.borrow().get(*i as usize).cloned()
{
selected.choose(v);
}
});
node
}
}
pub struct TextArea<S: Binding<String>> {
text: S,
placeholder: Option<TextSource>,
min_lines: u32,
max_lines: u32,
editable: Reactive<bool>,
selectable: Reactive<bool>,
spellcheck: Reactive<bool>,
on_submit: Option<Rc<dyn Fn()>>,
}
pub fn text_area<S: Binding<String>>(text: S) -> TextArea<S> {
TextArea {
text,
placeholder: None,
min_lines: 1,
max_lines: 0,
editable: true.into_reactive(),
selectable: true.into_reactive(),
spellcheck: true.into_reactive(),
on_submit: None,
}
}
#[day_macros::builder(TextAreaBuilder)]
impl<S: Binding<String>> TextArea<S> {
pub fn placeholder<M>(mut self, t: impl IntoText<M>) -> Self {
self.placeholder = Some(t.into_text());
self
}
pub fn min_lines(mut self, lines: u32) -> Self {
self.min_lines = lines.max(1);
self
}
pub fn max_lines(mut self, lines: u32) -> Self {
self.max_lines = lines;
self
}
pub fn editable<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.editable = v.into_reactive();
self
}
pub fn selectable<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.selectable = v.into_reactive();
self
}
pub fn spellcheck<M>(mut self, v: impl IntoReactive<bool, M>) -> Self {
self.spellcheck = v.into_reactive();
self
}
pub fn on_submit(mut self, f: impl Fn() + 'static) -> Self {
self.on_submit = Some(Rc::new(f));
self
}
}
impl<S: Binding<String>> Piece for TextArea<S> {
fn build(self, cx: &mut BuildCx) -> RNode {
let TextArea {
text,
placeholder,
min_lines,
max_lines,
editable,
selectable,
spellcheck,
on_submit,
} = self;
let initial = text.peek();
let ph = placeholder.map(|p| p.initial()).unwrap_or_default();
let node = cx.leaf(
kinds::TEXT_AREA,
&day_spec::props::TextAreaProps {
text: initial.clone(),
placeholder: ph,
min_lines,
max_lines: if max_lines == 0 {
0
} else {
max_lines.max(min_lines)
},
editable: editable.get_untracked(),
selectable: selectable.get_untracked(),
spellcheck: spellcheck.get_untracked(),
submit_on_enter: on_submit.is_some(),
},
Flex {
grow_w: true,
..Default::default()
},
);
if let Reactive::Dyn(_) = &editable {
bind(
move || editable.get(),
move |v: &bool| {
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::TextAreaPatch::SetEditable(*v)),
false,
)
});
},
);
}
if let Reactive::Dyn(_) = &selectable {
bind(
move || selectable.get(),
move |v: &bool| {
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::TextAreaPatch::SetSelectable(*v)),
false,
)
});
},
);
}
if let Reactive::Dyn(_) = &spellcheck {
bind(
move || spellcheck.get(),
move |v: &bool| {
with_tree(|t| {
t.patch(
node,
Box::new(day_spec::props::TextAreaPatch::SetSpellCheck(*v)),
false,
)
});
},
);
}
let guard: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let g = guard.clone();
let tx = text.clone();
bind_seeded(
initial,
move || tx.read(),
move |t: &String| {
let from_native = g.borrow_mut().take().as_deref() == Some(t.as_str());
if !from_native {
with_tree(|tr| {
tr.patch(
node,
Box::new(day_spec::props::TextAreaPatch::SetText(t.clone())),
true,
)
});
}
},
);
cx.on(node, move |ev| match ev {
Event::TextChanged(t) => {
with_tree(|tree| tree.set_probe_value(node, 0.0, t.clone()));
*guard.borrow_mut() = Some(t.clone());
text.write(t.clone());
}
Event::Submitted => {
if let Some(f) = &on_submit {
f();
}
}
_ => {}
});
node
}
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::conformance::{Case, Drive};
use day_reactive::Signal;
use day_spec::{Cap, kinds};
use crate::*;
const FRUIT: [(usize, &str); 3] = [(0, "Apple"), (1, "Pear"), (2, "Plum")];
#[day_macros::test(day_core)]
fn picker_select() -> Case {
let choice = Signal::new(0usize);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(choice, FRUIT).id(day_core::id!(picker)),
label(move || choice.get().to_string()).id(day_core::id!(index)),
))
.spacing(8.0)
})
.shot("default")
.drive(|d: Drive| async move {
d.assert_text("picker", "Apple").await?;
d.select("picker", 2).await?;
d.assert_text("index", "2").await?;
d.assert_text("picker", "Plum").await
})
}
#[day_macros::test(day_core)]
fn picker_segmented() -> Case {
let choice = Signal::new(1usize);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(choice, FRUIT).segmented().id(day_core::id!(picker)),
label(move || choice.get().to_string()).id(day_core::id!(index)),
))
.spacing(8.0)
})
.shot("default")
.drive(|d: Drive| async move {
d.assert_text("picker", "Pear").await?;
d.select("picker", 0).await?;
d.assert_text("index", "0").await?;
d.assert_text("picker", "Apple").await
})
}
#[day_macros::test(day_core)]
fn picker_inline() -> Case {
let choice = Signal::new(0usize);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(choice, FRUIT).inline().id(day_core::id!(picker)),
label(move || choice.get().to_string()).id(day_core::id!(index)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.select("picker", 1).await?;
d.assert_text("index", "1").await?;
d.assert_text("picker", "Pear").await
})
}
#[day_macros::test(day_core)]
fn picker_follows_signal() -> Case {
let choice = Signal::new(0usize);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(choice, FRUIT).id(day_core::id!(picker)),
button("Plum")
.action(move || choice.set(2))
.id(day_core::id!(set)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.tap("set").await?;
d.assert_text("picker", "Plum").await
})
}
#[day_macros::test(day_core)]
fn picker_options_reactive() -> Case {
let choice = Signal::new(1usize);
let more = Signal::new(false);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(choice, move || {
let mut v = vec![(0usize, "One"), (1, "Two")];
if more.get() {
v.push((2, "Three"));
}
v
})
.id(day_core::id!(picker)),
button("More")
.action(move || more.set(true))
.id(day_core::id!(more)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_text("picker", "Two").await?;
d.tap("more").await?;
d.select("picker", 2).await?;
d.assert_text("picker", "Three").await
})
}
#[day_macros::test(day_core)]
fn picker_disabled() -> Case {
let (a, b, c) = (
Signal::new(0usize),
Signal::new(0usize),
Signal::new(0usize),
);
let enabled = Signal::new(false);
Case::new()
.proves(kinds::PICKER)
.page(move || {
column((
picker(a, FRUIT)
.enabled(move || enabled.get())
.id(day_core::id!(menu)),
picker(b, FRUIT)
.segmented()
.enabled(move || enabled.get())
.id(day_core::id!(segmented)),
picker(c, FRUIT)
.inline()
.enabled(move || enabled.get())
.id(day_core::id!(inline)),
button("Enable")
.action(move || enabled.set(true))
.id(day_core::id!(enable)),
))
.spacing(8.0)
})
.shot("disabled")
.drive(|d: Drive| async move {
for id in ["menu", "segmented", "inline"] {
d.assert_enabled(id, false).await?;
}
d.tap("enable").await?;
for id in ["menu", "segmented", "inline"] {
d.assert_enabled(id, true).await?;
}
Ok(())
})
}
#[day_macros::test(day_core)]
fn text_area_binding() -> Case {
let text = Signal::new(String::new());
Case::new()
.proves(kinds::TEXT_AREA)
.page(move || {
column((
text_area(text).placeholder("Notes").id(day_core::id!(area)),
label(move || text.get().chars().count().to_string()).id(day_core::id!(len)),
button("Set")
.action(move || text.set("Set from Day".into()))
.id(day_core::id!(set)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.input("area", "typed").await?;
d.assert_text("len", "5").await?;
d.tap("set").await?;
d.assert_text("area", "Set from Day").await
})
}
#[day_macros::test(day_core)]
fn text_area_multiline() -> Case {
let text = Signal::new("first\nsecond".to_owned());
Case::new()
.proves(kinds::TEXT_AREA)
.page(move || text_area(text).min_lines(3).id(day_core::id!(area)))
.drive(|d: Drive| async move {
d.assert_text("area", "first\nsecond").await?;
d.input("area", "one\ntwo\nthree").await?;
d.assert_text("area", "one\ntwo\nthree").await
})
}
#[day_macros::test(day_core)]
fn text_area_submit() -> Case {
let text = Signal::new(String::new());
let sent = Signal::new(String::new());
let count = Signal::new(0u32);
Case::new()
.proves(kinds::TEXT_AREA)
.page(move || {
column((
text_area(text)
.min_lines(3)
.on_submit(move || {
sent.set(text.get_untracked());
count.update(|n| *n += 1);
text.set(String::new());
})
.id(day_core::id!(area)),
label(move || sent.get()).id(day_core::id!(sent)),
label(move || count.get().to_string()).id(day_core::id!(count)),
))
})
.drive(|d: Drive| async move {
d.input("area", "first\n東京").await?;
d.assert_text("count", "0").await?;
d.submit("area").await?;
d.assert_text("sent", "first\n東京").await?;
d.assert_text("count", "1").await?;
d.assert_text("area", "").await?;
d.input("area", "next").await?;
d.submit("area").await?;
d.assert_text("sent", "next").await?;
d.assert_text("count", "2").await?;
d.assert_text("area", "").await
})
}
#[day_macros::test(day_core)]
fn text_area_read_only() -> Case {
let text = Signal::new("Read me".to_owned());
Case::new()
.proves(kinds::TEXT_AREA)
.proves_cap(Cap::TextEditable)
.requires(Cap::TextEditable)
.page(move || {
column((
text_area(text).editable(false).id(day_core::id!(area)),
button("Replace")
.action(move || text.set("Replaced".into()))
.id(day_core::id!(replace)),
))
.spacing(8.0)
})
.drive(|d: Drive| async move {
d.assert_text("area", "Read me").await?;
d.tap("replace").await?;
d.assert_text("area", "Replaced").await
})
}
#[day_macros::test(day_core)]
fn text_area_unselectable() -> Case {
let text = Signal::new("Fixed".to_owned());
Case::new()
.proves(kinds::TEXT_AREA)
.proves_cap(Cap::TextSelectable)
.requires(Cap::TextSelectable)
.page(move || {
text_area(text)
.selectable(false)
.editable(false)
.id(day_core::id!(area))
})
.drive(|d: Drive| async move { d.assert_text("area", "Fixed").await })
}
#[day_macros::test(day_core)]
fn text_area_attributes_reactive() -> Case {
let text = Signal::new("First\nsecond".to_owned());
let editable = Signal::new(true);
let selectable = Signal::new(true);
Case::new()
.proves(kinds::TEXT_AREA)
.requires(Cap::TextEditable)
.requires(Cap::TextSelectable)
.page(move || {
column((
text_area(text)
.editable(editable)
.selectable(selectable)
.id(day_core::id!(area)),
label(move || text.get()).id(day_core::id!(model)),
button("Lock").id(day_core::id!(lock)).action(move || {
editable.set(false);
selectable.set(false);
}),
button("Replace")
.id(day_core::id!(replace))
.action(move || text.set("Updated\n東京".into())),
button("Unlock").id(day_core::id!(unlock)).action(move || {
selectable.set(true);
editable.set(true);
}),
))
})
.drive(|d: Drive| async move {
d.tap("lock").await?;
d.assert_text("area", "First\nsecond").await?;
d.tap("replace").await?;
d.assert_text("area", "Updated\n東京").await?;
d.assert_text("model", "Updated\n東京").await?;
d.tap("unlock").await?;
d.assert_text("area", "Updated\n東京").await?;
d.input("area", "Back to editing").await?;
d.assert_text("model", "Back to editing").await
})
}
}