use crate::prelude::*;
use day_core::{BuildCx, Flex, Piece, RNode, with_tree};
use day_reactive::{Binding, bind_seeded};
use day_spec::{Event, Support};
pub use day_spec::controls::stepper::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StepperIdiom {
Native,
Composed,
#[default]
Automatic,
}
pub fn support() -> day_spec::Support {
let support = day_core::capability(day_spec::Cap::Stepper);
if support == day_spec::Support::Unsupported {
day_spec::Support::Emulated
} else {
support
}
}
pub fn fmt_value(v: f64, decimals: u32) -> String {
format!("{v:.prec$}", prec = decimals as usize)
}
pub struct Stepper<V: Binding<f64>> {
value: V,
min: f64,
max: f64,
step: f64,
decimals: u32,
idiom: StepperIdiom,
key: day_core::ElementId,
}
pub fn stepper<V: Binding<f64>>(value: V) -> Stepper<V> {
Stepper {
value,
min: 0.0,
max: 100.0,
step: 1.0,
decimals: 0,
idiom: StepperIdiom::Automatic,
key: day_core::id!(stepper),
}
}
impl<V: Binding<f64>> Stepper<V> {
pub fn range(mut self, range: std::ops::RangeInclusive<f64>) -> Self {
self.min = *range.start();
self.max = *range.end();
self
}
pub fn step(mut self, step: f64) -> Self {
self.step = step.max(f64::EPSILON);
self
}
pub fn decimals(mut self, decimals: u32) -> Self {
self.decimals = decimals;
self
}
pub fn idiom(mut self, idiom: StepperIdiom) -> Self {
self.idiom = idiom;
self
}
pub fn native(self) -> Self {
self.idiom(StepperIdiom::Native)
}
pub fn composed(self) -> Self {
self.idiom(StepperIdiom::Composed)
}
pub fn id(mut self, id: day_core::ElementId) -> Self {
self.key = id;
self
}
}
impl<V: Binding<f64>> Piece for Stepper<V> {
fn build(self, cx: &mut BuildCx) -> RNode {
let native = match self.idiom {
StepperIdiom::Composed => false,
StepperIdiom::Native => true,
StepperIdiom::Automatic => support() == Support::Native,
};
if native {
build_native(self, cx)
} else {
build_composed(self, cx)
}
}
}
fn build_native<V: Binding<f64>>(stepper: Stepper<V>, cx: &mut BuildCx) -> RNode {
let Stepper {
value,
min,
max,
step,
decimals,
key,
..
} = stepper;
let clamp = move |v: f64| v.clamp(min, max);
let initial = clamp(value.peek());
let node = cx.leaf(
KIND,
&StepperProps {
value: initial,
min,
max,
step,
decimals,
},
Flex::default(),
);
with_tree(|t| t.set_id(node, key.as_str().to_owned()));
let note_probe = move |v: f64| {
with_tree(|t| t.set_probe_value(node, v, fmt_value(v, decimals)));
};
note_probe(initial);
{
let v = value.clone();
bind_seeded(
initial,
move || clamp(v.read()),
move |val: &f64| {
with_tree(|t| t.patch(node, Box::new(StepperPatch::SetValue(*val)), false));
note_probe(*val);
},
);
}
cx.on(node, move |ev| {
match ev {
Event::Custom { tag, text, .. } if *tag == VALUE_TAG || tag.is_empty() => {
if let Ok(v) = text.trim().parse::<f64>() {
value.write_commit(clamp(v));
}
}
Event::TextChanged(s) => {
if let Ok(v) = s.trim().parse::<f64>() {
value.write_commit(clamp(v));
}
}
Event::ValueChanged(v) => value.write_preview(clamp(*v)),
Event::ValueCommitted(v) => value.write_commit(clamp(*v)),
_ => {}
};
});
node
}
fn build_composed<V: Binding<f64>>(stepper: Stepper<V>, cx: &mut BuildCx) -> RNode {
let Stepper {
value,
min,
max,
step,
decimals,
key,
..
} = stepper;
let clamp = move |v: f64| v.clamp(min, max);
let stepped = {
let value = value.clone();
move |dir: f64| {
let v = clamp(value.peek() + dir * step);
value.write_commit(v);
}
};
let dec = {
let stepped = stepped.clone();
move || stepped(-1.0)
};
let inc = move || stepped(1.0);
struct FieldBinding<V: Binding<f64>> {
value: V,
min: f64,
max: f64,
decimals: u32,
}
impl<V: Binding<f64>> Clone for FieldBinding<V> {
fn clone(&self) -> Self {
FieldBinding {
value: self.value.clone(),
min: self.min,
max: self.max,
decimals: self.decimals,
}
}
}
impl<V: Binding<f64>> Binding<String> for FieldBinding<V> {
fn read(&self) -> String {
fmt_value(self.value.read().clamp(self.min, self.max), self.decimals)
}
fn peek(&self) -> String {
fmt_value(self.value.peek().clamp(self.min, self.max), self.decimals)
}
fn write(&self, s: String) {
self.write_commit(s);
}
fn write_preview(&self, _s: String) {}
fn write_commit(&self, s: String) {
if let Ok(v) = s.trim().parse::<f64>() {
self.value.write_commit(v.clamp(self.min, self.max));
}
}
}
let (dec_id, inc_id) = (
day_core::id!(stepper_decrement, key.as_str()),
day_core::id!(stepper_increment, key.as_str()),
);
let field = Valued {
inner: text_field(FieldBinding {
value: value.clone(),
min,
max,
decimals,
})
.id(key),
value,
clamp,
}
.width(56.0);
row((
button("−").compact().action(dec).id(dec_id),
field,
button("+").compact().action(inc).id(inc_id),
))
.spacing(4.0)
.align(VAlign::Center)
.build(cx)
}
struct Valued<P: Piece, V: Binding<f64>, C: Fn(f64) -> f64 + 'static> {
inner: P,
value: V,
clamp: C,
}
impl<P: Piece, V: Binding<f64>, C: Fn(f64) -> f64 + 'static> Piece for Valued<P, V, C> {
fn build(self, cx: &mut BuildCx) -> RNode {
let Valued {
inner,
value,
clamp,
} = self;
let node = inner.build(cx);
cx.on(node, move |ev| match ev {
Event::ValueChanged(v) => value.write_preview(clamp(*v)),
Event::ValueCommitted(v) => value.write_commit(clamp(*v)),
_ => {}
});
node
}
}
#[cfg(test)]
mod tests {
use super::*;
use day_mock::MockToolkit;
use day_reactive::{Signal, flush_sync};
use day_spec::WindowOptions;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone)]
struct Writes {
value: Signal<f64>,
events: Rc<RefCell<Vec<(&'static str, f64)>>>,
}
impl Binding<f64> for Writes {
fn read(&self) -> f64 {
self.value.get()
}
fn peek(&self) -> f64 {
self.value.get_untracked()
}
fn write(&self, value: f64) {
self.write_commit(value);
}
fn write_preview(&self, value: f64) {
self.events.borrow_mut().push(("preview", value));
self.value.set(value);
}
fn write_commit(&self, value: f64) {
self.events.borrow_mut().push(("commit", value));
self.value.set(value);
}
}
fn boot(native: bool) -> (Writes, RNode) {
day_core::uninstall_tree();
let writes = Writes {
value: Signal::new(3.0),
events: Rc::default(),
};
let binding = writes.clone();
let (mock, _) = MockToolkit::new();
day_core::launch_with(mock, WindowOptions::default(), move || {
let field = stepper(binding).range(0.0..=10.0);
if native {
field.native()
} else {
field.composed()
}
.id(day_core::id!(fixture_stepper))
.any()
});
let node = with_tree(|t| t.find_by_id(day_core::id!(fixture_stepper).as_str())).unwrap();
(writes, node)
}
fn emit(node: RNode, event: Event) {
day_core::enqueue_event(day_core::rnode_to_id(node), event);
flush_sync();
}
#[test]
fn scripted_typing_commits_once_in_both_idioms() {
for native in [true, false] {
let (writes, node) = boot(native);
day_core::synthesize_text(node, "7".into());
flush_sync();
emit(node, Event::Submitted);
assert_eq!(writes.peek(), 7.0);
assert_eq!(
*writes.events.borrow(),
[("commit", 7.0)],
"native={native}"
);
}
}
#[test]
fn native_custom_events_accept_only_value_tags_and_the_legacy_bridge() {
let (writes, node) = boot(true);
emit(node, Event::custom(day_core::SYNTHESIZED_TEXT, "8"));
emit(node, Event::custom("fixture:unrelated", "9"));
assert_eq!(writes.peek(), 3.0);
assert!(writes.events.borrow().is_empty());
emit(node, Event::custom(VALUE_TAG, "6"));
emit(node, Event::custom("", "12"));
assert_eq!(*writes.events.borrow(), [("commit", 6.0), ("commit", 10.0)]);
assert_eq!(writes.peek(), 10.0);
}
#[test]
fn scripted_values_keep_the_preview_and_commit_boundary() {
for native in [true, false] {
let (writes, node) = boot(native);
emit(node, Event::ValueChanged(12.0));
emit(node, Event::ValueCommitted(12.0));
assert_eq!(
*writes.events.borrow(),
[("preview", 10.0), ("commit", 10.0)],
"native={native}"
);
assert_eq!(writes.peek(), 10.0);
}
}
}
#[cfg(feature = "conformance")]
pub mod conformance {
use crate::*;
use day_core::AnyPiece;
use day_core::conformance::{Case, Drive};
use day_reactive::Signal;
use super::{KIND, stepper};
fn page(composed: bool) -> AnyPiece {
let value = Signal::new(3.0f64);
let field = stepper(value)
.range(0.0..=10.0)
.id(day_core::id!(stepper::count));
column((
label(move || format!("value {}", value.get())).id(day_core::id!(stepper::value)),
if composed {
field.composed().any()
} else {
field.native().any()
},
))
.any()
}
#[day_macros::test(day_core)]
fn stepper_sets_value() -> Case {
Case::new()
.proves(KIND)
.page(|| page(false))
.drive(|d: Drive| async move {
d.assert_text("stepper::count", "3").await?;
d.set_value("stepper::count", 7.0).await?;
d.assert_text("stepper::value", "value 7").await?;
d.assert_text("stepper::count", "7").await
})
}
#[day_macros::test(day_core)]
fn stepper_clamps_to_range() -> Case {
Case::new()
.proves(KIND)
.page(|| page(false))
.drive(|d: Drive| async move {
d.set_value("stepper::count", 42.0).await?;
d.assert_text("stepper::value", "value 10").await
})
}
#[day_macros::test(day_core)]
fn stepper_composed_sets_value() -> Case {
Case::new()
.page(|| page(true))
.drive(|d: Drive| async move {
d.input("stepper::count", "5").await?;
d.submit("stepper::count").await?;
d.assert_text("stepper::value", "value 5").await?;
d.tap(day_core::id!(stepper_increment, "stepper::count").as_str())
.await?;
d.assert_text("stepper::value", "value 6").await?;
d.assert_text("stepper::count", "6").await?;
d.set_value("stepper::count", 9.0).await?;
d.assert_text("stepper::value", "value 9").await?;
d.assert_text("stepper::count", "9").await
})
}
}