1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use std::future::Future;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use oxframe_core::{BlendMode, Color, Ease, Gradient, Seconds, Signal, Tween, Vec2, all};
9
10use crate::builder::{
11 self, CameraBuilder, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder,
12 LayoutBuilder, LineBuilder, NodeBuilder, ParticlesBuilder, PathBuilder, PolygonBuilder,
13 RectBuilder, ShaderBuilder, StarBuilder, SvgBuilder, TxtBuilder,
14};
15use crate::draw::{LineCap, LineJoin};
16use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
17use crate::place::{Edge, Place, SafeArea, Side};
18use crate::text::RichSpan;
19
20pub const SLIDE_DISTANCE: f64 = 160.0;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct Node(NodeId);
26
27macro_rules! base_signal {
28 ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
29 $(
30 $(#[$doc])*
31 #[doc = ""]
32 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
33 pub fn $field(self) -> Signal<$ty> {
34 with_graph(|g| g.node(self.0).$field.clone())
35 }
36 )*
37 };
38}
39
40impl Node {
41 pub fn id(self) -> NodeId {
43 self.0
44 }
45
46 pub fn from_id(id: NodeId) -> Self {
51 Self(id)
52 }
53
54 base_signal! {
55 x: f64,
56 y: f64,
57 rotation: f64,
59 scale: Vec2,
60 opacity: f64,
61 fill: Color,
62 stroke: Color,
63 line_width: f64,
64 start: f64,
66 end: f64,
68 fill_gradient: Option<Gradient>,
69 stroke_gradient: Option<Gradient>,
70 dash: Vec<f64>,
71 dash_offset: f64,
72 line_cap: LineCap,
73 line_join: LineJoin,
74 blur: f64,
75 shadow_color: Color,
76 shadow_blur: f64,
77 shadow_offset: Vec2,
78 clip: bool,
79 blend: BlendMode,
80 z_index: f64,
81 anchor: Vec2,
82 grow: f64,
83 shrink: f64,
84 basis: Option<f64>,
85 margin: f64,
86 align_self: Option<crate::LayoutAlign>,
87 }
88
89 #[allow(clippy::should_implement_trait)] #[track_caller]
95 pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
96 let loc = std::panic::Location::caller();
97 let prev = crate::graph::push_add_at(loc.file(), loc.line());
98 let handle = builder::add(self.0, child);
99 crate::graph::pop_add_at(prev);
100 handle
101 }
102
103 pub fn remove(self) {
105 with_graph(|g| g.detach(self.0));
106 }
107
108 pub fn fade_in(self) -> Tween<f64> {
111 let motion = crate::theme::current().motion;
112 self.opacity().set(0.0);
113 self.opacity().to(1.0, motion.normal).ease(motion.enter)
114 }
115
116 pub fn pop_in(self) -> Tween<Vec2> {
119 let motion = crate::theme::current().motion;
120 let full = self.scale().peek();
121 self.scale().set(Vec2::ZERO);
122 self.scale().to(full, motion.normal).ease(motion.emphasis)
123 }
124
125 pub fn draw_on(self) -> Tween<f64> {
129 let motion = crate::theme::current().motion;
130 self.end().set(0.0);
131 self.end().to(1.0, motion.slow).ease(motion.enter)
132 }
133
134 pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
137 let motion = crate::theme::current().motion;
138 let (axis, offset) = match from {
139 Side::Left => (self.x(), -SLIDE_DISTANCE),
140 Side::Right => (self.x(), SLIDE_DISTANCE),
141 Side::Top => (self.y(), -SLIDE_DISTANCE),
142 Side::Bottom => (self.y(), SLIDE_DISTANCE),
143 };
144 let home = axis.peek();
145 axis.set(home + offset);
146 let moves = all((
147 axis.to(home, motion.normal).ease(motion.enter),
148 self.fade_in(),
149 ));
150 async move {
151 moves.await;
152 }
153 }
154
155 pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
158 let other = other.into();
159 with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
160 self
161 }
162
163 pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
165 let other = other.into();
166 with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
167 self
168 }
169
170 pub fn to_edge(self, edge: Edge) -> Self {
172 with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
173 self
174 }
175
176 pub fn center_in(self, other: impl Into<Node>) -> Self {
178 let other = other.into();
179 with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
180 self
181 }
182
183 pub fn fade_out(self) -> Tween<f64> {
185 let motion = crate::theme::current().motion;
186 self.opacity().to(0.0, motion.normal).ease(motion.exit)
187 }
188
189 pub fn reparent(self, parent: impl Into<Node>) {
191 let parent = parent.into();
192 with_graph(|g| g.reparent(self.0, parent.0));
193 }
194
195 pub fn children(self) -> Vec<Node> {
197 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
198 }
199
200 pub fn size(self) -> (f64, f64) {
202 with_graph(|g| g.measure(self.0))
203 }
204
205 pub fn absolute_position(self) -> Vec2 {
207 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
208 }
209}
210
211macro_rules! typed_handle {
212 ($name:ident, $builder:ident) => {
213 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
214 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
215 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216 pub struct $name(Node);
217
218 impl $name {
219 #[allow(clippy::new_ret_no_self)]
221 pub fn new() -> $builder {
222 $builder::default()
223 }
224
225 pub(crate) fn from_id(id: NodeId) -> Self {
226 Self(Node(id))
227 }
228
229 pub fn node(self) -> Node {
231 self.0
232 }
233 }
234
235 impl Deref for $name {
236 type Target = Node;
237 fn deref(&self) -> &Node {
238 &self.0
239 }
240 }
241
242 impl From<$name> for Node {
243 fn from(h: $name) -> Node {
244 h.0
245 }
246 }
247 };
248}
249
250typed_handle!(Group, GroupBuilder);
251typed_handle!(Camera, CameraBuilder);
252typed_handle!(Rect, RectBuilder);
253typed_handle!(Circle, CircleBuilder);
254typed_handle!(Path, PathBuilder);
255typed_handle!(Line, LineBuilder);
256typed_handle!(Txt, TxtBuilder);
257typed_handle!(Img, ImgBuilder);
258typed_handle!(Svg, SvgBuilder);
259typed_handle!(Layout, LayoutBuilder);
260typed_handle!(Grid, GridBuilder);
261typed_handle!(Polygon, PolygonBuilder);
262typed_handle!(Star, StarBuilder);
263typed_handle!(Code, CodeBuilder);
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267pub struct Custom(Node);
268
269impl Custom {
270 #[allow(clippy::new_ret_no_self)]
274 pub fn new(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> crate::CustomBuilder {
275 crate::CustomBuilder::from_draw(draw)
276 }
277
278 pub(crate) fn from_id(id: NodeId) -> Self {
279 Self(Node(id))
280 }
281
282 pub fn node(self) -> Node {
284 self.0
285 }
286}
287
288impl Deref for Custom {
289 type Target = Node;
290 fn deref(&self) -> &Node {
291 &self.0
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub struct Shader(Node);
298
299impl Shader {
300 #[allow(clippy::new_ret_no_self)]
306 pub fn source(wgsl: impl Into<String>) -> ShaderBuilder {
307 ShaderBuilder::new(crate::shader::ShaderCode::Wgsl(wgsl.into()))
308 }
309
310 pub fn file(path: impl AsRef<std::path::Path>) -> ShaderBuilder {
313 ShaderBuilder::new(crate::shader::ShaderCode::File(path.as_ref().into()))
314 }
315
316 pub(crate) fn from_id(id: NodeId) -> Self {
317 Self(Node(id))
318 }
319
320 pub fn node(self) -> Node {
322 self.0
323 }
324
325 pub fn uniform(self, name: &str) -> Signal<f64> {
332 self.number(name)
333 }
334
335 pub fn amp(self) -> Signal<f64> {
337 self.number("amp")
338 }
339
340 pub fn speed(self) -> Signal<f64> {
342 self.number("speed")
343 }
344
345 pub fn deep(self) -> Signal<Color> {
347 self.color("deep")
348 }
349
350 pub fn foam(self) -> Signal<Color> {
352 self.color("foam")
353 }
354
355 fn number(self, name: &str) -> Signal<f64> {
356 with_graph(|g| {
357 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
358 unreachable!("handle does not point to a Shader");
359 };
360 for u in uniforms {
361 if u.name == name
362 && let crate::shader::UniformValue::Num(v) = &u.value
363 {
364 return v.clone();
365 }
366 }
367 panic!("no numeric uniform {name}. {}", uniform_names(uniforms));
368 })
369 }
370
371 fn color(self, name: &str) -> Signal<Color> {
372 with_graph(|g| {
373 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
374 unreachable!("handle does not point to a Shader");
375 };
376 for u in uniforms {
377 if u.name == name
378 && let crate::shader::UniformValue::Color(v) = &u.value
379 {
380 return v.clone();
381 }
382 }
383 panic!("no color uniform {name}. {}", uniform_names(uniforms));
384 })
385 }
386}
387
388fn uniform_names(uniforms: &[crate::shader::Uniform]) -> String {
389 let names: Vec<&str> = uniforms.iter().map(|u| u.name.as_str()).collect();
390 format!("Available: {}.", names.join(", "))
391}
392
393impl Deref for Shader {
394 type Target = Node;
395 fn deref(&self) -> &Node {
396 &self.0
397 }
398}
399
400impl From<Custom> for Node {
401 fn from(h: Custom) -> Node {
402 h.0
403 }
404}
405
406typed_handle!(Particles, ParticlesBuilder);
407
408macro_rules! kind_signal {
409 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
410 impl $handle {
411 $(
412 $(#[$doc])*
413 #[doc = ""]
414 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
415 pub fn $field(self) -> Signal<$ty> {
416 with_graph(|g| match &g.node(self.id()).kind {
417 NodeKind::$kind { $field, .. } => $field.clone(),
418 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
419 })
420 }
421 )*
422 }
423 };
424}
425
426kind_signal!(Particles::Particles { gravity: f64 });
427
428impl Particles {
429 pub fn burst(self, origin: impl Into<Vec2>, count: u32) -> oxframe_core::WaitFor {
435 let origin = origin.into();
436 let time = oxframe_core::time();
437 with_graph(|g| {
438 if let NodeKind::Particles { state, .. } = &g.node(self.id()).kind {
439 state.bursts.borrow_mut().push(crate::sim::Burst {
440 time,
441 origin,
442 count,
443 });
444 }
445 });
446 oxframe_core::wait_for(0.0)
447 }
448}
449
450kind_signal!(Rect::Rect {
451 width: f64,
452 height: f64,
453 radius: f64
454});
455kind_signal!(Circle::Circle {
456 size: f64,
458 start_angle: f64,
459 end_angle: f64,
460});
461kind_signal!(Polygon::Polygon {
462 sides: f64,
463 size: f64
464});
465kind_signal!(Star::Star {
466 points: f64,
467 size: f64,
468 inner: f64
469});
470kind_signal!(Path::Path { data: BezPath });
471kind_signal!(Camera::Camera {
472 zoom: f64
474});
475
476impl Camera {
477 pub fn center_on(
479 self,
480 node: impl Into<Node>,
481 duration: impl Seconds,
482 ) -> impl std::future::Future<Output = ()> + 'static {
483 let node = node.into();
484 let d = duration.seconds();
485 let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
486 let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
487 async move {
488 if let Some(m) = moves {
489 m.await;
490 }
491 }
492 }
493
494 pub fn fit(
498 self,
499 node: impl Into<Node>,
500 padding: f64,
501 duration: impl Seconds,
502 ) -> impl std::future::Future<Output = ()> + 'static {
503 let node = node.into();
504 let d = duration.seconds();
505 let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
506 let moves = bounds.map(|r| {
507 let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
508 .min((fh - 2.0 * padding) / r.height().max(1e-6))
509 .max(1e-3);
510 let c = r.center();
511 all((
512 self.x().to(c.x, d),
513 self.y().to(c.y, d),
514 self.zoom().to(zoom, d),
515 ))
516 });
517 async move {
518 if let Some(m) = moves {
519 m.await;
520 }
521 }
522 }
523
524 pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
526 let d = duration.seconds();
527 let moves = all((
528 self.x().to(0.0, d),
529 self.y().to(0.0, d),
530 self.zoom().to(1.0, d),
531 self.rotation().to(0.0, d),
532 ));
533 async move {
534 moves.await;
535 }
536 }
537}
538kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
539kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
540kind_signal!(Code::Code {
541 font_size: f64,
542 focus: Vec2,
544 dim: f64,
546});
547
548impl Txt {
549 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
552 let to = text.into();
553 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
554 NodeKind::Txt {
555 text,
556 morph,
557 morph_t,
558 ..
559 } => (text.clone(), morph.clone(), morph_t.clone()),
560 _ => unreachable!("handle does not point to a Txt"),
561 });
562 Box::pin(async move {
563 morph.set(Some((txt.get(), to.clone())));
564 t.set(0.0);
565 t.to(1.0, duration).await;
566 txt.set(to);
567 morph.set(None);
568 })
569 }
570}
571
572impl Code {
573 pub fn code(self) -> CodeText {
581 with_graph(|g| match &g.node(self.id()).kind {
582 NodeKind::Code {
583 code,
584 edit_from,
585 edit_to,
586 editing,
587 edit_t,
588 edit_flash,
589 focus,
590 lang,
591 ..
592 } => CodeText {
593 code: code.clone(),
594 edit_from: edit_from.clone(),
595 edit_to: edit_to.clone(),
596 editing: editing.clone(),
597 edit_t: edit_t.clone(),
598 edit_flash: edit_flash.clone(),
599 focus: focus.clone(),
600 lang: *lang,
601 },
602 _ => unreachable!("handle does not point to a Code"),
603 })
604 }
605
606 pub fn focus_lines(
612 self,
613 first: usize,
614 last: usize,
615 duration: f64,
616 ) -> oxframe_core::Tween<Vec2> {
617 self.focus()
618 .to(Vec2::new(first as f64, last as f64), duration)
619 }
620
621 pub fn insert(
628 self,
629 line: usize,
630 col: usize,
631 text: impl Into<String>,
632 duration: impl Seconds,
633 ) -> CodeTween {
634 let text = text.into();
635 let src = self.code().get();
636 let next = oxframe_text::insert_at(&src, line, col, &text)
637 .unwrap_or_else(|e| panic!("code.insert: {e}"));
638 self.code().to(next, duration)
639 }
640
641 pub fn remove(
650 self,
651 range: ((usize, usize), (usize, usize)),
652 duration: impl Seconds,
653 ) -> CodeTween {
654 self.rewrite(range, String::new(), duration, "code.remove")
655 }
656
657 pub fn replace(
664 self,
665 range: ((usize, usize), (usize, usize)),
666 text: impl Into<String>,
667 duration: impl Seconds,
668 ) -> CodeTween {
669 self.rewrite(range, text.into(), duration, "code.replace")
670 }
671
672 fn rewrite(
673 self,
674 ((l0, c0), (l1, c1)): ((usize, usize), (usize, usize)),
675 text: String,
676 duration: impl Seconds,
677 what: &str,
678 ) -> CodeTween {
679 let src = self.code().get();
680 let next = oxframe_text::replace_range(
681 &src,
682 oxframe_text::Loc { line: l0, col: c0 },
683 oxframe_text::Loc { line: l1, col: c1 },
684 &text,
685 )
686 .unwrap_or_else(|e| panic!("{what}: {e}"));
687 self.code().to(next, duration)
688 }
689}
690
691#[derive(Clone)]
697pub struct CodeText {
698 code: Signal<String>,
699 edit_from: Signal<String>,
700 edit_to: Signal<String>,
701 editing: Signal<bool>,
702 edit_t: Signal<f64>,
703 edit_flash: Signal<bool>,
704 focus: Signal<Vec2>,
705 lang: oxframe_text::Lang,
706}
707
708impl std::ops::Deref for CodeText {
709 type Target = Signal<String>;
710 fn deref(&self) -> &Signal<String> {
711 &self.code
712 }
713}
714
715impl CodeText {
716 pub fn set(&self, value: impl Into<String>) {
718 self.editing.set(false);
719 self.code.set(value.into());
720 }
721
722 pub fn bind(&self, f: impl Fn() -> String + 'static) {
724 self.editing.set(false);
725 self.code.bind(f);
726 }
727
728 pub fn to(&self, target: impl Into<String>, duration: impl Seconds) -> CodeTween {
730 let target = target.into();
731 let from = self.code.get();
732 self.edit_from.set(from.clone());
733 self.edit_to.set(target.clone());
734 self.edit_flash.set(false);
735 self.editing.set(true);
736 self.edit_t.set(0.0);
737 CodeTween {
738 tween: self.edit_t.to(1.0, duration),
739 code: self.code.clone(),
740 editing: self.editing.clone(),
741 edit_flash: self.edit_flash.clone(),
742 focus: self.focus.clone(),
743 lang: self.lang,
744 from,
745 target,
746 finished: false,
747 }
748 }
749}
750
751#[must_use = "a tween does nothing unless awaited"]
754pub struct CodeTween {
755 tween: Tween<f64>,
756 code: Signal<String>,
757 editing: Signal<bool>,
758 edit_flash: Signal<bool>,
759 focus: Signal<Vec2>,
760 lang: oxframe_text::Lang,
761 from: String,
762 target: String,
763 finished: bool,
764}
765
766impl CodeTween {
767 pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
769 self.tween = self.tween.ease(ease);
770 self
771 }
772
773 pub fn duration(mut self, duration: impl Seconds) -> Self {
775 self.tween = self.tween.duration(duration);
776 self
777 }
778
779 pub fn flash(self) -> Self {
781 self.edit_flash.set(true);
782 self
783 }
784}
785
786impl Future for CodeTween {
787 type Output = ();
788
789 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
790 let this = self.get_mut();
791 if Pin::new(&mut this.tween).poll(cx).is_pending() {
792 return Poll::Pending;
793 }
794 if !this.finished {
795 this.finished = true;
796 let focus = this.focus.get();
797 let (a, b) = (focus.x.round() as i64, focus.y.round() as i64);
798 if a > 0 || b > 0 {
799 let diff = oxframe_text::diff_code(&this.from, &this.target, this.lang);
800 if let Some((n0, n1)) =
801 oxframe_text::map_focus(&diff, a.max(0) as usize, b.max(0) as usize)
802 {
803 this.focus.set(Vec2::new(n0 as f64, n1 as f64));
804 }
805 }
806 this.code.set(this.target.clone());
807 this.editing.set(false);
808 }
809 Poll::Ready(())
810 }
811}
812kind_signal!(Svg::Svg {
813 width: f64,
814 height: f64
815});
816
817impl Svg {
818 pub fn find(self, id: &str) -> Node {
824 let id = with_graph(|g| g.descendant_named(self.id(), id))
825 .unwrap_or_else(|| panic!("svg has no element with id {id:?}"));
826 Node::from_id(id)
827 }
828
829 pub fn paths(self) -> Vec<Path> {
831 with_graph(|g| {
832 g.descendant_paths(self.id())
833 .into_iter()
834 .map(Path::from_id)
835 .collect()
836 })
837 }
838}
839
840kind_signal!(Img::Img {
841 width: f64,
842 height: f64
843});
844kind_signal!(Layout::Layout {
845 gap: f64,
846 padding: f64,
847 radius: f64,
848 width: Option<f64>,
850 height: Option<f64>,
852});
853kind_signal!(Grid::Grid {
854 width: f64,
855 height: f64,
856 spacing: f64
857});
858
859#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861pub struct View(Node);
862
863impl View {
864 pub fn current() -> Self {
866 Self(Node(SceneGraph::ROOT))
867 }
868
869 pub fn size(self) -> (u32, u32) {
871 with_graph(|g| g.size())
872 }
873
874 pub fn is_portrait(self) -> bool {
876 let (w, h) = self.size();
877 h > w
878 }
879
880 pub fn safe_area(self) -> SafeArea {
884 let (w, h) = self.size();
885 SafeArea::for_frame(f64::from(w), f64::from(h))
886 }
887}
888
889impl Deref for View {
890 type Target = Node;
891 fn deref(&self) -> &Node {
892 &self.0
893 }
894}
895
896impl From<View> for Node {
897 fn from(v: View) -> Node {
898 v.0
899 }
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905 use crate::Primitive;
906
907 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
908 let graph = SceneGraph::new(400, 400);
909 graph.enter(|| f(View::current()));
910 graph
911 }
912
913 fn play(
915 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
916 ) -> u32 {
917 let graph = SceneGraph::new(400, 400);
918 graph.enter(|| {
919 let fut = scene_fn(View::current());
920 let mut ex =
921 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
922 let mut frames = 0;
923 while !ex.step() {
924 frames += 1;
925 assert!(frames < 10_000, "scene did not finish");
926 }
927 frames
928 })
929 }
930
931 fn screen(n: impl Into<Node>) -> (f64, f64) {
933 let p = n.into().absolute_position();
934 ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
935 }
936
937 #[test]
938 fn camera_pans_zooms_and_turns_its_children_only() {
939 scene(|view| {
940 let cam = view.add(Camera::new());
941 let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
942 let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
943 assert_eq!(screen(dot), (100.0, 50.0));
945 cam.x().set(100.0);
947 cam.y().set(50.0);
948 assert_eq!(screen(dot), (0.0, 0.0));
949 cam.zoom().set(2.0);
951 let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
952 assert_eq!(screen(other), (20.0, 0.0));
953 cam.rotation().set(90.0);
955 assert_eq!(screen(other), (0.0, -20.0));
956 assert_eq!(screen(hud), (-300.0, -200.0));
958 });
959 }
960
961 #[test]
962 fn camera_center_on_fit_and_reset() {
963 play(|view| {
964 let cam = view.add(Camera::new());
965 let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
966 let row = cam.add(
967 Layout::new()
968 .x(-500.0)
969 .gap(10.0)
970 .child(Rect::new().size(50.0))
971 .child(Rect::new().size(50.0)),
972 );
973 Box::pin(async move {
974 cam.center_on(card, 1.0).await;
975 assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
976 assert_eq!(screen(card), (0.0, 0.0));
977 cam.fit(card, 20.0, 0.0).await;
979 assert!(
980 (cam.zoom().get() - 1.8).abs() < 1e-9,
981 "{}",
982 cam.zoom().get()
983 );
984 row.gap().to(110.0, 0.5).await;
986 cam.fit(row, 0.0, 0.0).await;
987 let (w, _) = row.size();
988 assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
989 assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
990 assert!((cam.x().get() + 500.0).abs() < 1e-9);
991 cam.reset(0.5).await;
992 assert_eq!(
993 (
994 cam.x().get(),
995 cam.y().get(),
996 cam.zoom().get(),
997 cam.rotation().get()
998 ),
999 (0.0, 0.0, 1.0, 0.0)
1000 );
1001 })
1002 });
1003 }
1004
1005 #[test]
1006 fn entrance_helpers_start_at_once_and_end_in_place() {
1007 let normal = crate::theme::current().motion.normal;
1008 let expected = (normal * 60.0).round() as u32;
1009 let frames = play(|view| {
1010 let r = view.add(Rect::new().size(100.0).x(50.0));
1011 let pop = r.pop_in();
1012 assert_eq!(r.scale().get(), Vec2::ZERO);
1015 let slide = r.slide_in(Side::Left);
1016 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
1017 assert_eq!(r.opacity().get(), 0.0);
1018 let draw = r.draw_on();
1019 assert_eq!(r.end().get(), 0.0);
1020 Box::pin(async move {
1021 all((pop, slide)).await;
1022 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
1023 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
1024 draw.await;
1025 assert_eq!(r.end().get(), 1.0);
1026 r.fade_out().duration(0.5).await;
1027 assert_eq!(r.opacity().get(), 0.0);
1028 })
1029 });
1030 let slow = crate::theme::current().motion.slow;
1031 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
1032 }
1033
1034 #[test]
1035 fn a_diff_lands_on_the_new_source_and_moves_the_focus() {
1036 play(|view| {
1037 let code = view.add(Code::new().code("a\nkeep\n").focus(2, 2));
1038 Box::pin(async move {
1039 code.insert(1, 0, "NEW\n", 0.2).flash().await;
1040 assert_eq!(code.code().get(), "NEW\na\nkeep\n");
1041 let f = code.focus().get();
1042 assert_eq!((f.x.round() as i32, f.y.round() as i32), (3, 3));
1043 code.replace(((3, 0), (3, 4)), "stay", 0.0).await;
1044 assert_eq!(code.code().get(), "NEW\na\nstay\n");
1045 })
1046 });
1047 }
1048
1049 #[test]
1050 fn add_records_its_call_site_and_scene_bounds() {
1051 let g = scene(|view| {
1052 view.add(Rect::new().size(100.0).name("box"));
1053 view.add(Rect::new().size(10.0).x(100.0));
1054 });
1055 let nodes = g.inspect_nodes();
1056 let named = nodes.iter().find(|n| n.name == "box").unwrap();
1057 assert_eq!(named.kind, "Rect");
1058 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
1059 assert!(
1060 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
1061 "{:?}",
1062 named.bounds
1063 );
1064 let auto = nodes
1065 .iter()
1066 .find(|n| n.name != "box" && n.name != "view")
1067 .unwrap();
1068 assert_eq!(auto.name, auto.source.clone().unwrap());
1069 }
1070
1071 #[test]
1072 fn builders_set_initial_values_and_handles_return_signals() {
1073 scene(|view| {
1074 let r = view.add(
1075 Rect::new()
1076 .size(50.0)
1077 .radius(5.0)
1078 .x(-10.0)
1079 .fill(Color::BLACK),
1080 );
1081 assert_eq!(r.width().get(), 50.0);
1082 assert_eq!(r.height().get(), 50.0);
1083 assert_eq!(r.radius().get(), 5.0);
1084 assert_eq!(r.x().get(), -10.0);
1085 assert_eq!(r.fill().get(), Color::BLACK);
1086 r.x().set(3.0);
1087 assert_eq!(r.x().get(), 3.0);
1088 let c = r.add(Circle::new().size(10.0));
1089 assert_eq!(r.children(), vec![c.node()]);
1090 assert_eq!(view.children(), vec![r.node()]);
1091 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
1092 });
1093 }
1094
1095 #[test]
1096 fn draw_list_composes_transforms() {
1097 let g = scene(|view| {
1098 let grp = view.add(Group::new().x(10.0).scale(2.0));
1099 grp.add(Circle::new().x(5.0));
1100 });
1101 let items = g.draw_list();
1102 assert_eq!(items.len(), 1);
1103 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
1104 }
1105
1106 #[test]
1107 fn layout_places_children_in_a_row() {
1108 let g = scene(|view| {
1109 view.add(
1110 Layout::new()
1111 .gap(10.0)
1112 .padding(5.0)
1113 .child(Rect::new().size(20.0))
1114 .child(Rect::new().width(40.0).height(10.0)),
1115 );
1116 });
1117 let layout = NodeId(1);
1118 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
1119 let centers: Vec<_> = g
1120 .draw_list()
1121 .iter()
1122 .map(|i| i.transform * Point::ZERO)
1123 .collect();
1124 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
1126 }
1127
1128 #[test]
1129 fn column_with_fixed_size_and_alignment() {
1130 let g = scene(|view| {
1131 view.add(
1132 Layout::new()
1133 .column()
1134 .height(100.0)
1135 .width(100.0)
1136 .justify(crate::Justify::Start)
1137 .align(crate::LayoutAlign::Start)
1138 .child(Rect::new().size(20.0)),
1139 );
1140 });
1141 let c = g.draw_list()[0].transform * Point::ZERO;
1142 assert_eq!(c, Point::new(-40.0, -40.0));
1143 }
1144
1145 #[test]
1146 fn text_line_path_and_img_draw() {
1147 let png = {
1148 let mut v = Vec::new();
1149 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
1150 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
1151 .unwrap();
1152 v
1153 };
1154 let g = scene(|view| {
1155 let t = view.add(Txt::new().text("Hi").font_size(40.0));
1156 assert!(t.size().0 > 20.0);
1157 view.add(
1158 Line::new()
1159 .points([(0.0, 0.0), (100.0, 0.0)])
1160 .end_arrow(true),
1161 );
1162 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
1163 let img = view.add(Img::new().src(&png).width(40.0));
1164 assert_eq!(img.height().get(), 20.0);
1165 });
1166 let kinds: Vec<_> = g
1167 .draw_list()
1168 .iter()
1169 .map(|i| match i.prim {
1170 Primitive::Fill { .. } => "fill",
1171 Primitive::Stroke { .. } => "stroke",
1172 Primitive::Image { .. } => "image",
1173 Primitive::PushLayer(_) => "push",
1174 Primitive::PopLayer => "pop",
1175 })
1176 .collect();
1177 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
1178 }
1179
1180 #[test]
1181 fn trimming_and_pick() {
1182 let g = scene(|view| {
1183 view.add(Rect::new().size(100.0).name("back"));
1184 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
1185 });
1186 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
1187 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
1188 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
1190 }
1191
1192 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
1193 g.draw_list()
1194 .iter()
1195 .map(|i| match i.prim {
1196 Primitive::Fill { .. } => "fill",
1197 Primitive::Stroke { .. } => "stroke",
1198 Primitive::Image { .. } => "image",
1199 Primitive::PushLayer(_) => "push",
1200 Primitive::PopLayer => "pop",
1201 })
1202 .collect()
1203 }
1204
1205 #[test]
1206 fn layers_only_when_needed_and_balanced() {
1207 let g = scene(|v| {
1209 v.add(Rect::new().opacity(0.5));
1210 });
1211 assert_eq!(kinds(&g), ["fill"]);
1212 let g = scene(|v| {
1214 v.add(
1215 Group::new()
1216 .opacity(0.5)
1217 .child(Rect::new())
1218 .child(Circle::new()),
1219 );
1220 });
1221 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
1222 let g = scene(|v| {
1224 v.add(
1225 Circle::new()
1226 .clip(true)
1227 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
1228 .child(Rect::new()),
1229 );
1230 });
1231 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
1232 }
1233
1234 #[test]
1235 fn z_index_orders_siblings_and_anchor_moves_box() {
1236 let g = scene(|v| {
1237 v.add(Rect::new().name("a").z_index(2.0));
1238 v.add(Rect::new().name("b"));
1239 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
1240 });
1241 let items = g.draw_list();
1242 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
1243 assert_eq!(order, vec![2, 3, 1]);
1244 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
1246 }
1247
1248 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
1249 g.draw_list()
1250 .iter()
1251 .filter_map(|i| match &i.prim {
1252 Primitive::Fill { path, .. } => {
1253 let bb = kurbo::Shape::bounding_box(path);
1254 Some((i.transform * Point::ZERO, bb.width()))
1255 }
1256 _ => None,
1257 })
1258 .collect()
1259 }
1260
1261 #[test]
1262 fn flex_grow_stretch_wrap_and_grid() {
1263 use crate::{Justify, LayoutAlign};
1264 let g = scene(|v| {
1266 v.add(
1267 Layout::new()
1268 .width(200.0)
1269 .justify(Justify::Start)
1270 .child(Rect::new().size(40.0))
1271 .child(Rect::new().size(40.0).grow(1.0)),
1272 );
1273 });
1274 let b = boxes(&g);
1275 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
1276 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
1277
1278 let g = scene(|v| {
1280 v.add(
1281 Layout::new()
1282 .column()
1283 .width(120.0)
1284 .align(LayoutAlign::Stretch)
1285 .child(Rect::new().size(30.0)),
1286 );
1287 });
1288 assert_eq!(boxes(&g)[0].1, 120.0);
1289
1290 let g = scene(|v| {
1292 v.add(
1293 Layout::new()
1294 .width(130.0)
1295 .wrap(true)
1296 .child(Rect::new().size(60.0))
1297 .child(Rect::new().size(60.0))
1298 .child(Rect::new().size(60.0)),
1299 );
1300 });
1301 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
1302 assert_eq!(ys[0], ys[1]);
1303 assert!(ys[2] > ys[0]);
1304
1305 let g = scene(|v| {
1307 v.add(
1308 Layout::new()
1309 .grid(2)
1310 .width(200.0)
1311 .align(LayoutAlign::Stretch)
1312 .child(Rect::new().size(10.0))
1313 .child(Rect::new().size(10.0))
1314 .child(Rect::new().size(10.0)),
1315 );
1316 });
1317 let b = boxes(&g);
1318 assert_eq!(
1319 b.iter().map(|b| b.1).collect::<Vec<_>>(),
1320 vec![100.0, 100.0, 100.0]
1321 );
1322 assert_eq!(b[2].0.x, b[0].0.x);
1323 assert!(b[2].0.y > b[0].0.y);
1324 }
1325
1326 #[test]
1327 fn describe_lists_signals() {
1328 let g = scene(|view| {
1329 view.add(Txt::new().text("abc"));
1330 });
1331 let d = g.describe(NodeId(1));
1332 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
1333 }
1334}