1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use oxframe_core::{BlendMode, Color, Gradient, Signal, Tween, Vec2, all};
5
6use crate::builder::{
7 self, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder, LayoutBuilder,
8 LineBuilder, NodeBuilder, PathBuilder, PolygonBuilder, RectBuilder, StarBuilder, TxtBuilder,
9};
10use crate::draw::{LineCap, LineJoin};
11use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
12use crate::place::{Edge, Place, SafeArea, Side};
13use crate::text::RichSpan;
14
15pub const SLIDE_DISTANCE: f64 = 160.0;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Node(NodeId);
21
22macro_rules! base_signal {
23 ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
24 $(
25 $(#[$doc])*
26 #[doc = ""]
27 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
28 pub fn $field(self) -> Signal<$ty> {
29 with_graph(|g| g.node(self.0).$field.clone())
30 }
31 )*
32 };
33}
34
35impl Node {
36 pub fn id(self) -> NodeId {
38 self.0
39 }
40
41 pub fn from_id(id: NodeId) -> Self {
46 Self(id)
47 }
48
49 base_signal! {
50 x: f64,
51 y: f64,
52 rotation: f64,
54 scale: Vec2,
55 opacity: f64,
56 fill: Color,
57 stroke: Color,
58 line_width: f64,
59 start: f64,
61 end: f64,
63 fill_gradient: Option<Gradient>,
64 stroke_gradient: Option<Gradient>,
65 dash: Vec<f64>,
66 dash_offset: f64,
67 line_cap: LineCap,
68 line_join: LineJoin,
69 blur: f64,
70 shadow_color: Color,
71 shadow_blur: f64,
72 shadow_offset: Vec2,
73 clip: bool,
74 blend: BlendMode,
75 z_index: f64,
76 anchor: Vec2,
77 grow: f64,
78 shrink: f64,
79 basis: Option<f64>,
80 margin: f64,
81 align_self: Option<crate::LayoutAlign>,
82 }
83
84 #[allow(clippy::should_implement_trait)] #[track_caller]
90 pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
91 let loc = std::panic::Location::caller();
92 let prev = crate::graph::push_add_at(loc.file(), loc.line());
93 let handle = builder::add(self.0, child);
94 crate::graph::pop_add_at(prev);
95 handle
96 }
97
98 pub fn remove(self) {
100 with_graph(|g| g.detach(self.0));
101 }
102
103 pub fn fade_in(self) -> Tween<f64> {
106 let motion = crate::theme::current().motion;
107 self.opacity().set(0.0);
108 self.opacity().to(1.0, motion.normal).ease(motion.enter)
109 }
110
111 pub fn pop_in(self) -> Tween<Vec2> {
114 let motion = crate::theme::current().motion;
115 let full = self.scale().peek();
116 self.scale().set(Vec2::ZERO);
117 self.scale().to(full, motion.normal).ease(motion.emphasis)
118 }
119
120 pub fn draw_on(self) -> Tween<f64> {
124 let motion = crate::theme::current().motion;
125 self.end().set(0.0);
126 self.end().to(1.0, motion.slow).ease(motion.enter)
127 }
128
129 pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
132 let motion = crate::theme::current().motion;
133 let (axis, offset) = match from {
134 Side::Left => (self.x(), -SLIDE_DISTANCE),
135 Side::Right => (self.x(), SLIDE_DISTANCE),
136 Side::Top => (self.y(), -SLIDE_DISTANCE),
137 Side::Bottom => (self.y(), SLIDE_DISTANCE),
138 };
139 let home = axis.peek();
140 axis.set(home + offset);
141 let moves = all((
142 axis.to(home, motion.normal).ease(motion.enter),
143 self.fade_in(),
144 ));
145 async move {
146 moves.await;
147 }
148 }
149
150 pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
153 let other = other.into();
154 with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
155 self
156 }
157
158 pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
160 let other = other.into();
161 with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
162 self
163 }
164
165 pub fn to_edge(self, edge: Edge) -> Self {
167 with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
168 self
169 }
170
171 pub fn center_in(self, other: impl Into<Node>) -> Self {
173 let other = other.into();
174 with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
175 self
176 }
177
178 pub fn fade_out(self) -> Tween<f64> {
180 let motion = crate::theme::current().motion;
181 self.opacity().to(0.0, motion.normal).ease(motion.exit)
182 }
183
184 pub fn reparent(self, parent: impl Into<Node>) {
186 let parent = parent.into();
187 with_graph(|g| g.reparent(self.0, parent.0));
188 }
189
190 pub fn children(self) -> Vec<Node> {
192 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
193 }
194
195 pub fn size(self) -> (f64, f64) {
197 with_graph(|g| g.measure(self.0))
198 }
199
200 pub fn absolute_position(self) -> Vec2 {
202 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
203 }
204}
205
206macro_rules! typed_handle {
207 ($name:ident, $builder:ident) => {
208 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
209 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
210 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211 pub struct $name(Node);
212
213 impl $name {
214 #[allow(clippy::new_ret_no_self)]
216 pub fn new() -> $builder {
217 $builder::default()
218 }
219
220 pub(crate) fn from_id(id: NodeId) -> Self {
221 Self(Node(id))
222 }
223
224 pub fn node(self) -> Node {
226 self.0
227 }
228 }
229
230 impl Deref for $name {
231 type Target = Node;
232 fn deref(&self) -> &Node {
233 &self.0
234 }
235 }
236
237 impl From<$name> for Node {
238 fn from(h: $name) -> Node {
239 h.0
240 }
241 }
242 };
243}
244
245typed_handle!(Group, GroupBuilder);
246typed_handle!(Rect, RectBuilder);
247typed_handle!(Circle, CircleBuilder);
248typed_handle!(Path, PathBuilder);
249typed_handle!(Line, LineBuilder);
250typed_handle!(Txt, TxtBuilder);
251typed_handle!(Img, ImgBuilder);
252typed_handle!(Layout, LayoutBuilder);
253typed_handle!(Grid, GridBuilder);
254typed_handle!(Polygon, PolygonBuilder);
255typed_handle!(Star, StarBuilder);
256typed_handle!(Code, CodeBuilder);
257
258macro_rules! kind_signal {
259 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
260 impl $handle {
261 $(
262 $(#[$doc])*
263 #[doc = ""]
264 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
265 pub fn $field(self) -> Signal<$ty> {
266 with_graph(|g| match &g.node(self.id()).kind {
267 NodeKind::$kind { $field, .. } => $field.clone(),
268 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
269 })
270 }
271 )*
272 }
273 };
274}
275
276kind_signal!(Rect::Rect {
277 width: f64,
278 height: f64,
279 radius: f64
280});
281kind_signal!(Circle::Circle {
282 size: f64,
284 start_angle: f64,
285 end_angle: f64,
286});
287kind_signal!(Polygon::Polygon {
288 sides: f64,
289 size: f64
290});
291kind_signal!(Star::Star {
292 points: f64,
293 size: f64,
294 inner: f64
295});
296kind_signal!(Path::Path { data: BezPath });
297kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
298kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
299kind_signal!(Code::Code {
300 code: String,
301 font_size: f64,
302 focus: Vec2,
304 dim: f64,
306});
307
308impl Txt {
309 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
312 let to = text.into();
313 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
314 NodeKind::Txt {
315 text,
316 morph,
317 morph_t,
318 ..
319 } => (text.clone(), morph.clone(), morph_t.clone()),
320 _ => unreachable!("handle does not point to a Txt"),
321 });
322 Box::pin(async move {
323 morph.set(Some((txt.get(), to.clone())));
324 t.set(0.0);
325 t.to(1.0, duration).await;
326 txt.set(to);
327 morph.set(None);
328 })
329 }
330}
331
332impl Code {
333 pub fn focus_lines(
335 self,
336 first: usize,
337 last: usize,
338 duration: f64,
339 ) -> oxframe_core::Tween<Vec2> {
340 self.focus()
341 .to(Vec2::new(first as f64, last as f64), duration)
342 }
343}
344kind_signal!(Img::Img {
345 width: f64,
346 height: f64
347});
348kind_signal!(Layout::Layout {
349 gap: f64,
350 padding: f64,
351 radius: f64,
352 width: Option<f64>,
354 height: Option<f64>,
356});
357kind_signal!(Grid::Grid {
358 width: f64,
359 height: f64,
360 spacing: f64
361});
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub struct View(Node);
366
367impl View {
368 pub fn current() -> Self {
370 Self(Node(SceneGraph::ROOT))
371 }
372
373 pub fn size(self) -> (u32, u32) {
375 with_graph(|g| g.size())
376 }
377
378 pub fn is_portrait(self) -> bool {
380 let (w, h) = self.size();
381 h > w
382 }
383
384 pub fn safe_area(self) -> SafeArea {
388 let (w, h) = self.size();
389 SafeArea::for_frame(f64::from(w), f64::from(h))
390 }
391}
392
393impl Deref for View {
394 type Target = Node;
395 fn deref(&self) -> &Node {
396 &self.0
397 }
398}
399
400impl From<View> for Node {
401 fn from(v: View) -> Node {
402 v.0
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use crate::Primitive;
410
411 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
412 let graph = SceneGraph::new(400, 400);
413 graph.enter(|| f(View::current()));
414 graph
415 }
416
417 fn play(
419 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
420 ) -> u32 {
421 let graph = SceneGraph::new(400, 400);
422 graph.enter(|| {
423 let fut = scene_fn(View::current());
424 let mut ex =
425 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
426 let mut frames = 0;
427 while !ex.step() {
428 frames += 1;
429 assert!(frames < 10_000, "scene did not finish");
430 }
431 frames
432 })
433 }
434
435 #[test]
436 fn entrance_helpers_start_at_once_and_end_in_place() {
437 let normal = crate::theme::current().motion.normal;
438 let expected = (normal * 60.0).round() as u32;
439 let frames = play(|view| {
440 let r = view.add(Rect::new().size(100.0).x(50.0));
441 let pop = r.pop_in();
442 assert_eq!(r.scale().get(), Vec2::ZERO);
445 let slide = r.slide_in(Side::Left);
446 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
447 assert_eq!(r.opacity().get(), 0.0);
448 let draw = r.draw_on();
449 assert_eq!(r.end().get(), 0.0);
450 Box::pin(async move {
451 all((pop, slide)).await;
452 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
453 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
454 draw.await;
455 assert_eq!(r.end().get(), 1.0);
456 r.fade_out().duration(0.5).await;
457 assert_eq!(r.opacity().get(), 0.0);
458 })
459 });
460 let slow = crate::theme::current().motion.slow;
461 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
462 }
463
464 #[test]
465 fn add_records_its_call_site_and_scene_bounds() {
466 let g = scene(|view| {
467 view.add(Rect::new().size(100.0).name("box"));
468 view.add(Rect::new().size(10.0).x(100.0));
469 });
470 let nodes = g.inspect_nodes();
471 let named = nodes.iter().find(|n| n.name == "box").unwrap();
472 assert_eq!(named.kind, "Rect");
473 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
474 assert!(
475 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
476 "{:?}",
477 named.bounds
478 );
479 let auto = nodes
480 .iter()
481 .find(|n| n.name != "box" && n.name != "view")
482 .unwrap();
483 assert_eq!(auto.name, auto.source.clone().unwrap());
484 }
485
486 #[test]
487 fn builders_set_initial_values_and_handles_return_signals() {
488 scene(|view| {
489 let r = view.add(
490 Rect::new()
491 .size(50.0)
492 .radius(5.0)
493 .x(-10.0)
494 .fill(Color::BLACK),
495 );
496 assert_eq!(r.width().get(), 50.0);
497 assert_eq!(r.height().get(), 50.0);
498 assert_eq!(r.radius().get(), 5.0);
499 assert_eq!(r.x().get(), -10.0);
500 assert_eq!(r.fill().get(), Color::BLACK);
501 r.x().set(3.0);
502 assert_eq!(r.x().get(), 3.0);
503 let c = r.add(Circle::new().size(10.0));
504 assert_eq!(r.children(), vec![c.node()]);
505 assert_eq!(view.children(), vec![r.node()]);
506 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
507 });
508 }
509
510 #[test]
511 fn draw_list_composes_transforms() {
512 let g = scene(|view| {
513 let grp = view.add(Group::new().x(10.0).scale(2.0));
514 grp.add(Circle::new().x(5.0));
515 });
516 let items = g.draw_list();
517 assert_eq!(items.len(), 1);
518 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
519 }
520
521 #[test]
522 fn layout_places_children_in_a_row() {
523 let g = scene(|view| {
524 view.add(
525 Layout::new()
526 .gap(10.0)
527 .padding(5.0)
528 .child(Rect::new().size(20.0))
529 .child(Rect::new().width(40.0).height(10.0)),
530 );
531 });
532 let layout = NodeId(1);
533 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
534 let centers: Vec<_> = g
535 .draw_list()
536 .iter()
537 .map(|i| i.transform * Point::ZERO)
538 .collect();
539 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
541 }
542
543 #[test]
544 fn column_with_fixed_size_and_alignment() {
545 let g = scene(|view| {
546 view.add(
547 Layout::new()
548 .column()
549 .height(100.0)
550 .width(100.0)
551 .justify(crate::Justify::Start)
552 .align(crate::LayoutAlign::Start)
553 .child(Rect::new().size(20.0)),
554 );
555 });
556 let c = g.draw_list()[0].transform * Point::ZERO;
557 assert_eq!(c, Point::new(-40.0, -40.0));
558 }
559
560 #[test]
561 fn text_line_path_and_img_draw() {
562 let png = {
563 let mut v = Vec::new();
564 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
565 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
566 .unwrap();
567 v
568 };
569 let g = scene(|view| {
570 let t = view.add(Txt::new().text("Hi").font_size(40.0));
571 assert!(t.size().0 > 20.0);
572 view.add(
573 Line::new()
574 .points([(0.0, 0.0), (100.0, 0.0)])
575 .end_arrow(true),
576 );
577 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
578 let img = view.add(Img::new().src(&png).width(40.0));
579 assert_eq!(img.height().get(), 20.0);
580 });
581 let kinds: Vec<_> = g
582 .draw_list()
583 .iter()
584 .map(|i| match i.prim {
585 Primitive::Fill { .. } => "fill",
586 Primitive::Stroke { .. } => "stroke",
587 Primitive::Image { .. } => "image",
588 Primitive::PushLayer(_) => "push",
589 Primitive::PopLayer => "pop",
590 })
591 .collect();
592 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
593 }
594
595 #[test]
596 fn trimming_and_pick() {
597 let g = scene(|view| {
598 view.add(Rect::new().size(100.0).name("back"));
599 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
600 });
601 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
602 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
603 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
605 }
606
607 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
608 g.draw_list()
609 .iter()
610 .map(|i| match i.prim {
611 Primitive::Fill { .. } => "fill",
612 Primitive::Stroke { .. } => "stroke",
613 Primitive::Image { .. } => "image",
614 Primitive::PushLayer(_) => "push",
615 Primitive::PopLayer => "pop",
616 })
617 .collect()
618 }
619
620 #[test]
621 fn layers_only_when_needed_and_balanced() {
622 let g = scene(|v| {
624 v.add(Rect::new().opacity(0.5));
625 });
626 assert_eq!(kinds(&g), ["fill"]);
627 let g = scene(|v| {
629 v.add(
630 Group::new()
631 .opacity(0.5)
632 .child(Rect::new())
633 .child(Circle::new()),
634 );
635 });
636 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
637 let g = scene(|v| {
639 v.add(
640 Circle::new()
641 .clip(true)
642 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
643 .child(Rect::new()),
644 );
645 });
646 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
647 }
648
649 #[test]
650 fn z_index_orders_siblings_and_anchor_moves_box() {
651 let g = scene(|v| {
652 v.add(Rect::new().name("a").z_index(2.0));
653 v.add(Rect::new().name("b"));
654 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
655 });
656 let items = g.draw_list();
657 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
658 assert_eq!(order, vec![2, 3, 1]);
659 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
661 }
662
663 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
664 g.draw_list()
665 .iter()
666 .filter_map(|i| match &i.prim {
667 Primitive::Fill { path, .. } => {
668 let bb = kurbo::Shape::bounding_box(path);
669 Some((i.transform * Point::ZERO, bb.width()))
670 }
671 _ => None,
672 })
673 .collect()
674 }
675
676 #[test]
677 fn flex_grow_stretch_wrap_and_grid() {
678 use crate::{Justify, LayoutAlign};
679 let g = scene(|v| {
681 v.add(
682 Layout::new()
683 .width(200.0)
684 .justify(Justify::Start)
685 .child(Rect::new().size(40.0))
686 .child(Rect::new().size(40.0).grow(1.0)),
687 );
688 });
689 let b = boxes(&g);
690 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
691 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
692
693 let g = scene(|v| {
695 v.add(
696 Layout::new()
697 .column()
698 .width(120.0)
699 .align(LayoutAlign::Stretch)
700 .child(Rect::new().size(30.0)),
701 );
702 });
703 assert_eq!(boxes(&g)[0].1, 120.0);
704
705 let g = scene(|v| {
707 v.add(
708 Layout::new()
709 .width(130.0)
710 .wrap(true)
711 .child(Rect::new().size(60.0))
712 .child(Rect::new().size(60.0))
713 .child(Rect::new().size(60.0)),
714 );
715 });
716 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
717 assert_eq!(ys[0], ys[1]);
718 assert!(ys[2] > ys[0]);
719
720 let g = scene(|v| {
722 v.add(
723 Layout::new()
724 .grid(2)
725 .width(200.0)
726 .align(LayoutAlign::Stretch)
727 .child(Rect::new().size(10.0))
728 .child(Rect::new().size(10.0))
729 .child(Rect::new().size(10.0)),
730 );
731 });
732 let b = boxes(&g);
733 assert_eq!(
734 b.iter().map(|b| b.1).collect::<Vec<_>>(),
735 vec![100.0, 100.0, 100.0]
736 );
737 assert_eq!(b[2].0.x, b[0].0.x);
738 assert!(b[2].0.y > b[0].0.y);
739 }
740
741 #[test]
742 fn describe_lists_signals() {
743 let g = scene(|view| {
744 view.add(Txt::new().text("abc"));
745 });
746 let d = g.describe(NodeId(1));
747 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
748 }
749}