1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use oxframe_core::{BlendMode, Color, Gradient, Seconds, Signal, Tween, Vec2, all};
5
6use crate::builder::{
7 self, CameraBuilder, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder,
8 LayoutBuilder, LineBuilder, NodeBuilder, PathBuilder, PolygonBuilder, RectBuilder, StarBuilder,
9 TxtBuilder,
10};
11use crate::draw::{LineCap, LineJoin};
12use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
13use crate::place::{Edge, Place, SafeArea, Side};
14use crate::text::RichSpan;
15
16pub const SLIDE_DISTANCE: f64 = 160.0;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct Node(NodeId);
22
23macro_rules! base_signal {
24 ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
25 $(
26 $(#[$doc])*
27 #[doc = ""]
28 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
29 pub fn $field(self) -> Signal<$ty> {
30 with_graph(|g| g.node(self.0).$field.clone())
31 }
32 )*
33 };
34}
35
36impl Node {
37 pub fn id(self) -> NodeId {
39 self.0
40 }
41
42 pub fn from_id(id: NodeId) -> Self {
47 Self(id)
48 }
49
50 base_signal! {
51 x: f64,
52 y: f64,
53 rotation: f64,
55 scale: Vec2,
56 opacity: f64,
57 fill: Color,
58 stroke: Color,
59 line_width: f64,
60 start: f64,
62 end: f64,
64 fill_gradient: Option<Gradient>,
65 stroke_gradient: Option<Gradient>,
66 dash: Vec<f64>,
67 dash_offset: f64,
68 line_cap: LineCap,
69 line_join: LineJoin,
70 blur: f64,
71 shadow_color: Color,
72 shadow_blur: f64,
73 shadow_offset: Vec2,
74 clip: bool,
75 blend: BlendMode,
76 z_index: f64,
77 anchor: Vec2,
78 grow: f64,
79 shrink: f64,
80 basis: Option<f64>,
81 margin: f64,
82 align_self: Option<crate::LayoutAlign>,
83 }
84
85 #[allow(clippy::should_implement_trait)] #[track_caller]
91 pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
92 let loc = std::panic::Location::caller();
93 let prev = crate::graph::push_add_at(loc.file(), loc.line());
94 let handle = builder::add(self.0, child);
95 crate::graph::pop_add_at(prev);
96 handle
97 }
98
99 pub fn remove(self) {
101 with_graph(|g| g.detach(self.0));
102 }
103
104 pub fn fade_in(self) -> Tween<f64> {
107 let motion = crate::theme::current().motion;
108 self.opacity().set(0.0);
109 self.opacity().to(1.0, motion.normal).ease(motion.enter)
110 }
111
112 pub fn pop_in(self) -> Tween<Vec2> {
115 let motion = crate::theme::current().motion;
116 let full = self.scale().peek();
117 self.scale().set(Vec2::ZERO);
118 self.scale().to(full, motion.normal).ease(motion.emphasis)
119 }
120
121 pub fn draw_on(self) -> Tween<f64> {
125 let motion = crate::theme::current().motion;
126 self.end().set(0.0);
127 self.end().to(1.0, motion.slow).ease(motion.enter)
128 }
129
130 pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
133 let motion = crate::theme::current().motion;
134 let (axis, offset) = match from {
135 Side::Left => (self.x(), -SLIDE_DISTANCE),
136 Side::Right => (self.x(), SLIDE_DISTANCE),
137 Side::Top => (self.y(), -SLIDE_DISTANCE),
138 Side::Bottom => (self.y(), SLIDE_DISTANCE),
139 };
140 let home = axis.peek();
141 axis.set(home + offset);
142 let moves = all((
143 axis.to(home, motion.normal).ease(motion.enter),
144 self.fade_in(),
145 ));
146 async move {
147 moves.await;
148 }
149 }
150
151 pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
154 let other = other.into();
155 with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
156 self
157 }
158
159 pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
161 let other = other.into();
162 with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
163 self
164 }
165
166 pub fn to_edge(self, edge: Edge) -> Self {
168 with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
169 self
170 }
171
172 pub fn center_in(self, other: impl Into<Node>) -> Self {
174 let other = other.into();
175 with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
176 self
177 }
178
179 pub fn fade_out(self) -> Tween<f64> {
181 let motion = crate::theme::current().motion;
182 self.opacity().to(0.0, motion.normal).ease(motion.exit)
183 }
184
185 pub fn reparent(self, parent: impl Into<Node>) {
187 let parent = parent.into();
188 with_graph(|g| g.reparent(self.0, parent.0));
189 }
190
191 pub fn children(self) -> Vec<Node> {
193 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
194 }
195
196 pub fn size(self) -> (f64, f64) {
198 with_graph(|g| g.measure(self.0))
199 }
200
201 pub fn absolute_position(self) -> Vec2 {
203 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
204 }
205}
206
207macro_rules! typed_handle {
208 ($name:ident, $builder:ident) => {
209 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
210 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
211 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212 pub struct $name(Node);
213
214 impl $name {
215 #[allow(clippy::new_ret_no_self)]
217 pub fn new() -> $builder {
218 $builder::default()
219 }
220
221 pub(crate) fn from_id(id: NodeId) -> Self {
222 Self(Node(id))
223 }
224
225 pub fn node(self) -> Node {
227 self.0
228 }
229 }
230
231 impl Deref for $name {
232 type Target = Node;
233 fn deref(&self) -> &Node {
234 &self.0
235 }
236 }
237
238 impl From<$name> for Node {
239 fn from(h: $name) -> Node {
240 h.0
241 }
242 }
243 };
244}
245
246typed_handle!(Group, GroupBuilder);
247typed_handle!(Camera, CameraBuilder);
248typed_handle!(Rect, RectBuilder);
249typed_handle!(Circle, CircleBuilder);
250typed_handle!(Path, PathBuilder);
251typed_handle!(Line, LineBuilder);
252typed_handle!(Txt, TxtBuilder);
253typed_handle!(Img, ImgBuilder);
254typed_handle!(Layout, LayoutBuilder);
255typed_handle!(Grid, GridBuilder);
256typed_handle!(Polygon, PolygonBuilder);
257typed_handle!(Star, StarBuilder);
258typed_handle!(Code, CodeBuilder);
259
260macro_rules! kind_signal {
261 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
262 impl $handle {
263 $(
264 $(#[$doc])*
265 #[doc = ""]
266 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
267 pub fn $field(self) -> Signal<$ty> {
268 with_graph(|g| match &g.node(self.id()).kind {
269 NodeKind::$kind { $field, .. } => $field.clone(),
270 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
271 })
272 }
273 )*
274 }
275 };
276}
277
278kind_signal!(Rect::Rect {
279 width: f64,
280 height: f64,
281 radius: f64
282});
283kind_signal!(Circle::Circle {
284 size: f64,
286 start_angle: f64,
287 end_angle: f64,
288});
289kind_signal!(Polygon::Polygon {
290 sides: f64,
291 size: f64
292});
293kind_signal!(Star::Star {
294 points: f64,
295 size: f64,
296 inner: f64
297});
298kind_signal!(Path::Path { data: BezPath });
299kind_signal!(Camera::Camera {
300 zoom: f64
302});
303
304impl Camera {
305 pub fn center_on(
307 self,
308 node: impl Into<Node>,
309 duration: impl Seconds,
310 ) -> impl std::future::Future<Output = ()> + 'static {
311 let node = node.into();
312 let d = duration.seconds();
313 let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
314 let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
315 async move {
316 if let Some(m) = moves {
317 m.await;
318 }
319 }
320 }
321
322 pub fn fit(
326 self,
327 node: impl Into<Node>,
328 padding: f64,
329 duration: impl Seconds,
330 ) -> impl std::future::Future<Output = ()> + 'static {
331 let node = node.into();
332 let d = duration.seconds();
333 let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
334 let moves = bounds.map(|r| {
335 let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
336 .min((fh - 2.0 * padding) / r.height().max(1e-6))
337 .max(1e-3);
338 let c = r.center();
339 all((
340 self.x().to(c.x, d),
341 self.y().to(c.y, d),
342 self.zoom().to(zoom, d),
343 ))
344 });
345 async move {
346 if let Some(m) = moves {
347 m.await;
348 }
349 }
350 }
351
352 pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
354 let d = duration.seconds();
355 let moves = all((
356 self.x().to(0.0, d),
357 self.y().to(0.0, d),
358 self.zoom().to(1.0, d),
359 self.rotation().to(0.0, d),
360 ));
361 async move {
362 moves.await;
363 }
364 }
365}
366kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
367kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
368kind_signal!(Code::Code {
369 code: String,
370 font_size: f64,
371 focus: Vec2,
373 dim: f64,
375});
376
377impl Txt {
378 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
381 let to = text.into();
382 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
383 NodeKind::Txt {
384 text,
385 morph,
386 morph_t,
387 ..
388 } => (text.clone(), morph.clone(), morph_t.clone()),
389 _ => unreachable!("handle does not point to a Txt"),
390 });
391 Box::pin(async move {
392 morph.set(Some((txt.get(), to.clone())));
393 t.set(0.0);
394 t.to(1.0, duration).await;
395 txt.set(to);
396 morph.set(None);
397 })
398 }
399}
400
401impl Code {
402 pub fn focus_lines(
404 self,
405 first: usize,
406 last: usize,
407 duration: f64,
408 ) -> oxframe_core::Tween<Vec2> {
409 self.focus()
410 .to(Vec2::new(first as f64, last as f64), duration)
411 }
412}
413kind_signal!(Img::Img {
414 width: f64,
415 height: f64
416});
417kind_signal!(Layout::Layout {
418 gap: f64,
419 padding: f64,
420 radius: f64,
421 width: Option<f64>,
423 height: Option<f64>,
425});
426kind_signal!(Grid::Grid {
427 width: f64,
428 height: f64,
429 spacing: f64
430});
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434pub struct View(Node);
435
436impl View {
437 pub fn current() -> Self {
439 Self(Node(SceneGraph::ROOT))
440 }
441
442 pub fn size(self) -> (u32, u32) {
444 with_graph(|g| g.size())
445 }
446
447 pub fn is_portrait(self) -> bool {
449 let (w, h) = self.size();
450 h > w
451 }
452
453 pub fn safe_area(self) -> SafeArea {
457 let (w, h) = self.size();
458 SafeArea::for_frame(f64::from(w), f64::from(h))
459 }
460}
461
462impl Deref for View {
463 type Target = Node;
464 fn deref(&self) -> &Node {
465 &self.0
466 }
467}
468
469impl From<View> for Node {
470 fn from(v: View) -> Node {
471 v.0
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::Primitive;
479
480 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
481 let graph = SceneGraph::new(400, 400);
482 graph.enter(|| f(View::current()));
483 graph
484 }
485
486 fn play(
488 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
489 ) -> u32 {
490 let graph = SceneGraph::new(400, 400);
491 graph.enter(|| {
492 let fut = scene_fn(View::current());
493 let mut ex =
494 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
495 let mut frames = 0;
496 while !ex.step() {
497 frames += 1;
498 assert!(frames < 10_000, "scene did not finish");
499 }
500 frames
501 })
502 }
503
504 fn screen(n: impl Into<Node>) -> (f64, f64) {
506 let p = n.into().absolute_position();
507 ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
508 }
509
510 #[test]
511 fn camera_pans_zooms_and_turns_its_children_only() {
512 scene(|view| {
513 let cam = view.add(Camera::new());
514 let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
515 let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
516 assert_eq!(screen(dot), (100.0, 50.0));
518 cam.x().set(100.0);
520 cam.y().set(50.0);
521 assert_eq!(screen(dot), (0.0, 0.0));
522 cam.zoom().set(2.0);
524 let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
525 assert_eq!(screen(other), (20.0, 0.0));
526 cam.rotation().set(90.0);
528 assert_eq!(screen(other), (0.0, -20.0));
529 assert_eq!(screen(hud), (-300.0, -200.0));
531 });
532 }
533
534 #[test]
535 fn camera_center_on_fit_and_reset() {
536 play(|view| {
537 let cam = view.add(Camera::new());
538 let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
539 let row = cam.add(
540 Layout::new()
541 .x(-500.0)
542 .gap(10.0)
543 .child(Rect::new().size(50.0))
544 .child(Rect::new().size(50.0)),
545 );
546 Box::pin(async move {
547 cam.center_on(card, 1.0).await;
548 assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
549 assert_eq!(screen(card), (0.0, 0.0));
550 cam.fit(card, 20.0, 0.0).await;
552 assert!(
553 (cam.zoom().get() - 1.8).abs() < 1e-9,
554 "{}",
555 cam.zoom().get()
556 );
557 row.gap().to(110.0, 0.5).await;
559 cam.fit(row, 0.0, 0.0).await;
560 let (w, _) = row.size();
561 assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
562 assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
563 assert!((cam.x().get() + 500.0).abs() < 1e-9);
564 cam.reset(0.5).await;
565 assert_eq!(
566 (
567 cam.x().get(),
568 cam.y().get(),
569 cam.zoom().get(),
570 cam.rotation().get()
571 ),
572 (0.0, 0.0, 1.0, 0.0)
573 );
574 })
575 });
576 }
577
578 #[test]
579 fn entrance_helpers_start_at_once_and_end_in_place() {
580 let normal = crate::theme::current().motion.normal;
581 let expected = (normal * 60.0).round() as u32;
582 let frames = play(|view| {
583 let r = view.add(Rect::new().size(100.0).x(50.0));
584 let pop = r.pop_in();
585 assert_eq!(r.scale().get(), Vec2::ZERO);
588 let slide = r.slide_in(Side::Left);
589 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
590 assert_eq!(r.opacity().get(), 0.0);
591 let draw = r.draw_on();
592 assert_eq!(r.end().get(), 0.0);
593 Box::pin(async move {
594 all((pop, slide)).await;
595 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
596 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
597 draw.await;
598 assert_eq!(r.end().get(), 1.0);
599 r.fade_out().duration(0.5).await;
600 assert_eq!(r.opacity().get(), 0.0);
601 })
602 });
603 let slow = crate::theme::current().motion.slow;
604 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
605 }
606
607 #[test]
608 fn add_records_its_call_site_and_scene_bounds() {
609 let g = scene(|view| {
610 view.add(Rect::new().size(100.0).name("box"));
611 view.add(Rect::new().size(10.0).x(100.0));
612 });
613 let nodes = g.inspect_nodes();
614 let named = nodes.iter().find(|n| n.name == "box").unwrap();
615 assert_eq!(named.kind, "Rect");
616 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
617 assert!(
618 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
619 "{:?}",
620 named.bounds
621 );
622 let auto = nodes
623 .iter()
624 .find(|n| n.name != "box" && n.name != "view")
625 .unwrap();
626 assert_eq!(auto.name, auto.source.clone().unwrap());
627 }
628
629 #[test]
630 fn builders_set_initial_values_and_handles_return_signals() {
631 scene(|view| {
632 let r = view.add(
633 Rect::new()
634 .size(50.0)
635 .radius(5.0)
636 .x(-10.0)
637 .fill(Color::BLACK),
638 );
639 assert_eq!(r.width().get(), 50.0);
640 assert_eq!(r.height().get(), 50.0);
641 assert_eq!(r.radius().get(), 5.0);
642 assert_eq!(r.x().get(), -10.0);
643 assert_eq!(r.fill().get(), Color::BLACK);
644 r.x().set(3.0);
645 assert_eq!(r.x().get(), 3.0);
646 let c = r.add(Circle::new().size(10.0));
647 assert_eq!(r.children(), vec![c.node()]);
648 assert_eq!(view.children(), vec![r.node()]);
649 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
650 });
651 }
652
653 #[test]
654 fn draw_list_composes_transforms() {
655 let g = scene(|view| {
656 let grp = view.add(Group::new().x(10.0).scale(2.0));
657 grp.add(Circle::new().x(5.0));
658 });
659 let items = g.draw_list();
660 assert_eq!(items.len(), 1);
661 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
662 }
663
664 #[test]
665 fn layout_places_children_in_a_row() {
666 let g = scene(|view| {
667 view.add(
668 Layout::new()
669 .gap(10.0)
670 .padding(5.0)
671 .child(Rect::new().size(20.0))
672 .child(Rect::new().width(40.0).height(10.0)),
673 );
674 });
675 let layout = NodeId(1);
676 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
677 let centers: Vec<_> = g
678 .draw_list()
679 .iter()
680 .map(|i| i.transform * Point::ZERO)
681 .collect();
682 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
684 }
685
686 #[test]
687 fn column_with_fixed_size_and_alignment() {
688 let g = scene(|view| {
689 view.add(
690 Layout::new()
691 .column()
692 .height(100.0)
693 .width(100.0)
694 .justify(crate::Justify::Start)
695 .align(crate::LayoutAlign::Start)
696 .child(Rect::new().size(20.0)),
697 );
698 });
699 let c = g.draw_list()[0].transform * Point::ZERO;
700 assert_eq!(c, Point::new(-40.0, -40.0));
701 }
702
703 #[test]
704 fn text_line_path_and_img_draw() {
705 let png = {
706 let mut v = Vec::new();
707 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
708 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
709 .unwrap();
710 v
711 };
712 let g = scene(|view| {
713 let t = view.add(Txt::new().text("Hi").font_size(40.0));
714 assert!(t.size().0 > 20.0);
715 view.add(
716 Line::new()
717 .points([(0.0, 0.0), (100.0, 0.0)])
718 .end_arrow(true),
719 );
720 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
721 let img = view.add(Img::new().src(&png).width(40.0));
722 assert_eq!(img.height().get(), 20.0);
723 });
724 let kinds: Vec<_> = g
725 .draw_list()
726 .iter()
727 .map(|i| match i.prim {
728 Primitive::Fill { .. } => "fill",
729 Primitive::Stroke { .. } => "stroke",
730 Primitive::Image { .. } => "image",
731 Primitive::PushLayer(_) => "push",
732 Primitive::PopLayer => "pop",
733 })
734 .collect();
735 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
736 }
737
738 #[test]
739 fn trimming_and_pick() {
740 let g = scene(|view| {
741 view.add(Rect::new().size(100.0).name("back"));
742 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
743 });
744 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
745 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
746 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
748 }
749
750 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
751 g.draw_list()
752 .iter()
753 .map(|i| match i.prim {
754 Primitive::Fill { .. } => "fill",
755 Primitive::Stroke { .. } => "stroke",
756 Primitive::Image { .. } => "image",
757 Primitive::PushLayer(_) => "push",
758 Primitive::PopLayer => "pop",
759 })
760 .collect()
761 }
762
763 #[test]
764 fn layers_only_when_needed_and_balanced() {
765 let g = scene(|v| {
767 v.add(Rect::new().opacity(0.5));
768 });
769 assert_eq!(kinds(&g), ["fill"]);
770 let g = scene(|v| {
772 v.add(
773 Group::new()
774 .opacity(0.5)
775 .child(Rect::new())
776 .child(Circle::new()),
777 );
778 });
779 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
780 let g = scene(|v| {
782 v.add(
783 Circle::new()
784 .clip(true)
785 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
786 .child(Rect::new()),
787 );
788 });
789 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
790 }
791
792 #[test]
793 fn z_index_orders_siblings_and_anchor_moves_box() {
794 let g = scene(|v| {
795 v.add(Rect::new().name("a").z_index(2.0));
796 v.add(Rect::new().name("b"));
797 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
798 });
799 let items = g.draw_list();
800 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
801 assert_eq!(order, vec![2, 3, 1]);
802 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
804 }
805
806 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
807 g.draw_list()
808 .iter()
809 .filter_map(|i| match &i.prim {
810 Primitive::Fill { path, .. } => {
811 let bb = kurbo::Shape::bounding_box(path);
812 Some((i.transform * Point::ZERO, bb.width()))
813 }
814 _ => None,
815 })
816 .collect()
817 }
818
819 #[test]
820 fn flex_grow_stretch_wrap_and_grid() {
821 use crate::{Justify, LayoutAlign};
822 let g = scene(|v| {
824 v.add(
825 Layout::new()
826 .width(200.0)
827 .justify(Justify::Start)
828 .child(Rect::new().size(40.0))
829 .child(Rect::new().size(40.0).grow(1.0)),
830 );
831 });
832 let b = boxes(&g);
833 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
834 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
835
836 let g = scene(|v| {
838 v.add(
839 Layout::new()
840 .column()
841 .width(120.0)
842 .align(LayoutAlign::Stretch)
843 .child(Rect::new().size(30.0)),
844 );
845 });
846 assert_eq!(boxes(&g)[0].1, 120.0);
847
848 let g = scene(|v| {
850 v.add(
851 Layout::new()
852 .width(130.0)
853 .wrap(true)
854 .child(Rect::new().size(60.0))
855 .child(Rect::new().size(60.0))
856 .child(Rect::new().size(60.0)),
857 );
858 });
859 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
860 assert_eq!(ys[0], ys[1]);
861 assert!(ys[2] > ys[0]);
862
863 let g = scene(|v| {
865 v.add(
866 Layout::new()
867 .grid(2)
868 .width(200.0)
869 .align(LayoutAlign::Stretch)
870 .child(Rect::new().size(10.0))
871 .child(Rect::new().size(10.0))
872 .child(Rect::new().size(10.0)),
873 );
874 });
875 let b = boxes(&g);
876 assert_eq!(
877 b.iter().map(|b| b.1).collect::<Vec<_>>(),
878 vec![100.0, 100.0, 100.0]
879 );
880 assert_eq!(b[2].0.x, b[0].0.x);
881 assert!(b[2].0.y > b[0].0.y);
882 }
883
884 #[test]
885 fn describe_lists_signals() {
886 let g = scene(|view| {
887 view.add(Txt::new().text("abc"));
888 });
889 let d = g.describe(NodeId(1));
890 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
891 }
892}