use std::cell::Cell;
use std::rc::Rc;
use day_core::*;
use day_reactive::{Scope, bind};
use day_spec::props::*;
use day_spec::{A11yProps, AnimSpec, Color, Cursor, Event, Insets, Role, Transform, kinds};
use crate::menus::lower_menu_scoped;
use crate::*;
pub trait IntoInsets {
fn into_insets(self) -> Insets;
}
impl IntoInsets for f64 {
fn into_insets(self) -> Insets {
Insets::all(self)
}
}
impl IntoInsets for Insets {
fn into_insets(self) -> Insets {
self
}
}
pub trait Modifier {
fn apply(self, content: AnyPiece) -> AnyPiece;
}
impl<F> Modifier for F
where
F: FnOnce(AnyPiece) -> AnyPiece,
{
fn apply(self, content: AnyPiece) -> AnyPiece {
self(content)
}
}
#[derive(Clone)]
pub struct NativeRef {
cell: Rc<std::cell::Cell<Option<day_core::RNode>>>,
changed: day_reactive::Trigger,
}
impl Default for NativeRef {
fn default() -> Self {
Self::new()
}
}
impl NativeRef {
pub fn new() -> Self {
NativeRef {
cell: Rc::new(std::cell::Cell::new(None)),
changed: day_reactive::Trigger::new(),
}
}
pub fn node(&self) -> Option<day_core::RNode> {
self.changed.track();
let node = self.cell.get()?;
let live = day_core::try_with_tree(|t| t.node_kind(node).is_some()).unwrap_or(false);
live.then_some(node)
}
pub fn with<R>(&self, f: impl FnOnce(day_core::RNode) -> R) -> Option<R> {
self.node().map(f)
}
fn transition(&self, node: Option<day_core::RNode>) {
self.cell.set(node);
self.changed.notify();
}
}
fn layer_node(cx: &mut BuildCx) -> RNode {
cx.native(
kinds::CONTAINER,
&ContainerProps {
background: None,
corner_radius: 0.0,
clips: false,
role: None,
},
Rc::new(FillThrough),
Flex::default(),
Boundary::No,
)
}
type Build = Box<dyn FnOnce(&mut BuildCx) -> RNode>;
pub struct Decorated<P> {
inner: P,
ops: Vec<Box<dyn FnOnce(Build) -> Build>>,
node_ops: Vec<Box<dyn FnOnce(RNode)>>,
outer_ops: Vec<Box<dyn FnOnce(RNode)>>,
}
impl<P: Piece> Decorated<P> {
pub fn new(inner: P) -> Self {
Decorated {
inner,
ops: Vec::new(),
node_ops: Vec::new(),
outer_ops: Vec::new(),
}
}
fn push(mut self, op: impl FnOnce(Build) -> Build + 'static) -> Self {
self.ops.push(Box::new(op));
self
}
fn push_node(mut self, op: impl FnOnce(RNode) + 'static) -> Self {
self.node_ops.push(Box::new(op));
self
}
fn push_outer(mut self, op: impl FnOnce(RNode) + 'static) -> Self {
self.outer_ops.push(Box::new(op));
self
}
pub fn map_inner<Q: Piece>(self, f: impl FnOnce(P) -> Q) -> Decorated<Q> {
Decorated {
inner: f(self.inner),
ops: self.ops,
node_ops: self.node_ops,
outer_ops: self.outer_ops,
}
}
pub fn apply_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
if cond { f(self) } else { self }
}
pub fn apply_some<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
match value {
Some(v) => f(self, v),
None => self,
}
}
}
impl<P: Piece> Piece for Decorated<P> {
fn build(self, cx: &mut BuildCx) -> RNode {
let Decorated {
inner,
ops,
node_ops,
outer_ops,
} = self;
let wraps = !ops.is_empty();
let own = Rc::new(Cell::new(None::<RNode>));
let own_out = own.clone();
let mut build: Build = Box::new(move |cx| {
let n = inner.build(cx);
for op in node_ops {
op(n);
}
own_out.set(Some(n));
n
});
for op in ops {
build = op(build);
}
let outer = build(cx);
if wraps && let Some(n) = own.get() {
with_tree(|t| t.set_element_outer(n, outer));
}
for op in outer_ops {
op(outer);
}
outer
}
}
fn op_id(id: day_core::ElementId) -> impl FnOnce(RNode) {
move |n| with_tree(|t| t.set_id(n, id.as_str().to_owned()))
}
fn op_id_of(id: impl Fn() -> day_core::ElementId + 'static) -> impl FnOnce(RNode) {
move |n| {
day_reactive::bind(id, move |s: &day_core::ElementId| {
let s = s.as_str().to_owned();
with_tree(|t| t.set_id(n, s));
});
}
}
fn op_wrapper_id(id: day_core::ElementId) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.set_id(n, id.as_str().to_owned()));
n
})
}
}
fn op_tweak(f: impl FnOnce(day_core::RNode) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
f(n);
with_tree(|t| t.note_node_tweaked(n));
n
})
}
}
fn op_selectable() -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.set_node_selectable(n, true));
n
})
}
}
fn op_cursor(cursor: Reactive<Cursor>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.set_node_cursor(n, cursor.get_untracked()));
if let Reactive::Dyn(_) = &cursor {
bind(
move || cursor.get(),
move |c: &Cursor| {
with_tree(|t| t.set_node_cursor(n, c.clone()));
},
);
}
n
})
}
}
fn op_window_drag_region() -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.set_node_drag_region(n, true));
n
})
}
}
fn op_native_ref(r: NativeRef) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
r.transition(Some(n));
let cleared = r.clone();
Scope::current().on_cleanup(move || cleared.transition(None));
n
})
}
}
fn op_padding(insets: Insets) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(
Rc::new(PaddingLayout { insets }),
Flex::default(),
Boundary::No,
);
cx.under(w, |cx| {
let _ = inner(cx);
});
w
})
}
}
fn op_max_width(max: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(
Rc::new(MaxWidthLayout { max }),
Flex::default(),
Boundary::No,
);
cx.under(w, |cx| {
let _ = inner(cx);
});
w
})
}
}
fn op_max_height(max: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let h = cx.layout_only(
Rc::new(day_core::MaxHeightLayout { max }),
Flex {
yields_h: true,
..Flex::default()
},
Boundary::No,
);
cx.under(h, |cx| {
let _ = inner(cx);
});
h
})
}
}
fn op_min_width(min: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(
Rc::new(day_core::MinWidthLayout { min }),
Flex::default(),
Boundary::No,
);
cx.under(w, |cx| {
let _ = inner(cx);
});
w
})
}
}
fn op_reserving(sample: String) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(
Rc::new(day_core::ReserveLayout),
Flex::default(),
Boundary::No,
);
cx.under(w, |cx| {
let _ = crate::label(sample.clone()).opacity(0.0).build(cx);
let _ = inner(cx);
});
w
})
}
}
fn op_frame(
width: Option<f64>,
height: Option<f64>,
boundary: Boundary,
) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(
Rc::new(FrameLayout { width, height }),
Flex::default(),
boundary,
);
cx.under(w, |cx| {
let _ = inner(cx);
});
w
})
}
}
fn op_a11y(f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
let (props, live, selected) = f(A11yBuilder::default()).split();
let seed = props.selected;
with_tree(|t| t.set_a11y(n, props));
if let Some(selected) = selected {
day_reactive::bind_seeded(
seed.unwrap_or(false),
move || selected.get(),
move |on: &bool| {
with_tree(|t| {
t.set_a11y(
n,
A11yProps {
selected: Some(*on),
..Default::default()
},
)
})
},
);
}
for (src, patch) in live {
if let TextSource::Dyn(read) = src {
let seed = day_reactive::untrack(|| read());
day_reactive::bind_seeded(
seed,
move || read(),
move |s: &String| with_tree(|t| t.set_a11y(n, patch(s.clone()))),
);
}
}
n
})
}
}
fn op_on_tap(f: impl Fn() + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Tap));
cx.on(n, move |ev| {
if matches!(ev, Event::Tap(_)) {
f();
}
});
n
})
}
}
fn op_on_tap_at(f: impl Fn(day_spec::Point) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Tap));
cx.on(n, move |ev| {
if let Event::Tap(p) = ev {
f(*p);
}
});
n
})
}
}
fn op_on_key(f: impl Fn(&day_spec::KeyEvent) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
day_spec::keys::mark(day_core::rnode_to_id(n));
if let Some(view) = with_tree(|t| t.native_view_of(n))
&& view != n
{
day_spec::keys::mark(day_core::rnode_to_id(view));
}
cx.on(n, move |ev| {
if let Event::Key(k) = ev {
f(k);
}
});
n
})
}
}
fn op_toolbar(source: crate::ToolbarSource) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
crate::contribute(day_core::current_chrome(), source);
n
})
}
}
fn op_focusable() -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.set_focusable(n, true));
n
})
}
}
fn op_focused(
want: Box<dyn Fn() -> bool>,
on_native: Box<dyn Fn(bool)>,
) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
let native = Rc::new(Cell::new(false));
{
let native = native.clone();
cx.on(n, move |ev| {
if let Event::FocusChanged(f) = ev {
native.set(*f);
on_native(*f);
}
});
}
let first = Cell::new(true);
bind(want, move |want: &bool| {
let want = *want;
if first.replace(false) && !want {
return;
}
if native.get() == want {
return;
}
day_reactive::on_main(move || with_tree(|t| t.focus_node(n, want)));
});
n
})
}
}
fn op_context_menu(items: Vec<MenuEntry>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(Rc::new(OverlayHost), Flex::default(), Boundary::No);
let mut n = w;
cx.under(w, |cx| {
n = inner(cx);
let model = lower_menu_scoped(items);
with_tree(|t| t.set_context_menu(n, model.clone()));
let model = std::rc::Rc::new(model);
crate::menus::mount_composed_menu(cx, n, Rc::new(move |_p| (*model).clone()));
});
with_tree(|t| t.forward_whole_row(w, n));
n
})
}
}
struct OverlayHost;
impl Layout for OverlayHost {
fn measure(
&self,
cx: &mut dyn LayoutOps,
children: &[day_core::RNode],
p: day_spec::Proposal,
) -> day_spec::Size {
match children.first() {
Some(&c) => cx.measure_child(c, p),
None => day_spec::Size::ZERO,
}
}
fn place(&self, cx: &mut dyn LayoutOps, children: &[day_core::RNode], bounds: day_spec::Rect) {
let mut it = children.iter();
if let Some(&c) = it.next() {
cx.place_child(c, day_spec::Rect::from_size(bounds.size));
}
for &c in it {
cx.place_child(c, day_spec::Rect::ZERO);
}
}
fn baseline(
&self,
cx: &mut dyn LayoutOps,
children: &[day_core::RNode],
size: day_spec::Size,
) -> Option<f64> {
children.first().and_then(|&c| cx.baseline_of(c, size))
}
}
fn op_context_menu_fn(
f: Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>,
) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.layout_only(Rc::new(OverlayHost), Flex::default(), Boundary::No);
let mut out = w;
cx.under(w, |cx| {
out = op_context_menu_fn_body(inner, f, cx);
});
with_tree(|t| t.forward_whole_row(w, out));
out
})
}
}
fn op_context_menu_fn_body(
inner: Build,
f: Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>,
cx: &mut day_core::BuildCx,
) -> day_core::RNode {
{
{
let n = inner(cx);
let last: Rc<std::cell::RefCell<Option<day_reactive::Scope>>> = Rc::default();
{
let last = last.clone();
day_reactive::Scope::current().on_cleanup(move || {
if let Some(s) = last.borrow_mut().take() {
s.dispose();
}
});
}
let provider: day_spec::ContextMenuFn = Rc::new(move |p| {
if let Some(s) = last.borrow_mut().take() {
s.dispose();
}
let scope = day_reactive::Scope::child();
let items = scope.enter(|| crate::menus::lower_menu_scoped(f(p)));
*last.borrow_mut() = Some(scope);
items
});
with_tree(|t| t.set_context_menu_fn(n, provider.clone()));
crate::menus::mount_composed_menu(cx, n, provider);
n
}
}
}
fn op_on_drag(f: impl Fn(Drag) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Drag));
cx.on(n, move |ev| {
if let Event::Drag {
phase,
location,
translation,
} = ev
{
f(Drag {
phase: *phase,
location: *location,
translation: *translation,
});
}
});
n
})
}
}
fn op_on_pinch(f: impl Fn(Pinch) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Pinch));
cx.on(n, move |ev| {
if let Event::Pinch {
phase,
scale,
location,
} = ev
{
f(Pinch {
phase: *phase,
scale: *scale,
location: *location,
});
}
});
n
})
}
}
fn op_on_hover(f: impl Fn(Option<day_spec::Point>) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Hover));
cx.on(n, move |ev| {
if let Event::Hover { phase, location } = ev {
f(match phase {
day_spec::DragPhase::Ended => None,
_ => Some(*location),
});
}
});
n
})
}
}
fn op_on_pan(f: impl Fn(Pan) + 'static) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let n = inner(cx);
with_tree(|t| t.enable_gesture(n, GestureKind::Pan));
cx.on(n, move |ev| {
if let Event::Pan {
phase,
delta,
location,
} = ev
{
f(Pan {
phase: *phase,
delta: *delta,
location: *location,
});
}
});
n
})
}
}
fn op_background(color: Reactive<Color>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = cx.native(
kinds::CONTAINER,
&ContainerProps {
background: Some(color.get_untracked()),
corner_radius: 0.0,
clips: false,
role: None,
},
Rc::new(FillThrough),
Flex::default(),
Boundary::No,
);
cx.under(node, |cx| {
let _ = inner(cx);
});
if let Reactive::Dyn(_) = &color {
bind(
move || color.get(),
move |c: &Color| {
with_tree(|t| {
t.patch(node, Box::new(ContainerPatch::Background(Some(*c))), false)
});
},
);
}
node
})
}
}
fn op_surface(role: day_spec::SurfaceRole, radius: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let w = cx.native(
kinds::CONTAINER,
&ContainerProps {
background: None,
corner_radius: radius,
clips: true,
role: Some(role),
},
Rc::new(FillThrough),
Flex::default(),
Boundary::No,
);
cx.under(w, |cx| {
let _ = inner(cx);
});
w
})
}
}
fn op_corner_radius(radius: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = cx.native(
kinds::CONTAINER,
&ContainerProps {
background: None,
corner_radius: radius,
clips: true,
role: None,
},
Rc::new(FillThrough),
Flex::default(),
Boundary::No,
);
cx.under(node, |cx| {
let _ = inner(cx);
});
node
})
}
}
fn op_opacity(op: Reactive<f64>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = layer_node(cx);
cx.under(node, |cx| {
let _ = inner(cx);
});
bind(
move || op.get(),
move |v: &f64| with_tree(|t| t.set_node_opacity(node, *v)),
);
node
})
}
}
fn op_transform(t: Reactive<Transform>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = layer_node(cx);
cx.under(node, |cx| {
let _ = inner(cx);
});
bind(
move || t.get(),
move |v: &Transform| with_tree(|tr| tr.set_node_transform(node, *v)),
);
node
})
}
}
fn op_animation(anim: AnimSpec) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = cx.layout_only(Rc::new(FillThrough), Flex::default(), Boundary::No);
with_tree(|t| t.set_implicit_anim(node, Some(anim)));
cx.under(node, |cx| {
let _ = inner(cx);
});
node
})
}
}
fn op_overlay_aligned(align: Alignment, over: impl Piece) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = cx.native(
kinds::CONTAINER,
&ContainerProps::default(),
Rc::new(OverlayLayout {
align,
size_to_first: true,
}),
Flex::default(),
Boundary::No,
);
cx.under(node, |cx| {
let _ = inner(cx); let _ = over.build(cx); });
node
})
}
}
fn op_aspect_ratio(ratio: f64) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
if !(ratio.is_finite() && ratio > 0.0) {
return inner(cx);
}
let node = cx.layout_only(
Rc::new(AspectRatioLayout { ratio }),
Flex::default(),
Boundary::No,
);
cx.under(node, |cx| {
let _ = inner(cx);
});
node
})
}
}
fn op_grow_axes(w: bool, h: bool) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let node = cx.layout_only(
Rc::new(GrowLayout { w, h }),
Flex {
grow_w: w,
grow_h: h,
..Default::default()
},
Boundary::No,
);
cx.under(node, |cx| {
let _ = inner(cx);
});
node
})
}
}
fn op_grid_facts(facts: GridFacts) -> impl FnOnce(RNode) {
move |node| with_tree(|t| t.set_grid_facts(node, facts))
}
fn op_defers_system_gestures(edges: day_spec::Edges) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let token = day_core::shield::push_gesture_deferral(edges);
Scope::current().on_cleanup(move || day_core::shield::pop_gesture_deferral(token));
inner(cx)
})
}
}
fn op_status_bar_hidden(hidden: Reactive<bool>) -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let token = day_core::shield::push_status_bar_request(hidden.get_untracked());
Scope::current().on_cleanup(move || day_core::shield::pop_status_bar_request(token));
if let Reactive::Dyn(_) = &hidden {
bind(
move || hidden.get(),
move |h: &bool| day_core::shield::set_status_bar_request(token, *h),
);
}
inner(cx)
})
}
}
fn op_interactive_dismiss_disabled() -> impl FnOnce(Build) -> Build {
move |inner| {
Box::new(move |cx| {
let token = day_core::shield::push_dismiss_disabled();
Scope::current().on_cleanup(move || day_core::shield::pop_dismiss_disabled(token));
inner(cx)
})
}
}
impl<P: Piece> Decorated<P> {
pub fn id(self, id: day_core::ElementId) -> Self {
self.push_node(op_id(id))
}
pub fn id_of(self, id: impl Fn() -> day_core::ElementId + 'static) -> Self {
self.push_node(op_id_of(id))
}
pub fn wrapper_id(self, id: day_core::ElementId) -> Self {
self.push(op_wrapper_id(id))
}
pub fn tweak(self, f: impl FnOnce(day_core::RNode) + 'static) -> Self {
self.push(op_tweak(f))
}
pub fn selectable(self) -> Self {
self.push(op_selectable())
}
pub fn cursor<M>(self, cursor: impl IntoReactive<Cursor, M>) -> Self {
self.push(op_cursor(cursor.into_reactive()))
}
pub fn native_ref(self, r: &NativeRef) -> Self {
self.push(op_native_ref(r.clone()))
}
pub fn window_drag_region(self) -> Self {
self.push(op_window_drag_region())
}
pub fn padding(self, insets: impl IntoInsets) -> Self {
self.push(op_padding(insets.into_insets()))
}
pub fn max_width(self, max: f64) -> Self {
self.push(op_max_width(max))
}
pub fn max_height(self, max: f64) -> Self {
self.push(op_max_height(max))
}
pub fn min_width(self, min: f64) -> Self {
self.push(op_min_width(min))
}
pub fn reserving(self, sample: impl Into<String>) -> Self {
self.push(op_reserving(sample.into()))
}
pub fn frame(self, width: f64, height: f64) -> Self {
self.push(op_frame(Some(width), Some(height), Boundary::Yes))
}
pub fn width(self, width: f64) -> Self {
self.push(op_frame(Some(width), None, Boundary::No))
}
pub fn height(self, height: f64) -> Self {
self.push(op_frame(None, Some(height), Boundary::No))
}
pub fn a11y(self, f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> Self {
self.push(op_a11y(f))
}
pub fn on_tap(self, f: impl Fn() + 'static) -> Self {
self.push(op_on_tap(f))
}
pub fn on_tap_at(self, f: impl Fn(day_spec::Point) + 'static) -> Self {
self.push(op_on_tap_at(f))
}
pub fn focused<M>(self, binding: impl IntoFocusBinding<M>) -> Self {
let (want, on_native) = binding.into_focus_binding();
self.push(op_focused(want, on_native))
}
pub fn on_key(self, f: impl Fn(&day_spec::KeyEvent) + 'static) -> Self {
self.push(op_on_key(f))
}
pub fn focusable(self) -> Self {
self.push(op_focusable())
}
pub fn toolbar<M>(self, content: impl crate::ToolbarContent<M>) -> Self {
self.push(op_toolbar(content.into_source()))
}
pub fn searchable(self, search: crate::Search) -> Self {
self.push(move |inner| {
Box::new(move |cx| {
let n = inner(cx);
crate::nav::install_page_search(search);
n
})
})
}
pub fn drag_source(
self,
source: impl Fn(day_spec::Point) -> Option<day_spec::transfer::Offer> + 'static,
) -> Self {
self.push(move |inner| {
Box::new(move |cx| {
let n = layer_node(cx);
cx.under(n, |cx| {
inner(cx);
});
let source = scoped_drag_source(Rc::new(source));
with_tree(|t| t.set_drag_source(n, source));
n
})
})
}
pub fn drop_target(self, target: day_spec::transfer::Target) -> Self {
self.push(move |inner| {
Box::new(move |cx| {
let n = layer_node(cx);
cx.under(n, |cx| {
inner(cx);
});
let target = scoped_drop_target(target);
with_tree(|t| t.set_drop_target(n, target));
n
})
})
}
pub fn context_menu<M>(self, menu: impl ContextMenuContent<M>) -> Self {
match menu.into_context_menu() {
ContextMenuSource::Fixed(items) => self.push(op_context_menu(items)),
ContextMenuSource::OnSummon(f) => self.push(op_context_menu_fn(f)),
}
}
pub fn on_drag(self, f: impl Fn(Drag) + 'static) -> Self {
self.push(op_on_drag(f))
}
pub fn on_pinch(self, f: impl Fn(Pinch) + 'static) -> Self {
self.push(op_on_pinch(f))
}
pub fn on_pan(self, f: impl Fn(Pan) + 'static) -> Self {
self.push(op_on_pan(f))
}
pub fn on_hover(self, f: impl Fn(Option<day_spec::Point>) + 'static) -> Self {
self.push(op_on_hover(f))
}
pub fn card(self) -> Self {
self.padding(crate::Space::M)
.push(op_surface(day_spec::SurfaceRole::SectionCard, 10.0))
}
pub fn background<M>(self, color: impl IntoReactive<Color, M>) -> Self {
self.push(op_background(color.into_reactive()))
}
pub fn corner_radius(self, radius: f64) -> Self {
self.push(op_corner_radius(radius))
}
pub fn opacity<M>(self, opacity: impl IntoReactive<f64, M>) -> Self {
self.push(op_opacity(opacity.into_reactive()))
}
pub fn transform<M>(self, t: impl IntoReactive<Transform, M>) -> Self {
self.push(op_transform(t.into_reactive()))
}
pub fn scale<M>(self, factor: impl IntoReactive<f64, M>) -> Self {
let f = factor.into_reactive();
self.transform(move || Transform::scale(f.get(), f.get()))
}
pub fn rotation<M>(self, degrees: impl IntoReactive<f64, M>) -> Self {
let d = degrees.into_reactive();
self.transform(move || Transform::rotate(d.get()))
}
pub fn translation<Mx, My>(
self,
x: impl IntoReactive<f64, Mx>,
y: impl IntoReactive<f64, My>,
) -> Self {
let (x, y) = (x.into_reactive(), y.into_reactive());
self.transform(move || Transform::translate(x.get(), y.get()))
}
pub fn animation(self, anim: AnimSpec) -> Self {
self.push(op_animation(anim))
}
pub fn modifier(self, m: impl Modifier) -> AnyPiece {
m.apply(self.any())
}
pub fn overlay(self, over: impl Piece) -> Self {
self.overlay_aligned(Alignment::Center, over)
}
pub fn overlay_aligned(self, align: Alignment, over: impl Piece) -> Self {
self.push(op_overlay_aligned(align, over))
}
pub fn aspect_ratio(self, ratio: f64) -> Self {
self.push(op_aspect_ratio(ratio))
}
pub fn grow(self) -> Self {
self.grow_axes(true, true)
}
pub fn grow_w(self) -> Self {
self.grow_axes(true, false)
}
pub fn grow_h(self) -> Self {
self.grow_axes(false, true)
}
#[doc(hidden)]
fn grow_axes(self, w: bool, h: bool) -> Self {
self.push(op_grow_axes(w, h))
}
pub fn grid_span(self, n: usize) -> Self {
self.push_outer(op_grid_facts(GridFacts {
col_span: n.clamp(1, u16::MAX as usize) as u16,
..Default::default()
}))
}
pub fn grid_align(self, a: Alignment) -> Self {
self.push_outer(op_grid_facts(GridFacts {
align: Some(a),
..Default::default()
}))
}
pub fn defers_system_gestures(self, edges: day_spec::Edges) -> Self {
self.push(op_defers_system_gestures(edges))
}
pub fn interactive_dismiss_disabled(self) -> Self {
self.push(op_interactive_dismiss_disabled())
}
pub fn status_bar_hidden<M>(self, hidden: impl IntoReactive<bool, M>) -> Self {
self.push(op_status_bar_hidden(hidden.into_reactive()))
}
pub fn any(self) -> AnyPiece {
AnyPiece::new(self)
}
}
pub enum ContextMenuSource {
Fixed(Vec<MenuEntry>),
OnSummon(Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>),
}
pub trait ContextMenuContent<M> {
fn into_context_menu(self) -> ContextMenuSource;
}
pub struct AtPointMark;
impl ContextMenuContent<crate::ManyMark> for Vec<MenuEntry> {
fn into_context_menu(self) -> ContextMenuSource {
ContextMenuSource::Fixed(self)
}
}
impl<const N: usize> ContextMenuContent<crate::ManyMark> for [MenuEntry; N] {
fn into_context_menu(self) -> ContextMenuSource {
ContextMenuSource::Fixed(Vec::from(self))
}
}
impl<C, F> ContextMenuContent<crate::DerivedMark> for F
where
C: IntoIterator<Item = MenuEntry>,
F: Fn() -> C + 'static,
{
fn into_context_menu(self) -> ContextMenuSource {
ContextMenuSource::OnSummon(Rc::new(move |_| self().into_iter().collect()))
}
}
impl<C, F> ContextMenuContent<AtPointMark> for F
where
C: IntoIterator<Item = MenuEntry>,
F: Fn(day_spec::Point) -> C + 'static,
{
fn into_context_menu(self) -> ContextMenuSource {
ContextMenuSource::OnSummon(Rc::new(move |p| self(p).into_iter().collect()))
}
}
pub trait Decorate: Piece + Sized {
fn id(self, id: day_core::ElementId) -> Decorated<Self> {
Decorated::new(self).id(id)
}
fn wrapper_id(self, id: day_core::ElementId) -> Decorated<Self> {
Decorated::new(self).wrapper_id(id)
}
fn apply_if(
self,
cond: bool,
f: impl FnOnce(Decorated<Self>) -> Decorated<Self>,
) -> Decorated<Self> {
Decorated::new(self).apply_if(cond, f)
}
fn apply_some<T>(
self,
value: Option<T>,
f: impl FnOnce(Decorated<Self>, T) -> Decorated<Self>,
) -> Decorated<Self> {
Decorated::new(self).apply_some(value, f)
}
fn id_of(self, id: impl Fn() -> day_core::ElementId + 'static) -> Decorated<Self> {
Decorated::new(self).id_of(id)
}
fn tweak(self, f: impl FnOnce(day_core::RNode) + 'static) -> Decorated<Self> {
Decorated::new(self).tweak(f)
}
fn selectable(self) -> Decorated<Self> {
Decorated::new(self).selectable()
}
fn cursor<M>(self, cursor: impl IntoReactive<Cursor, M>) -> Decorated<Self> {
Decorated::new(self).cursor(cursor)
}
fn window_drag_region(self) -> Decorated<Self> {
Decorated::new(self).window_drag_region()
}
fn native_ref(self, r: &NativeRef) -> Decorated<Self> {
Decorated::new(self).native_ref(r)
}
fn padding(self, insets: impl IntoInsets) -> Decorated<Self> {
Decorated::new(self).padding(insets)
}
fn max_width(self, max: f64) -> Decorated<Self> {
Decorated::new(self).max_width(max)
}
fn max_height(self, max: f64) -> Decorated<Self> {
Decorated::new(self).max_height(max)
}
fn min_width(self, min: f64) -> Decorated<Self> {
Decorated::new(self).min_width(min)
}
fn reserving(self, sample: impl Into<String>) -> Decorated<Self> {
Decorated::new(self).reserving(sample)
}
fn frame(self, width: f64, height: f64) -> Decorated<Self> {
Decorated::new(self).frame(width, height)
}
fn width(self, width: f64) -> Decorated<Self> {
Decorated::new(self).width(width)
}
fn height(self, height: f64) -> Decorated<Self> {
Decorated::new(self).height(height)
}
fn a11y(self, f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> Decorated<Self> {
Decorated::new(self).a11y(f)
}
fn on_tap(self, f: impl Fn() + 'static) -> Decorated<Self> {
Decorated::new(self).on_tap(f)
}
fn on_tap_at(self, f: impl Fn(day_spec::Point) + 'static) -> Decorated<Self> {
Decorated::new(self).on_tap_at(f)
}
fn focused<M>(self, binding: impl IntoFocusBinding<M>) -> Decorated<Self> {
Decorated::new(self).focused(binding)
}
fn on_key(self, f: impl Fn(&day_spec::KeyEvent) + 'static) -> Decorated<Self> {
Decorated::new(self).on_key(f)
}
fn toolbar<M>(self, content: impl crate::ToolbarContent<M>) -> Decorated<Self> {
Decorated::new(self).toolbar(content)
}
fn searchable(self, search: crate::Search) -> Decorated<Self> {
Decorated::new(self).searchable(search)
}
fn focusable(self) -> Decorated<Self> {
Decorated::new(self).focusable()
}
fn drag_source(
self,
source: impl Fn(day_spec::Point) -> Option<day_spec::transfer::Offer> + 'static,
) -> Decorated<Self> {
Decorated::new(self).drag_source(source)
}
fn drop_target(self, target: day_spec::transfer::Target) -> Decorated<Self> {
Decorated::new(self).drop_target(target)
}
fn context_menu<M>(self, menu: impl ContextMenuContent<M>) -> Decorated<Self> {
Decorated::new(self).context_menu(menu)
}
fn on_drag(self, f: impl Fn(Drag) + 'static) -> Decorated<Self> {
Decorated::new(self).on_drag(f)
}
fn on_pinch(self, f: impl Fn(Pinch) + 'static) -> Decorated<Self> {
Decorated::new(self).on_pinch(f)
}
fn on_pan(self, f: impl Fn(Pan) + 'static) -> Decorated<Self> {
Decorated::new(self).on_pan(f)
}
fn on_hover(self, f: impl Fn(Option<day_spec::Point>) + 'static) -> Decorated<Self> {
Decorated::new(self).on_hover(f)
}
fn card(self) -> Decorated<Self> {
Decorated::new(self).card()
}
fn background<M>(self, color: impl IntoReactive<Color, M>) -> Decorated<Self> {
Decorated::new(self).background(color)
}
fn corner_radius(self, radius: f64) -> Decorated<Self> {
Decorated::new(self).corner_radius(radius)
}
fn opacity<M>(self, opacity: impl IntoReactive<f64, M>) -> Decorated<Self> {
Decorated::new(self).opacity(opacity)
}
fn transform<M>(self, t: impl IntoReactive<Transform, M>) -> Decorated<Self> {
Decorated::new(self).transform(t)
}
fn scale<M>(self, factor: impl IntoReactive<f64, M>) -> Decorated<Self> {
Decorated::new(self).scale(factor)
}
fn rotation<M>(self, degrees: impl IntoReactive<f64, M>) -> Decorated<Self> {
Decorated::new(self).rotation(degrees)
}
fn translation<Mx, My>(
self,
x: impl IntoReactive<f64, Mx>,
y: impl IntoReactive<f64, My>,
) -> Decorated<Self> {
Decorated::new(self).translation(x, y)
}
fn animation(self, anim: AnimSpec) -> Decorated<Self> {
Decorated::new(self).animation(anim)
}
fn modifier(self, m: impl Modifier) -> AnyPiece {
m.apply(self.any())
}
fn overlay(self, over: impl Piece) -> Decorated<Self> {
Decorated::new(self).overlay(over)
}
fn overlay_aligned(self, align: Alignment, over: impl Piece) -> Decorated<Self> {
Decorated::new(self).overlay_aligned(align, over)
}
fn aspect_ratio(self, ratio: f64) -> Decorated<Self> {
Decorated::new(self).aspect_ratio(ratio)
}
fn grow(self) -> Decorated<Self> {
Decorated::new(self).grow()
}
fn grow_w(self) -> Decorated<Self> {
Decorated::new(self).grow_w()
}
fn grow_h(self) -> Decorated<Self> {
Decorated::new(self).grow_h()
}
#[doc(hidden)]
fn grid_span(self, n: usize) -> Decorated<Self> {
Decorated::new(self).grid_span(n)
}
fn grid_align(self, a: Alignment) -> Decorated<Self> {
Decorated::new(self).grid_align(a)
}
fn defers_system_gestures(self, edges: day_spec::Edges) -> Decorated<Self> {
Decorated::new(self).defers_system_gestures(edges)
}
fn status_bar_hidden<M>(self, hidden: impl IntoReactive<bool, M>) -> Decorated<Self> {
Decorated::new(self).status_bar_hidden(hidden)
}
fn interactive_dismiss_disabled(self) -> Decorated<Self> {
Decorated::new(self).interactive_dismiss_disabled()
}
fn any(self) -> AnyPiece {
AnyPiece::new(self)
}
}
impl<P: Piece> Decorate for P {}
type LiveA11yString = (TextSource, fn(String) -> A11yProps);
#[derive(Default)]
pub struct A11yBuilder {
label: Option<TextSource>,
hint: Option<TextSource>,
value: Option<TextSource>,
selected: Option<Reactive<bool>>,
props: A11yProps,
}
impl A11yBuilder {
pub fn label<M>(mut self, s: impl IntoText<M>) -> Self {
self.label = Some(s.into_text());
self
}
pub fn hint<M>(mut self, s: impl IntoText<M>) -> Self {
self.hint = Some(s.into_text());
self
}
pub fn value<M>(mut self, s: impl IntoText<M>) -> Self {
self.value = Some(s.into_text());
self
}
pub fn role(mut self, r: Role) -> Self {
self.props.role = r;
self
}
pub fn selected<M>(mut self, on: impl IntoReactive<bool, M>) -> Self {
self.selected = Some(on.into_reactive());
self
}
pub fn hidden(mut self) -> Self {
self.props.hidden = true;
self
}
pub fn decorative(mut self) -> Self {
self.props.decorative = true;
self.props.hidden = true;
self
}
fn split(self) -> (A11yProps, Vec<LiveA11yString>, Option<Reactive<bool>>) {
let mut props = self.props;
let mut live = Vec::new();
let selected = match self.selected {
Some(Reactive::Const(on)) => {
props.selected = Some(on);
None
}
Some(dynamic @ Reactive::Dyn(_)) => {
props.selected = Some(day_reactive::untrack(|| dynamic.get()));
Some(dynamic)
}
None => None,
};
let mut take =
|src: Option<TextSource>, slot: &mut Option<String>, patch: fn(String) -> A11yProps| {
if let Some(src) = src {
*slot = Some(src.initial());
if matches!(src, TextSource::Dyn(_)) {
live.push((src, patch));
}
}
};
take(self.label, &mut props.label, |label| A11yProps {
label: Some(label),
..Default::default()
});
take(self.hint, &mut props.hint, |hint| A11yProps {
hint: Some(hint),
..Default::default()
});
take(self.value, &mut props.value, |value| A11yProps {
value: Some(value),
..Default::default()
});
(props, live, selected)
}
}
fn scoped_drag_source(source: day_spec::transfer::Source) -> day_spec::transfer::Source {
let scope = Scope::current();
let alive = Rc::new(Cell::new(true));
let cleanup = alive.clone();
scope.on_cleanup(move || cleanup.set(false));
Rc::new(move |point| alive.get().then(|| scope.enter(|| source(point))).flatten())
}
fn scoped_drop_target(target: day_spec::transfer::Target) -> day_spec::transfer::Target {
let scope = Scope::current();
let alive = Rc::new(Cell::new(true));
let cleanup = alive.clone();
scope.on_cleanup(move || cleanup.set(false));
let accept_alive = alive.clone();
day_spec::transfer::Target {
types: target.types,
accept: Rc::new(move |location| {
if accept_alive.get() {
scope.enter(|| (target.accept)(location))
} else {
day_spec::transfer::Operation::None
}
}),
receive: Rc::new(move |drop| alive.get() && scope.enter(|| (target.receive)(drop))),
}
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use day_core::AnyPiece;
use day_core::conformance::{Case, Drive, FrameExpect};
use day_reactive::Signal;
use day_spec::{Cap, Color, DragPhase, Transform};
use crate::*;
fn size(width: f64, height: f64) -> FrameExpect {
FrameExpect {
width: Some(width),
height: Some(height),
..Default::default()
}
}
#[day_macros::test(day_core)]
fn padding_insets() -> Case {
Case::new()
.proves_modifier("padding")
.proves_modifier("wrapper_id")
.page(|| {
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.id(day_core::id!(inner))
.frame(40.0, 20.0)
.padding(10.0)
.wrapper_id(day_core::id!(outer))
})
.drive(|d: Drive| async move {
d.assert_frame("outer", size(60.0, 40.0)).await?;
d.assert_frame(
"inner",
FrameExpect {
x: Some(10.0),
y: Some(10.0),
relative_to: Some("outer".into()),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn min_max_width() -> Case {
Case::new()
.proves_modifier("min_width")
.proves_modifier("max_width")
.page(|| {
column((
label("Hi").min_width(120.0).wrapper_id(day_core::id!(min)),
label("A label long enough that it would run past eighty points")
.max_width(80.0)
.wrapper_id(day_core::id!(max)),
))
.align(HAlign::Leading)
})
.drive(|d: Drive| async move {
d.assert_frame(
"min",
FrameExpect {
width: Some(120.0),
..Default::default()
},
)
.await?;
d.assert_frame(
"max",
FrameExpect {
width: Some(80.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn max_height_caps_a_scroll() -> Case {
Case::new()
.proves_modifier("max_height")
.page(|| {
column((
scroll(column((
label("One"),
label("Two"),
label("Three"),
label("Four"),
label("Five"),
label("Six"),
)))
.max_height(60.0)
.wrapper_id(day_core::id!(capped)),
spacer(),
))
.align(HAlign::Leading)
})
.drive(|d: Drive| async move {
d.assert_frame(
"capped",
FrameExpect {
height: Some(60.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn aspect_ratio_height() -> Case {
Case::new()
.proves_modifier("aspect_ratio")
.page(|| {
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.aspect_ratio(2.0)
.id(day_core::id!(shape))
.width(100.0)
})
.drive(|d: Drive| async move { d.assert_frame("shape", size(100.0, 50.0)).await })
}
#[day_macros::test(day_core)]
fn grow_width() -> Case {
Case::new()
.proves_modifier("grow_w")
.page(|| {
row((
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.frame(40.0, 20.0),
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.height(20.0)
.grow_w()
.id(day_core::id!(grower)),
))
.spacing(0.0)
.width(200.0)
})
.drive(|d: Drive| async move {
d.assert_frame(
"grower",
FrameExpect {
width: Some(160.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn overlay_aligned_corner() -> Case {
Case::new()
.proves_modifier("overlay_aligned")
.requires(Cap::Snapshot)
.page(|| {
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.frame(100.0, 60.0)
.overlay_aligned(
day_core::Alignment::BottomTrailing,
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.frame(20.0, 20.0),
)
.id(day_core::id!(base))
})
.drive(|d: Drive| async move {
d.sample_pixel("base", 0.95, 0.9, "#0000ff").await?;
d.sample_pixel("base", 0.1, 0.1, "#ff0000").await
})
}
#[day_macros::test(day_core)]
fn background_fill() -> Case {
Case::new()
.proves_modifier("background")
.requires(Cap::Snapshot)
.page(|| {
label("")
.frame(80.0, 40.0)
.background(Color::rgb(1.0, 0.0, 0.0))
.id(day_core::id!(filled))
})
.drive(|d: Drive| async move { d.sample_pixel("filled", 0.5, 0.5, "#ff0000").await })
}
#[day_macros::test(day_core)]
fn corner_radius_cuts() -> Case {
Case::new()
.proves_modifier("corner_radius")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
label("")
.frame(80.0, 80.0)
.background(Color::rgb(0.0, 0.0, 1.0))
.corner_radius(30.0),
))
.id(day_core::id!(ground))
.frame(80.0, 80.0)
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.03, 0.03, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn opacity_hides() -> Case {
Case::new()
.proves_duty("set_opacity")
.proves_modifier("opacity")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
rectangle().fill(Color::rgb(1.0, 0.0, 0.0)).opacity(0.0),
))
.id(day_core::id!(ground))
.frame(80.0, 40.0)
})
.drive(|d: Drive| async move { d.sample_pixel("ground", 0.5, 0.5, "#ffffff").await })
}
#[day_macros::test(day_core)]
fn translation_moves_drawing() -> Case {
Case::new()
.proves_modifier("translation")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.frame(40.0, 40.0)
.translation(40.0, 0.0),
))
.align(day_core::Alignment::Leading)
.id(day_core::id!(ground))
.frame(120.0, 40.0)
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn translation_moves_view() -> Case {
Case::new()
.proves_modifier("translation")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
label("")
.frame(40.0, 40.0)
.background(Color::rgb(0.0, 0.0, 1.0))
.translation(40.0, 0.0),
))
.align(day_core::Alignment::Leading)
.id(day_core::id!(ground))
.frame(120.0, 40.0)
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn scale_grows_drawing() -> Case {
Case::new()
.proves_modifier("scale")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
label("")
.frame(40.0, 40.0)
.background(Color::rgb(0.0, 0.0, 1.0))
.scale(2.0),
))
.id(day_core::id!(ground))
.frame(120.0, 80.0)
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.25, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.08, 0.5, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn rotation_turns_clockwise() -> Case {
Case::new()
.proves_modifier("rotation")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
shape_group([
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.at(0.0, 0.0, 0.5, 1.0),
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.at(0.5, 0.0, 0.5, 1.0),
])
.frame(80.0, 40.0)
.rotation(90.0),
))
.id(day_core::id!(ground))
.frame(120.0, 120.0)
})
.shot("default")
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.5, 0.4, "#ff0000").await?;
d.sample_pixel("ground", 0.5, 0.6, "#0000ff").await
})
}
fn pad(id: day_core::ElementId) -> Decorated<ShapePiece> {
rectangle()
.fill(Color::rgb(0.2, 0.4, 0.9))
.frame(120.0, 80.0)
.id(id)
}
#[day_macros::test(day_core)]
fn on_tap_runs() -> Case {
Case::new()
.proves_modifier("on_tap")
.page(|| {
let taps = Signal::new(0i64);
column((
label(move || format!("taps {}", taps.get())).id(day_core::id!(taps)),
label("Tap me")
.padding(12.0)
.on_tap(move || taps.update(|n| *n += 1))
.id(day_core::id!(target)),
))
})
.drive(|d: Drive| async move {
d.tap("target").await?;
d.tap("target").await?;
d.assert_text("taps", "taps 2").await
})
}
#[day_macros::test(day_core)]
fn on_tap_at_reports_point() -> Case {
Case::new()
.proves_modifier("on_tap_at")
.page(|| {
let at = Signal::new(String::from("none"));
column((
label(move || format!("at {}", at.get())).id(day_core::id!(at)),
rectangle()
.fill(Color::rgb(0.2, 0.4, 0.9))
.frame(120.0, 80.0)
.on_tap_at(move |p| at.set(format!("{:.0},{:.0}", p.x, p.y)))
.id(day_core::id!(target)),
))
})
.drive(|d: Drive| async move {
d.tap_at("target", 30.0, 20.0).await?;
d.assert_text("at", "at 30,20").await
})
}
#[day_macros::test(day_core)]
fn on_drag_reports_translation() -> Case {
Case::new()
.proves_duty("enable_gesture")
.proves_modifier("on_drag")
.page(|| {
let state = Signal::new(String::from("idle"));
column((
label(move || state.get()).id(day_core::id!(state)),
pad(day_core::id!(target)).on_drag(move |d: Drag| {
state.set(match d.phase {
DragPhase::Began => "began".into(),
DragPhase::Changed => "moving".into(),
DragPhase::Ended => {
format!("moved {:.0},{:.0}", d.translation.x, d.translation.y)
}
})
}),
))
})
.drive(|d: Drive| async move {
d.drag("target", (10.0, 10.0), (50.0, 30.0)).await?;
d.assert_text("state", "moved 40,20").await
})
}
#[day_macros::test(day_core)]
fn on_hover_enters_and_leaves() -> Case {
Case::new()
.proves_modifier("on_hover")
.page(|| {
let state = Signal::new(String::from("out"));
column((
label(move || state.get()).id(day_core::id!(state)),
pad(day_core::id!(target)).on_hover(move |p| {
state.set(match p {
Some(p) => format!("over {:.0},{:.0}", p.x, p.y),
None => "out".into(),
})
}),
))
})
.drive(|d: Drive| async move {
d.hover("target", Some((5.0, 6.0))).await?;
d.assert_text("state", "over 5,6").await?;
d.hover_leave("target").await?;
d.assert_text("state", "out").await
})
}
#[day_macros::test(day_core)]
fn on_pan_adds_deltas() -> Case {
Case::new()
.proves_modifier("on_pan")
.page(|| {
let total = Signal::new((0.0f64, 0.0f64));
column((
label(move || {
let (x, y) = total.get();
format!("pan {x:.0},{y:.0}")
})
.id(day_core::id!(total)),
pad(day_core::id!(target)).on_pan(move |p: Pan| {
total.update(|(x, y)| {
*x += p.delta.x;
*y += p.delta.y;
})
}),
))
})
.drive(|d: Drive| async move {
d.pan("target", 40.0, -20.0).await?;
d.assert_text("total", "pan 40,-20").await
})
}
#[day_macros::test(day_core)]
fn on_pinch_reports_scale() -> Case {
Case::new()
.proves_modifier("on_pinch")
.page(|| {
let scale = Signal::new(String::from("none"));
column((
label(move || format!("scale {}", scale.get())).id(day_core::id!(scale)),
pad(day_core::id!(target)).on_pinch(move |p: Pinch| {
if p.phase == DragPhase::Ended {
scale.set(format!("{:.1}", p.scale));
}
}),
))
})
.drive(|d: Drive| async move {
d.pinch("target", 2.5).await?;
d.assert_text("scale", "scale 2.5").await
})
}
#[day_macros::test(day_core)]
fn on_key_reaches_focused() -> Case {
Case::new()
.proves_duty("focus")
.proves_duty("set_focusable")
.proves_modifier("on_key")
.proves_modifier("focusable")
.page(|| {
let last = Signal::new(String::from("none"));
column((
label(move || format!("key {}", last.get())).id(day_core::id!(last)),
pad(day_core::id!(target))
.focusable()
.on_key(move |k| last.set(k.key.clone())),
))
})
.drive(|d: Drive| async move {
d.focus("target").await?;
d.assert_focused("target", true).await?;
d.key(None, "ArrowRight").await?;
d.assert_text("last", "key ArrowRight").await
})
}
#[day_macros::test(day_core)]
fn focused_binding_moves_focus() -> Case {
Case::new()
.proves_modifier("focused")
.page(|| {
let want = Signal::new(false);
column((
label(move || format!("focused {}", want.get())).id(day_core::id!(state)),
button("Focus")
.action(move || want.set(true))
.id(day_core::id!(go)),
text_field(Signal::new(String::new()))
.focused(want)
.id(day_core::id!(field)),
))
})
.drive(|d: Drive| async move {
d.assert_focused("field", false).await?;
d.tap("go").await?;
d.assert_focused("field", true).await?;
d.assert_text("state", "focused true").await
})
}
#[day_macros::test(day_core)]
fn focused_inputs_survive_page_removal() -> Case {
Case::new()
.proves_duty("focus")
.page(|| {
let shown = Signal::new(true);
column((
button("Toggle page")
.action(move || shown.set(!shown.get()))
.id(day_core::id!(toggle)),
text_field(Signal::new(String::new())).id(day_core::id!(survivor)),
when(
move || shown.get(),
|| {
column((
text_field(Signal::new(String::new())).id(day_core::id!(entry)),
))
.background(Color::rgb(0.5, 0.5, 0.5))
},
),
))
})
.drive(|d: Drive| async move {
for _ in 0..4 {
d.focus("entry").await?;
d.assert_focused("entry", true).await?;
d.tap("toggle").await?;
d.assert_missing("entry").await?;
d.wait_idle().await?;
d.tap("toggle").await?;
}
d.focus("survivor").await?;
d.tap("toggle").await?;
d.assert_focused("survivor", true).await?;
d.input("survivor", "still editable").await?;
d.assert_text("survivor", "still editable").await
})
}
#[day_macros::test(day_core)]
fn height_fixes_height() -> Case {
Case::new()
.proves_modifier("height")
.page(|| {
column((rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.id(day_core::id!(bar))
.height(37.0),))
.width(100.0)
})
.drive(|d: Drive| async move {
d.assert_frame(
"bar",
FrameExpect {
height: Some(37.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn grow_vertical_fills_column() -> Case {
Case::new()
.proves_modifier("grow_h")
.page(|| {
row((
column((
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.id(day_core::id!(top))
.frame(40.0, 20.0),
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.id(day_core::id!(rest))
.width(40.0)
.grow_h(),
))
.spacing(0.0)
.height(120.0),
rectangle()
.fill(Color::rgb(0.0, 1.0, 0.0))
.id(day_core::id!(both))
.width(30.0)
.grow_h(),
))
.spacing(0.0)
.align(VAlign::Top)
.height(120.0)
})
.drive(|d: Drive| async move {
d.assert_frame(
"rest",
FrameExpect {
height: Some(100.0),
..Default::default()
},
)
.await?;
d.assert_frame(
"both",
FrameExpect {
height: Some(120.0),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn grid_span_and_align() -> Case {
Case::new()
.proves_modifier("grid_span")
.proves_modifier("grid_align")
.page(|| {
let cell = |id: day_core::ElementId, w: f64| {
rectangle()
.fill(Color::rgb(0.2, 0.4, 0.9))
.id(id)
.frame(w, 20.0)
};
grid((
grid_row((cell(day_core::id!(a), 40.0), cell(day_core::id!(b), 60.0))),
grid_row((cell(day_core::id!(wide), 30.0)
.grid_align(day_core::Alignment::TopTrailing)
.grid_span(2),)),
))
.spacing(0.0)
.align(day_core::Alignment::TopLeading)
})
.drive(|d: Drive| async move {
d.assert_frame(
"wide",
FrameExpect {
x: Some(70.0),
relative_to: Some("a".into()),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn overlay_centers() -> Case {
Case::new()
.proves_modifier("overlay")
.requires(Cap::Snapshot)
.page(|| {
rectangle()
.fill(Color::rgb(1.0, 0.0, 0.0))
.frame(90.0, 90.0)
.overlay(
rectangle()
.fill(Color::rgb(0.0, 0.0, 1.0))
.frame(30.0, 30.0),
)
.id(day_core::id!(base))
})
.drive(|d: Drive| async move {
d.sample_pixel("base", 0.5, 0.5, "#0000ff").await?;
d.sample_pixel("base", 0.1, 0.1, "#ff0000").await
})
}
#[day_macros::test(day_core)]
fn transform_applies_whole() -> Case {
Case::new()
.proves_duty("set_transform")
.proves_modifier("transform")
.requires(Cap::Snapshot)
.page(|| {
zstack((
rectangle().fill(Color::WHITE),
label("")
.frame(20.0, 20.0)
.background(Color::rgb(0.0, 0.0, 1.0))
.transform(Transform {
tx: 30.0,
sx: 2.0,
sy: 2.0,
..Transform::default()
}),
))
.id(day_core::id!(ground))
.frame(120.0, 60.0)
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.75, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.5, 0.5, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn animation_lands_on_final() -> Case {
Case::new()
.proves_modifier("animation")
.requires(Cap::Snapshot)
.page(|| {
let moved = Signal::new(false);
column((
button("Move")
.action(move || moved.set(true))
.id(day_core::id!(move_button)),
zstack((
rectangle().fill(Color::WHITE),
label("")
.frame(40.0, 40.0)
.background(Color::rgb(0.0, 0.0, 1.0))
.translation(move || if moved.get() { 80.0 } else { 0.0 }, 0.0)
.animation(day_spec::AnimSpec::linear(150)),
))
.align(day_core::Alignment::Leading)
.id(day_core::id!(ground))
.frame(120.0, 40.0),
))
})
.drive(|d: Drive| async move {
d.sample_pixel("ground", 0.1, 0.5, "#0000ff").await?;
d.tap("move_button").await?;
d.wait_idle().await?;
d.pause(0.4).await?;
d.sample_pixel("ground", 0.85, 0.5, "#0000ff").await?;
d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
})
}
#[day_macros::test(day_core)]
fn ids_name_elements() -> Case {
Case::new()
.proves_modifier("id")
.proves_modifier("id_of")
.page(|| {
let n = Signal::new(1i64);
column((
label("Fixed").id(day_core::id!(fixed)),
label("Derived").id_of(move || day_core::id!(derived, n.get())),
label("Keyed").id(day_core::id!(keyed, 7)),
button("Next")
.action(move || n.set(2))
.id(day_core::id!(next)),
))
})
.drive(|d: Drive| async move {
d.assert_text("fixed", "Fixed").await?;
d.assert_text("keyed:7", "Keyed").await?;
d.assert_text("derived:1", "Derived").await?;
d.tap("next").await?;
d.assert_missing("derived:1").await?;
d.assert_text("derived:2", "Derived").await
})
}
#[day_macros::test(day_core)]
fn tweak_and_native_ref_see_node() -> Case {
Case::new()
.proves_modifier("tweak")
.proves_modifier("native_ref")
.page(|| {
let tweaked = Signal::new(false);
let r = NativeRef::new();
let held = r.clone();
column((
label(move || format!("tweaked {}", tweaked.get())).id(day_core::id!(tweaked)),
label(move || format!("held {}", held.node().is_some()))
.id(day_core::id!(held)),
label("Target")
.tweak(move |_node| tweaked.set(true))
.native_ref(&r),
))
})
.drive(|d: Drive| async move {
d.assert_text("tweaked", "tweaked true").await?;
d.assert_text("held", "held true").await
})
}
#[day_macros::test(day_core)]
fn modifier_applies() -> Case {
Case::new()
.proves_modifier("modifier")
.page(|| {
let padded = |content: AnyPiece| content.padding(10.0).any();
column((label("Inner")
.id(day_core::id!(inner))
.modifier(padded)
.id(day_core::id!(outer)),))
.align(HAlign::Leading)
})
.drive(|d: Drive| async move {
d.assert_frame(
"inner",
FrameExpect {
x: Some(10.0),
y: Some(10.0),
relative_to: Some("outer".into()),
..Default::default()
},
)
.await
})
}
#[day_macros::test(day_core)]
fn announce_reaches_reader() -> Case {
Case::new()
.proves_duty("announce")
.proves_cap(Cap::Announce)
.page(|| {
button("Save")
.action(|| day_core::announce("Saved"))
.id(day_core::id!(save))
})
.drive(|d: Drive| async move {
d.tap("save").await?;
d.assert_announced("Saved").await
})
}
}
#[cfg(test)]
mod transfer_tests {
use super::*;
use day_spec::transfer::*;
#[test]
fn transfer_callbacks_restore_owner_scope_and_reject_after_disposal() {
let owner = Scope::child();
let foreign = Scope::child();
foreign.provide(22u32);
let (source, target) = owner.enter(|| {
owner.provide(11u32);
let check = || assert_eq!(day_core::environment::<u32>(), Some(11));
(
scoped_drag_source(Rc::new(move |_| {
check();
Some(Offer::default())
})),
scoped_drop_target(Target {
types: vec![],
accept: Rc::new(move |_| {
check();
Operation::Copy
}),
receive: Rc::new(move |_| {
check();
true
}),
}),
)
});
let location = || Location {
position: day_spec::Point::new(40., 20.),
types: vec![],
allowed: vec![Operation::Copy],
local: true,
};
foreign.enter(|| {
assert!(source(day_spec::Point::new(0., 0.)).is_some());
assert!(target.deliver(location(), Offer::default()));
assert_eq!(day_core::environment::<u32>(), Some(22));
owner.dispose();
assert!(source(day_spec::Point::new(0., 0.)).is_none());
assert_eq!(target.proposal(&location()), Operation::None);
assert!(!(target.receive)(Drop {
local: true,
position: day_spec::Point::new(0., 0.),
operation: Operation::Copy,
items: vec![]
}));
});
foreign.dispose();
}
}