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