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 effect(self, effect: crate::builder::EffectBuilder) -> Effect {
207 with_graph(|g| g.set_effect(self.0, effect.build(self.0)));
208 Effect(self.0)
209 }
210
211 pub fn children(self) -> Vec<Node> {
213 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
214 }
215
216 pub fn size(self) -> (f64, f64) {
218 with_graph(|g| g.measure(self.0))
219 }
220
221 pub fn absolute_position(self) -> Vec2 {
223 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
224 }
225}
226
227macro_rules! typed_handle {
228 ($name:ident, $builder:ident) => {
229 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
230 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
231 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
232 pub struct $name(Node);
233
234 impl $name {
235 #[allow(clippy::new_ret_no_self)]
237 pub fn new() -> $builder {
238 $builder::default()
239 }
240
241 pub(crate) fn from_id(id: NodeId) -> Self {
242 Self(Node(id))
243 }
244
245 pub fn node(self) -> Node {
247 self.0
248 }
249 }
250
251 impl Deref for $name {
252 type Target = Node;
253 fn deref(&self) -> &Node {
254 &self.0
255 }
256 }
257
258 impl From<$name> for Node {
259 fn from(h: $name) -> Node {
260 h.0
261 }
262 }
263 };
264}
265
266typed_handle!(Group, GroupBuilder);
267typed_handle!(Camera, CameraBuilder);
268typed_handle!(Rect, RectBuilder);
269typed_handle!(Circle, CircleBuilder);
270typed_handle!(Path, PathBuilder);
271typed_handle!(Line, LineBuilder);
272typed_handle!(Txt, TxtBuilder);
273typed_handle!(Img, ImgBuilder);
274typed_handle!(Svg, SvgBuilder);
275typed_handle!(Layout, LayoutBuilder);
276typed_handle!(Grid, GridBuilder);
277typed_handle!(Polygon, PolygonBuilder);
278typed_handle!(Star, StarBuilder);
279typed_handle!(Code, CodeBuilder);
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
283pub struct Custom(Node);
284
285impl Custom {
286 #[allow(clippy::new_ret_no_self)]
290 pub fn new(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> crate::CustomBuilder {
291 crate::CustomBuilder::from_draw(draw)
292 }
293
294 pub(crate) fn from_id(id: NodeId) -> Self {
295 Self(Node(id))
296 }
297
298 pub fn node(self) -> Node {
300 self.0
301 }
302}
303
304impl Deref for Custom {
305 type Target = Node;
306 fn deref(&self) -> &Node {
307 &self.0
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313pub struct Shader(Node);
314
315impl Shader {
316 #[allow(clippy::new_ret_no_self)]
322 pub fn source(wgsl: impl Into<String>) -> ShaderBuilder {
323 ShaderBuilder::new(crate::shader::ShaderCode::Wgsl(wgsl.into()))
324 }
325
326 pub fn file(path: impl AsRef<std::path::Path>) -> ShaderBuilder {
329 ShaderBuilder::new(crate::shader::ShaderCode::File(path.as_ref().into()))
330 }
331
332 pub(crate) fn from_id(id: NodeId) -> Self {
333 Self(Node(id))
334 }
335
336 pub fn node(self) -> Node {
338 self.0
339 }
340
341 pub fn uniform(self, name: &str) -> Signal<f64> {
348 self.number(name)
349 }
350
351 pub fn amp(self) -> Signal<f64> {
353 self.number("amp")
354 }
355
356 pub fn speed(self) -> Signal<f64> {
358 self.number("speed")
359 }
360
361 pub fn deep(self) -> Signal<Color> {
363 self.color("deep")
364 }
365
366 pub fn foam(self) -> Signal<Color> {
368 self.color("foam")
369 }
370
371 pub fn wetness(self) -> Signal<f64> {
373 self.number("wetness")
374 }
375
376 fn number(self, name: &str) -> Signal<f64> {
377 with_graph(|g| {
378 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
379 unreachable!("handle does not point to a Shader");
380 };
381 for u in uniforms {
382 if u.name == name
383 && let crate::shader::UniformValue::Num(v) = &u.value
384 {
385 return v.clone();
386 }
387 }
388 panic!("no numeric uniform {name}. {}", uniform_names(uniforms));
389 })
390 }
391
392 fn color(self, name: &str) -> Signal<Color> {
393 with_graph(|g| {
394 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
395 unreachable!("handle does not point to a Shader");
396 };
397 for u in uniforms {
398 if u.name == name
399 && let crate::shader::UniformValue::Color(v) = &u.value
400 {
401 return v.clone();
402 }
403 }
404 panic!("no color uniform {name}. {}", uniform_names(uniforms));
405 })
406 }
407}
408
409fn uniform_names(uniforms: &[crate::shader::Uniform]) -> String {
410 let names: Vec<&str> = uniforms.iter().map(|u| u.name.as_str()).collect();
411 format!("Available: {}.", names.join(", "))
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
416pub struct Effect(NodeId);
417
418impl Effect {
419 pub fn node(self) -> Node {
421 Node(self.0)
422 }
423
424 pub fn strength(self) -> Signal<f64> {
426 self.data().strength
427 }
428
429 pub fn speed(self) -> Signal<f64> {
431 self.data().speed
432 }
433
434 fn data(self) -> crate::post::EffectData {
435 with_graph(|g| {
436 g.node(self.0)
437 .effect
438 .clone()
439 .expect("an Effect handle always points to a node with an effect")
440 })
441 }
442}
443
444impl Deref for Shader {
445 type Target = Node;
446 fn deref(&self) -> &Node {
447 &self.0
448 }
449}
450
451impl From<Custom> for Node {
452 fn from(h: Custom) -> Node {
453 h.0
454 }
455}
456
457impl From<Shader> for Node {
458 fn from(h: Shader) -> Node {
459 h.0
460 }
461}
462
463typed_handle!(Particles, ParticlesBuilder);
464
465macro_rules! kind_signal {
466 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
467 impl $handle {
468 $(
469 $(#[$doc])*
470 #[doc = ""]
471 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
472 pub fn $field(self) -> Signal<$ty> {
473 with_graph(|g| match &g.node(self.id()).kind {
474 NodeKind::$kind { $field, .. } => $field.clone(),
475 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
476 })
477 }
478 )*
479 }
480 };
481}
482
483kind_signal!(Particles::Particles { gravity: f64 });
484
485impl Particles {
486 pub fn burst(self, origin: impl Into<Vec2>, count: u32) -> oxframe_core::WaitFor {
492 let origin = origin.into();
493 let time = oxframe_core::time();
494 with_graph(|g| {
495 if let NodeKind::Particles { state, .. } = &g.node(self.id()).kind {
496 state.bursts.borrow_mut().push(crate::sim::Burst {
497 time,
498 origin,
499 count,
500 });
501 }
502 });
503 oxframe_core::wait_for(0.0)
504 }
505}
506
507kind_signal!(Rect::Rect {
508 width: f64,
509 height: f64,
510 radius: f64
511});
512kind_signal!(Circle::Circle {
513 size: f64,
515 start_angle: f64,
516 end_angle: f64,
517});
518kind_signal!(Polygon::Polygon {
519 sides: f64,
520 size: f64
521});
522kind_signal!(Star::Star {
523 points: f64,
524 size: f64,
525 inner: f64
526});
527kind_signal!(Path::Path { data: BezPath });
528kind_signal!(Camera::Camera {
529 zoom: f64
531});
532
533impl Camera {
534 pub fn center_on(
536 self,
537 node: impl Into<Node>,
538 duration: impl Seconds,
539 ) -> impl std::future::Future<Output = ()> + 'static {
540 let node = node.into();
541 let d = duration.seconds();
542 let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
543 let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
544 async move {
545 if let Some(m) = moves {
546 m.await;
547 }
548 }
549 }
550
551 pub fn fit(
555 self,
556 node: impl Into<Node>,
557 padding: f64,
558 duration: impl Seconds,
559 ) -> impl std::future::Future<Output = ()> + 'static {
560 let node = node.into();
561 let d = duration.seconds();
562 let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
563 let moves = bounds.map(|r| {
564 let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
565 .min((fh - 2.0 * padding) / r.height().max(1e-6))
566 .max(1e-3);
567 let c = r.center();
568 all((
569 self.x().to(c.x, d),
570 self.y().to(c.y, d),
571 self.zoom().to(zoom, d),
572 ))
573 });
574 async move {
575 if let Some(m) = moves {
576 m.await;
577 }
578 }
579 }
580
581 pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
583 let d = duration.seconds();
584 let moves = all((
585 self.x().to(0.0, d),
586 self.y().to(0.0, d),
587 self.zoom().to(1.0, d),
588 self.rotation().to(0.0, d),
589 ));
590 async move {
591 moves.await;
592 }
593 }
594}
595kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
596kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
597kind_signal!(Code::Code {
598 font_size: f64,
599 focus: Vec2,
601 dim: f64,
603});
604
605impl Txt {
606 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
609 let to = text.into();
610 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
611 NodeKind::Txt {
612 text,
613 morph,
614 morph_t,
615 ..
616 } => (text.clone(), morph.clone(), morph_t.clone()),
617 _ => unreachable!("handle does not point to a Txt"),
618 });
619 Box::pin(async move {
620 morph.set(Some((txt.get(), to.clone())));
621 t.set(0.0);
622 t.to(1.0, duration).await;
623 txt.set(to);
624 morph.set(None);
625 })
626 }
627}
628
629impl Code {
630 pub fn code(self) -> CodeText {
638 with_graph(|g| match &g.node(self.id()).kind {
639 NodeKind::Code {
640 code,
641 edit_from,
642 edit_to,
643 editing,
644 edit_t,
645 edit_flash,
646 focus,
647 lang,
648 ..
649 } => CodeText {
650 code: code.clone(),
651 edit_from: edit_from.clone(),
652 edit_to: edit_to.clone(),
653 editing: editing.clone(),
654 edit_t: edit_t.clone(),
655 edit_flash: edit_flash.clone(),
656 focus: focus.clone(),
657 lang: *lang,
658 },
659 _ => unreachable!("handle does not point to a Code"),
660 })
661 }
662
663 pub fn focus_lines(
669 self,
670 first: usize,
671 last: usize,
672 duration: f64,
673 ) -> oxframe_core::Tween<Vec2> {
674 self.focus()
675 .to(Vec2::new(first as f64, last as f64), duration)
676 }
677
678 pub fn insert(
685 self,
686 line: usize,
687 col: usize,
688 text: impl Into<String>,
689 duration: impl Seconds,
690 ) -> CodeTween {
691 let text = text.into();
692 let src = self.code().get();
693 let next = oxframe_text::insert_at(&src, line, col, &text)
694 .unwrap_or_else(|e| panic!("code.insert: {e}"));
695 self.code().to(next, duration)
696 }
697
698 pub fn remove(
707 self,
708 range: ((usize, usize), (usize, usize)),
709 duration: impl Seconds,
710 ) -> CodeTween {
711 self.rewrite(range, String::new(), duration, "code.remove")
712 }
713
714 pub fn replace(
721 self,
722 range: ((usize, usize), (usize, usize)),
723 text: impl Into<String>,
724 duration: impl Seconds,
725 ) -> CodeTween {
726 self.rewrite(range, text.into(), duration, "code.replace")
727 }
728
729 fn rewrite(
730 self,
731 ((l0, c0), (l1, c1)): ((usize, usize), (usize, usize)),
732 text: String,
733 duration: impl Seconds,
734 what: &str,
735 ) -> CodeTween {
736 let src = self.code().get();
737 let next = oxframe_text::replace_range(
738 &src,
739 oxframe_text::Loc { line: l0, col: c0 },
740 oxframe_text::Loc { line: l1, col: c1 },
741 &text,
742 )
743 .unwrap_or_else(|e| panic!("{what}: {e}"));
744 self.code().to(next, duration)
745 }
746}
747
748#[derive(Clone)]
754pub struct CodeText {
755 code: Signal<String>,
756 edit_from: Signal<String>,
757 edit_to: Signal<String>,
758 editing: Signal<bool>,
759 edit_t: Signal<f64>,
760 edit_flash: Signal<bool>,
761 focus: Signal<Vec2>,
762 lang: oxframe_text::Lang,
763}
764
765impl std::ops::Deref for CodeText {
766 type Target = Signal<String>;
767 fn deref(&self) -> &Signal<String> {
768 &self.code
769 }
770}
771
772impl CodeText {
773 pub fn set(&self, value: impl Into<String>) {
775 self.editing.set(false);
776 self.code.set(value.into());
777 }
778
779 pub fn bind(&self, f: impl Fn() -> String + 'static) {
781 self.editing.set(false);
782 self.code.bind(f);
783 }
784
785 pub fn to(&self, target: impl Into<String>, duration: impl Seconds) -> CodeTween {
787 let target = target.into();
788 let from = self.code.get();
789 self.edit_from.set(from.clone());
790 self.edit_to.set(target.clone());
791 self.edit_flash.set(false);
792 self.editing.set(true);
793 self.edit_t.set(0.0);
794 CodeTween {
795 tween: self.edit_t.to(1.0, duration),
796 code: self.code.clone(),
797 editing: self.editing.clone(),
798 edit_flash: self.edit_flash.clone(),
799 focus: self.focus.clone(),
800 lang: self.lang,
801 from,
802 target,
803 finished: false,
804 }
805 }
806}
807
808#[must_use = "a tween does nothing unless awaited"]
811pub struct CodeTween {
812 tween: Tween<f64>,
813 code: Signal<String>,
814 editing: Signal<bool>,
815 edit_flash: Signal<bool>,
816 focus: Signal<Vec2>,
817 lang: oxframe_text::Lang,
818 from: String,
819 target: String,
820 finished: bool,
821}
822
823impl CodeTween {
824 pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
826 self.tween = self.tween.ease(ease);
827 self
828 }
829
830 pub fn duration(mut self, duration: impl Seconds) -> Self {
832 self.tween = self.tween.duration(duration);
833 self
834 }
835
836 pub fn flash(self) -> Self {
838 self.edit_flash.set(true);
839 self
840 }
841}
842
843impl Future for CodeTween {
844 type Output = ();
845
846 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
847 let this = self.get_mut();
848 if Pin::new(&mut this.tween).poll(cx).is_pending() {
849 return Poll::Pending;
850 }
851 if !this.finished {
852 this.finished = true;
853 let focus = this.focus.get();
854 let (a, b) = (focus.x.round() as i64, focus.y.round() as i64);
855 if a > 0 || b > 0 {
856 let diff = oxframe_text::diff_code(&this.from, &this.target, this.lang);
857 if let Some((n0, n1)) =
858 oxframe_text::map_focus(&diff, a.max(0) as usize, b.max(0) as usize)
859 {
860 this.focus.set(Vec2::new(n0 as f64, n1 as f64));
861 }
862 }
863 this.code.set(this.target.clone());
864 this.editing.set(false);
865 }
866 Poll::Ready(())
867 }
868}
869kind_signal!(Svg::Svg {
870 width: f64,
871 height: f64
872});
873
874impl Svg {
875 pub fn find(self, id: &str) -> Node {
881 let id = with_graph(|g| g.descendant_named(self.id(), id))
882 .unwrap_or_else(|| panic!("svg has no element with id {id:?}"));
883 Node::from_id(id)
884 }
885
886 pub fn paths(self) -> Vec<Path> {
888 with_graph(|g| {
889 g.descendant_paths(self.id())
890 .into_iter()
891 .map(Path::from_id)
892 .collect()
893 })
894 }
895}
896
897kind_signal!(Img::Img {
898 width: f64,
899 height: f64
900});
901kind_signal!(Layout::Layout {
902 gap: f64,
903 padding: f64,
904 radius: f64,
905 width: Option<f64>,
907 height: Option<f64>,
909});
910kind_signal!(Grid::Grid {
911 width: f64,
912 height: f64,
913 spacing: f64
914});
915
916#[derive(Debug, Clone, Copy, PartialEq, Eq)]
918pub struct View(Node);
919
920impl View {
921 pub fn current() -> Self {
923 Self(Node(SceneGraph::ROOT))
924 }
925
926 pub fn size(self) -> (u32, u32) {
928 with_graph(|g| g.size())
929 }
930
931 pub fn is_portrait(self) -> bool {
933 let (w, h) = self.size();
934 h > w
935 }
936
937 pub fn safe_area(self) -> SafeArea {
941 let (w, h) = self.size();
942 SafeArea::for_frame(f64::from(w), f64::from(h))
943 }
944}
945
946impl Deref for View {
947 type Target = Node;
948 fn deref(&self) -> &Node {
949 &self.0
950 }
951}
952
953impl From<View> for Node {
954 fn from(v: View) -> Node {
955 v.0
956 }
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use crate::Primitive;
963
964 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
965 let graph = SceneGraph::new(400, 400);
966 graph.enter(|| f(View::current()));
967 graph
968 }
969
970 fn play(
972 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
973 ) -> u32 {
974 let graph = SceneGraph::new(400, 400);
975 graph.enter(|| {
976 let fut = scene_fn(View::current());
977 let mut ex =
978 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
979 let mut frames = 0;
980 while !ex.step() {
981 frames += 1;
982 assert!(frames < 10_000, "scene did not finish");
983 }
984 frames
985 })
986 }
987
988 fn screen(n: impl Into<Node>) -> (f64, f64) {
990 let p = n.into().absolute_position();
991 ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
992 }
993
994 #[test]
995 fn camera_pans_zooms_and_turns_its_children_only() {
996 scene(|view| {
997 let cam = view.add(Camera::new());
998 let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
999 let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
1000 assert_eq!(screen(dot), (100.0, 50.0));
1002 cam.x().set(100.0);
1004 cam.y().set(50.0);
1005 assert_eq!(screen(dot), (0.0, 0.0));
1006 cam.zoom().set(2.0);
1008 let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
1009 assert_eq!(screen(other), (20.0, 0.0));
1010 cam.rotation().set(90.0);
1012 assert_eq!(screen(other), (0.0, -20.0));
1013 assert_eq!(screen(hud), (-300.0, -200.0));
1015 });
1016 }
1017
1018 #[test]
1019 fn camera_center_on_fit_and_reset() {
1020 play(|view| {
1021 let cam = view.add(Camera::new());
1022 let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
1023 let row = cam.add(
1024 Layout::new()
1025 .x(-500.0)
1026 .gap(10.0)
1027 .child(Rect::new().size(50.0))
1028 .child(Rect::new().size(50.0)),
1029 );
1030 Box::pin(async move {
1031 cam.center_on(card, 1.0).await;
1032 assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
1033 assert_eq!(screen(card), (0.0, 0.0));
1034 cam.fit(card, 20.0, 0.0).await;
1036 assert!(
1037 (cam.zoom().get() - 1.8).abs() < 1e-9,
1038 "{}",
1039 cam.zoom().get()
1040 );
1041 row.gap().to(110.0, 0.5).await;
1043 cam.fit(row, 0.0, 0.0).await;
1044 let (w, _) = row.size();
1045 assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
1046 assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
1047 assert!((cam.x().get() + 500.0).abs() < 1e-9);
1048 cam.reset(0.5).await;
1049 assert_eq!(
1050 (
1051 cam.x().get(),
1052 cam.y().get(),
1053 cam.zoom().get(),
1054 cam.rotation().get()
1055 ),
1056 (0.0, 0.0, 1.0, 0.0)
1057 );
1058 })
1059 });
1060 }
1061
1062 #[test]
1063 fn entrance_helpers_start_at_once_and_end_in_place() {
1064 let normal = crate::theme::current().motion.normal;
1065 let expected = (normal * 60.0).round() as u32;
1066 let frames = play(|view| {
1067 let r = view.add(Rect::new().size(100.0).x(50.0));
1068 let pop = r.pop_in();
1069 assert_eq!(r.scale().get(), Vec2::ZERO);
1072 let slide = r.slide_in(Side::Left);
1073 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
1074 assert_eq!(r.opacity().get(), 0.0);
1075 let draw = r.draw_on();
1076 assert_eq!(r.end().get(), 0.0);
1077 Box::pin(async move {
1078 all((pop, slide)).await;
1079 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
1080 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
1081 draw.await;
1082 assert_eq!(r.end().get(), 1.0);
1083 r.fade_out().duration(0.5).await;
1084 assert_eq!(r.opacity().get(), 0.0);
1085 })
1086 });
1087 let slow = crate::theme::current().motion.slow;
1088 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
1089 }
1090
1091 #[test]
1092 fn a_diff_lands_on_the_new_source_and_moves_the_focus() {
1093 play(|view| {
1094 let code = view.add(Code::new().code("a\nkeep\n").focus(2, 2));
1095 Box::pin(async move {
1096 code.insert(1, 0, "NEW\n", 0.2).flash().await;
1097 assert_eq!(code.code().get(), "NEW\na\nkeep\n");
1098 let f = code.focus().get();
1099 assert_eq!((f.x.round() as i32, f.y.round() as i32), (3, 3));
1100 code.replace(((3, 0), (3, 4)), "stay", 0.0).await;
1101 assert_eq!(code.code().get(), "NEW\na\nstay\n");
1102 })
1103 });
1104 }
1105
1106 #[test]
1107 fn add_records_its_call_site_and_scene_bounds() {
1108 let g = scene(|view| {
1109 view.add(Rect::new().size(100.0).name("box"));
1110 view.add(Rect::new().size(10.0).x(100.0));
1111 });
1112 let nodes = g.inspect_nodes();
1113 let named = nodes.iter().find(|n| n.name == "box").unwrap();
1114 assert_eq!(named.kind, "Rect");
1115 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
1116 assert!(
1117 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
1118 "{:?}",
1119 named.bounds
1120 );
1121 let auto = nodes
1122 .iter()
1123 .find(|n| n.name != "box" && n.name != "view")
1124 .unwrap();
1125 assert_eq!(auto.name, auto.source.clone().unwrap());
1126 }
1127
1128 #[test]
1129 fn builders_set_initial_values_and_handles_return_signals() {
1130 scene(|view| {
1131 let r = view.add(
1132 Rect::new()
1133 .size(50.0)
1134 .radius(5.0)
1135 .x(-10.0)
1136 .fill(Color::BLACK),
1137 );
1138 assert_eq!(r.width().get(), 50.0);
1139 assert_eq!(r.height().get(), 50.0);
1140 assert_eq!(r.radius().get(), 5.0);
1141 assert_eq!(r.x().get(), -10.0);
1142 assert_eq!(r.fill().get(), Color::BLACK);
1143 r.x().set(3.0);
1144 assert_eq!(r.x().get(), 3.0);
1145 let c = r.add(Circle::new().size(10.0));
1146 assert_eq!(r.children(), vec![c.node()]);
1147 assert_eq!(view.children(), vec![r.node()]);
1148 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
1149 });
1150 }
1151
1152 #[test]
1153 fn draw_list_composes_transforms() {
1154 let g = scene(|view| {
1155 let grp = view.add(Group::new().x(10.0).scale(2.0));
1156 grp.add(Circle::new().x(5.0));
1157 });
1158 let items = g.draw_list();
1159 assert_eq!(items.len(), 1);
1160 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
1161 }
1162
1163 #[test]
1164 fn layout_places_children_in_a_row() {
1165 let g = scene(|view| {
1166 view.add(
1167 Layout::new()
1168 .gap(10.0)
1169 .padding(5.0)
1170 .child(Rect::new().size(20.0))
1171 .child(Rect::new().width(40.0).height(10.0)),
1172 );
1173 });
1174 let layout = NodeId(1);
1175 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
1176 let centers: Vec<_> = g
1177 .draw_list()
1178 .iter()
1179 .map(|i| i.transform * Point::ZERO)
1180 .collect();
1181 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
1183 }
1184
1185 #[test]
1186 fn column_with_fixed_size_and_alignment() {
1187 let g = scene(|view| {
1188 view.add(
1189 Layout::new()
1190 .column()
1191 .height(100.0)
1192 .width(100.0)
1193 .justify(crate::Justify::Start)
1194 .align(crate::LayoutAlign::Start)
1195 .child(Rect::new().size(20.0)),
1196 );
1197 });
1198 let c = g.draw_list()[0].transform * Point::ZERO;
1199 assert_eq!(c, Point::new(-40.0, -40.0));
1200 }
1201
1202 #[test]
1203 fn text_line_path_and_img_draw() {
1204 let png = {
1205 let mut v = Vec::new();
1206 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
1207 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
1208 .unwrap();
1209 v
1210 };
1211 let g = scene(|view| {
1212 let t = view.add(Txt::new().text("Hi").font_size(40.0));
1213 assert!(t.size().0 > 20.0);
1214 view.add(
1215 Line::new()
1216 .points([(0.0, 0.0), (100.0, 0.0)])
1217 .end_arrow(true),
1218 );
1219 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
1220 let img = view.add(Img::new().src(&png).width(40.0));
1221 assert_eq!(img.height().get(), 20.0);
1222 });
1223 let kinds: Vec<_> = g
1224 .draw_list()
1225 .iter()
1226 .map(|i| match i.prim {
1227 Primitive::Fill { .. } => "fill",
1228 Primitive::Stroke { .. } => "stroke",
1229 Primitive::Image { .. } => "image",
1230 Primitive::PushLayer(_) => "push",
1231 Primitive::PopLayer => "pop",
1232 Primitive::ReadFrame(_) => "frame",
1233 Primitive::BeginEffect(_) => "effect",
1234 Primitive::EndEffect => "end_effect",
1235 })
1236 .collect();
1237 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
1238 }
1239
1240 #[test]
1241 fn trimming_and_pick() {
1242 let g = scene(|view| {
1243 view.add(Rect::new().size(100.0).name("back"));
1244 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
1245 });
1246 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
1247 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
1248 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
1250 }
1251
1252 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
1253 g.draw_list()
1254 .iter()
1255 .map(|i| match i.prim {
1256 Primitive::Fill { .. } => "fill",
1257 Primitive::Stroke { .. } => "stroke",
1258 Primitive::Image { .. } => "image",
1259 Primitive::PushLayer(_) => "push",
1260 Primitive::PopLayer => "pop",
1261 Primitive::ReadFrame(_) => "frame",
1262 Primitive::BeginEffect(_) => "effect",
1263 Primitive::EndEffect => "end_effect",
1264 })
1265 .collect()
1266 }
1267
1268 #[test]
1269 fn layers_only_when_needed_and_balanced() {
1270 let g = scene(|v| {
1272 v.add(Rect::new().opacity(0.5));
1273 });
1274 assert_eq!(kinds(&g), ["fill"]);
1275 let g = scene(|v| {
1277 v.add(
1278 Group::new()
1279 .opacity(0.5)
1280 .child(Rect::new())
1281 .child(Circle::new()),
1282 );
1283 });
1284 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
1285 let g = scene(|v| {
1287 v.add(
1288 Circle::new()
1289 .clip(true)
1290 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
1291 .child(Rect::new()),
1292 );
1293 });
1294 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
1295 }
1296
1297 #[test]
1298 fn z_index_orders_siblings_and_anchor_moves_box() {
1299 let g = scene(|v| {
1300 v.add(Rect::new().name("a").z_index(2.0));
1301 v.add(Rect::new().name("b"));
1302 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
1303 });
1304 let items = g.draw_list();
1305 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
1306 assert_eq!(order, vec![2, 3, 1]);
1307 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
1309 }
1310
1311 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
1312 g.draw_list()
1313 .iter()
1314 .filter_map(|i| match &i.prim {
1315 Primitive::Fill { path, .. } => {
1316 let bb = kurbo::Shape::bounding_box(path);
1317 Some((i.transform * Point::ZERO, bb.width()))
1318 }
1319 _ => None,
1320 })
1321 .collect()
1322 }
1323
1324 #[test]
1325 fn flex_grow_stretch_wrap_and_grid() {
1326 use crate::{Justify, LayoutAlign};
1327 let g = scene(|v| {
1329 v.add(
1330 Layout::new()
1331 .width(200.0)
1332 .justify(Justify::Start)
1333 .child(Rect::new().size(40.0))
1334 .child(Rect::new().size(40.0).grow(1.0)),
1335 );
1336 });
1337 let b = boxes(&g);
1338 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
1339 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
1340
1341 let g = scene(|v| {
1343 v.add(
1344 Layout::new()
1345 .column()
1346 .width(120.0)
1347 .align(LayoutAlign::Stretch)
1348 .child(Rect::new().size(30.0)),
1349 );
1350 });
1351 assert_eq!(boxes(&g)[0].1, 120.0);
1352
1353 let g = scene(|v| {
1355 v.add(
1356 Layout::new()
1357 .width(130.0)
1358 .wrap(true)
1359 .child(Rect::new().size(60.0))
1360 .child(Rect::new().size(60.0))
1361 .child(Rect::new().size(60.0)),
1362 );
1363 });
1364 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
1365 assert_eq!(ys[0], ys[1]);
1366 assert!(ys[2] > ys[0]);
1367
1368 let g = scene(|v| {
1370 v.add(
1371 Layout::new()
1372 .grid(2)
1373 .width(200.0)
1374 .align(LayoutAlign::Stretch)
1375 .child(Rect::new().size(10.0))
1376 .child(Rect::new().size(10.0))
1377 .child(Rect::new().size(10.0)),
1378 );
1379 });
1380 let b = boxes(&g);
1381 assert_eq!(
1382 b.iter().map(|b| b.1).collect::<Vec<_>>(),
1383 vec![100.0, 100.0, 100.0]
1384 );
1385 assert_eq!(b[2].0.x, b[0].0.x);
1386 assert!(b[2].0.y > b[0].0.y);
1387 }
1388
1389 #[test]
1390 fn describe_lists_signals() {
1391 let g = scene(|view| {
1392 view.add(Txt::new().text("abc"));
1393 });
1394 let d = g.describe(NodeId(1));
1395 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
1396 }
1397}