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 opacity: f64,
60 fill: Color,
61 stroke: Color,
62 line_width: f64,
63 start: f64,
65 end: f64,
67 fill_gradient: Option<Gradient>,
68 stroke_gradient: Option<Gradient>,
69 dash: Vec<f64>,
70 dash_offset: f64,
71 line_cap: LineCap,
72 line_join: LineJoin,
73 blur: f64,
74 shadow_color: Color,
75 shadow_blur: f64,
76 shadow_offset: Vec2,
77 clip: bool,
78 blend: BlendMode,
79 z_index: f64,
80 anchor: Vec2,
81 grow: f64,
82 shrink: f64,
83 basis: Option<f64>,
84 margin: f64,
85 align_self: Option<crate::LayoutAlign>,
86 }
87
88 pub fn scale(self) -> crate::ScaleSignal {
92 crate::ScaleSignal(with_graph(|g| g.node(self.0).scale.clone()))
93 }
94
95 #[allow(clippy::should_implement_trait)] #[track_caller]
101 pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
102 let loc = std::panic::Location::caller();
103 let prev = crate::graph::push_add_at(loc.file(), loc.line());
104 let handle = builder::add(self.0, child);
105 crate::graph::pop_add_at(prev);
106 handle
107 }
108
109 pub fn remove(self) {
111 with_graph(|g| g.detach(self.0));
112 }
113
114 pub fn fade_in(self) -> Tween<f64> {
117 let motion = crate::theme::current().motion;
118 self.opacity().set(0.0);
119 self.opacity().to(1.0, motion.normal).ease(motion.enter)
120 }
121
122 pub fn pop_in(self) -> Tween<Vec2> {
125 let motion = crate::theme::current().motion;
126 let full = self.scale().peek();
127 self.scale().set(Vec2::ZERO);
128 self.scale().to(full, motion.normal).ease(motion.emphasis)
129 }
130
131 pub fn draw_on(self) -> Tween<f64> {
135 let motion = crate::theme::current().motion;
136 self.end().set(0.0);
137 self.end().to(1.0, motion.slow).ease(motion.enter)
138 }
139
140 pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
143 let motion = crate::theme::current().motion;
144 let (axis, offset) = match from {
145 Side::Left => (self.x(), -SLIDE_DISTANCE),
146 Side::Right => (self.x(), SLIDE_DISTANCE),
147 Side::Top => (self.y(), -SLIDE_DISTANCE),
148 Side::Bottom => (self.y(), SLIDE_DISTANCE),
149 };
150 let home = axis.peek();
151 axis.set(home + offset);
152 let moves = all((
153 axis.to(home, motion.normal).ease(motion.enter),
154 self.fade_in(),
155 ));
156 async move {
157 moves.await;
158 }
159 }
160
161 pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
164 let other = other.into();
165 with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
166 self
167 }
168
169 pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
171 let other = other.into();
172 with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
173 self
174 }
175
176 pub fn to_edge(self, edge: Edge) -> Self {
178 with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
179 self
180 }
181
182 pub fn center_in(self, other: impl Into<Node>) -> Self {
184 let other = other.into();
185 with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
186 self
187 }
188
189 pub fn fade_out(self) -> Tween<f64> {
191 let motion = crate::theme::current().motion;
192 self.opacity().to(0.0, motion.normal).ease(motion.exit)
193 }
194
195 pub fn reparent(self, parent: impl Into<Node>) {
197 let parent = parent.into();
198 with_graph(|g| g.reparent(self.0, parent.0));
199 }
200
201 pub fn effect(self, effect: crate::builder::EffectBuilder) -> Effect {
213 with_graph(|g| g.set_effect(self.0, effect.build(self.0)));
214 Effect(self.0)
215 }
216
217 pub fn children(self) -> Vec<Node> {
219 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
220 }
221
222 pub fn size(self) -> (f64, f64) {
224 with_graph(|g| g.measure(self.0))
225 }
226
227 pub fn absolute_position(self) -> Vec2 {
229 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
230 }
231}
232
233macro_rules! typed_handle {
234 ($name:ident, $builder:ident) => {
235 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
236 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
237 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
238 pub struct $name(Node);
239
240 impl $name {
241 #[allow(clippy::new_ret_no_self)]
243 pub fn new() -> $builder {
244 $builder::default()
245 }
246
247 pub(crate) fn from_id(id: NodeId) -> Self {
248 Self(Node(id))
249 }
250
251 pub fn node(self) -> Node {
253 self.0
254 }
255 }
256
257 impl Deref for $name {
258 type Target = Node;
259 fn deref(&self) -> &Node {
260 &self.0
261 }
262 }
263
264 impl From<$name> for Node {
265 fn from(h: $name) -> Node {
266 h.0
267 }
268 }
269 };
270}
271
272typed_handle!(Group, GroupBuilder);
273typed_handle!(Camera, CameraBuilder);
274typed_handle!(Rect, RectBuilder);
275typed_handle!(Circle, CircleBuilder);
276typed_handle!(Path, PathBuilder);
277typed_handle!(Line, LineBuilder);
278typed_handle!(Txt, TxtBuilder);
279typed_handle!(Img, ImgBuilder);
280typed_handle!(Svg, SvgBuilder);
281typed_handle!(Layout, LayoutBuilder);
282typed_handle!(Grid, GridBuilder);
283typed_handle!(Polygon, PolygonBuilder);
284typed_handle!(Star, StarBuilder);
285typed_handle!(Code, CodeBuilder);
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub struct Custom(Node);
290
291impl Custom {
292 #[allow(clippy::new_ret_no_self)]
296 pub fn new(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> crate::CustomBuilder {
297 crate::CustomBuilder::from_draw(draw)
298 }
299
300 pub(crate) fn from_id(id: NodeId) -> Self {
301 Self(Node(id))
302 }
303
304 pub fn node(self) -> Node {
306 self.0
307 }
308}
309
310impl Deref for Custom {
311 type Target = Node;
312 fn deref(&self) -> &Node {
313 &self.0
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
319pub struct Shader(Node);
320
321impl Shader {
322 #[allow(clippy::new_ret_no_self)]
328 pub fn source(wgsl: impl Into<String>) -> ShaderBuilder {
329 ShaderBuilder::new(crate::shader::ShaderCode::Wgsl(wgsl.into()))
330 }
331
332 pub fn file(path: impl AsRef<std::path::Path>) -> ShaderBuilder {
335 ShaderBuilder::new(crate::shader::ShaderCode::File(path.as_ref().into()))
336 }
337
338 pub(crate) fn from_id(id: NodeId) -> Self {
339 Self(Node(id))
340 }
341
342 pub fn node(self) -> Node {
344 self.0
345 }
346
347 pub fn uniform(self, name: &str) -> Signal<f64> {
354 self.number(name)
355 }
356
357 pub fn amp(self) -> Signal<f64> {
359 self.number("amp")
360 }
361
362 pub fn speed(self) -> Signal<f64> {
364 self.number("speed")
365 }
366
367 pub fn deep(self) -> Signal<Color> {
369 self.color("deep")
370 }
371
372 pub fn foam(self) -> Signal<Color> {
374 self.color("foam")
375 }
376
377 pub fn wetness(self) -> Signal<f64> {
379 self.number("wetness")
380 }
381
382 fn number(self, name: &str) -> Signal<f64> {
383 with_graph(|g| {
384 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
385 unreachable!("handle does not point to a Shader");
386 };
387 for u in uniforms {
388 if u.name == name
389 && let crate::shader::UniformValue::Num(v) = &u.value
390 {
391 return v.clone();
392 }
393 }
394 panic!("no numeric uniform {name}. {}", uniform_names(uniforms));
395 })
396 }
397
398 fn color(self, name: &str) -> Signal<Color> {
399 with_graph(|g| {
400 let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
401 unreachable!("handle does not point to a Shader");
402 };
403 for u in uniforms {
404 if u.name == name
405 && let crate::shader::UniformValue::Color(v) = &u.value
406 {
407 return v.clone();
408 }
409 }
410 panic!("no color uniform {name}. {}", uniform_names(uniforms));
411 })
412 }
413}
414
415fn uniform_names(uniforms: &[crate::shader::Uniform]) -> String {
416 let names: Vec<&str> = uniforms.iter().map(|u| u.name.as_str()).collect();
417 format!("Available: {}.", names.join(", "))
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
422pub struct Effect(NodeId);
423
424impl Effect {
425 pub fn node(self) -> Node {
427 Node(self.0)
428 }
429
430 pub fn strength(self) -> Signal<f64> {
432 self.data().strength
433 }
434
435 pub fn speed(self) -> Signal<f64> {
437 self.data().speed
438 }
439
440 fn data(self) -> crate::post::EffectData {
441 with_graph(|g| {
442 g.node(self.0)
443 .effect
444 .clone()
445 .expect("an Effect handle always points to a node with an effect")
446 })
447 }
448}
449
450impl Deref for Shader {
451 type Target = Node;
452 fn deref(&self) -> &Node {
453 &self.0
454 }
455}
456
457impl From<Custom> for Node {
458 fn from(h: Custom) -> Node {
459 h.0
460 }
461}
462
463impl From<Shader> for Node {
464 fn from(h: Shader) -> Node {
465 h.0
466 }
467}
468
469typed_handle!(Particles, ParticlesBuilder);
470
471macro_rules! kind_signal {
472 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
473 impl $handle {
474 $(
475 $(#[$doc])*
476 #[doc = ""]
477 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
478 pub fn $field(self) -> Signal<$ty> {
479 with_graph(|g| match &g.node(self.id()).kind {
480 NodeKind::$kind { $field, .. } => $field.clone(),
481 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
482 })
483 }
484 )*
485 }
486 };
487}
488
489kind_signal!(Particles::Particles { gravity: f64 });
490
491impl Particles {
492 pub fn burst(self, origin: impl Into<Vec2>, count: u32) -> oxframe_core::WaitFor {
498 let origin = origin.into();
499 let time = oxframe_core::time();
500 with_graph(|g| {
501 if let NodeKind::Particles { state, .. } = &g.node(self.id()).kind {
502 state.bursts.borrow_mut().push(crate::sim::Burst {
503 time,
504 origin,
505 count,
506 });
507 }
508 });
509 oxframe_core::wait_for(0.0)
510 }
511}
512
513kind_signal!(Rect::Rect {
514 width: f64,
515 height: f64,
516 radius: f64
517});
518kind_signal!(Circle::Circle {
519 size: f64,
521 start_angle: f64,
522 end_angle: f64,
523});
524kind_signal!(Polygon::Polygon {
525 sides: f64,
526 size: f64
527});
528kind_signal!(Star::Star {
529 points: f64,
530 size: f64,
531 inner: f64
532});
533kind_signal!(Path::Path { data: BezPath });
534kind_signal!(Camera::Camera {
535 zoom: f64
537});
538
539impl Camera {
540 pub fn center_on(
542 self,
543 node: impl Into<Node>,
544 duration: impl Seconds,
545 ) -> impl std::future::Future<Output = ()> + 'static {
546 let node = node.into();
547 let d = duration.seconds();
548 let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
549 let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
550 async move {
551 if let Some(m) = moves {
552 m.await;
553 }
554 }
555 }
556
557 pub fn fit(
561 self,
562 node: impl Into<Node>,
563 padding: f64,
564 duration: impl Seconds,
565 ) -> impl std::future::Future<Output = ()> + 'static {
566 let node = node.into();
567 let d = duration.seconds();
568 let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
569 let moves = bounds.map(|r| {
570 let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
571 .min((fh - 2.0 * padding) / r.height().max(1e-6))
572 .max(1e-3);
573 let c = r.center();
574 all((
575 self.x().to(c.x, d),
576 self.y().to(c.y, d),
577 self.zoom().to(zoom, d),
578 ))
579 });
580 async move {
581 if let Some(m) = moves {
582 m.await;
583 }
584 }
585 }
586
587 pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
589 let d = duration.seconds();
590 let moves = all((
591 self.x().to(0.0, d),
592 self.y().to(0.0, d),
593 self.zoom().to(1.0, d),
594 self.rotation().to(0.0, d),
595 ));
596 async move {
597 moves.await;
598 }
599 }
600}
601kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
602kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
603kind_signal!(Code::Code {
604 font_size: f64,
605 focus: Vec2,
607 dim: f64,
609});
610
611impl Txt {
612 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
615 let to = text.into();
616 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
617 NodeKind::Txt {
618 text,
619 morph,
620 morph_t,
621 ..
622 } => (text.clone(), morph.clone(), morph_t.clone()),
623 _ => unreachable!("handle does not point to a Txt"),
624 });
625 Box::pin(async move {
626 morph.set(Some((txt.get(), to.clone())));
627 t.set(0.0);
628 t.to(1.0, duration).await;
629 txt.set(to);
630 morph.set(None);
631 })
632 }
633}
634
635impl Code {
636 pub fn code(self) -> CodeText {
644 with_graph(|g| match &g.node(self.id()).kind {
645 NodeKind::Code {
646 code,
647 edit_from,
648 edit_to,
649 editing,
650 edit_t,
651 edit_flash,
652 focus,
653 lang,
654 ..
655 } => CodeText {
656 code: code.clone(),
657 edit_from: edit_from.clone(),
658 edit_to: edit_to.clone(),
659 editing: editing.clone(),
660 edit_t: edit_t.clone(),
661 edit_flash: edit_flash.clone(),
662 focus: focus.clone(),
663 lang: *lang,
664 },
665 _ => unreachable!("handle does not point to a Code"),
666 })
667 }
668
669 pub fn focus_lines(
675 self,
676 first: usize,
677 last: usize,
678 duration: f64,
679 ) -> oxframe_core::Tween<Vec2> {
680 self.focus()
681 .to(Vec2::new(first as f64, last as f64), duration)
682 }
683
684 pub fn insert(
691 self,
692 line: usize,
693 col: usize,
694 text: impl Into<String>,
695 duration: impl Seconds,
696 ) -> CodeTween {
697 let text = text.into();
698 let src = self.code().get();
699 let next = oxframe_text::insert_at(&src, line, col, &text)
700 .unwrap_or_else(|e| panic!("code.insert: {e}"));
701 self.code().to(next, duration)
702 }
703
704 pub fn remove(
713 self,
714 range: ((usize, usize), (usize, usize)),
715 duration: impl Seconds,
716 ) -> CodeTween {
717 self.rewrite(range, String::new(), duration, "code.remove")
718 }
719
720 pub fn replace(
727 self,
728 range: ((usize, usize), (usize, usize)),
729 text: impl Into<String>,
730 duration: impl Seconds,
731 ) -> CodeTween {
732 self.rewrite(range, text.into(), duration, "code.replace")
733 }
734
735 fn rewrite(
736 self,
737 ((l0, c0), (l1, c1)): ((usize, usize), (usize, usize)),
738 text: String,
739 duration: impl Seconds,
740 what: &str,
741 ) -> CodeTween {
742 let src = self.code().get();
743 let next = oxframe_text::replace_range(
744 &src,
745 oxframe_text::Loc { line: l0, col: c0 },
746 oxframe_text::Loc { line: l1, col: c1 },
747 &text,
748 )
749 .unwrap_or_else(|e| panic!("{what}: {e}"));
750 self.code().to(next, duration)
751 }
752}
753
754#[derive(Clone)]
760pub struct CodeText {
761 code: Signal<String>,
762 edit_from: Signal<String>,
763 edit_to: Signal<String>,
764 editing: Signal<bool>,
765 edit_t: Signal<f64>,
766 edit_flash: Signal<bool>,
767 focus: Signal<Vec2>,
768 lang: oxframe_text::Lang,
769}
770
771impl std::ops::Deref for CodeText {
772 type Target = Signal<String>;
773 fn deref(&self) -> &Signal<String> {
774 &self.code
775 }
776}
777
778impl CodeText {
779 pub fn set(&self, value: impl Into<String>) {
781 self.editing.set(false);
782 self.code.set(value);
783 }
784
785 pub fn bind(&self, f: impl Fn() -> String + 'static) {
787 self.editing.set(false);
788 self.code.bind(f);
789 }
790
791 pub fn to(&self, target: impl Into<String>, duration: impl Seconds) -> CodeTween {
793 let target = target.into();
794 let from = self.code.get();
795 self.edit_from.set(from.clone());
796 self.edit_to.set(target.clone());
797 self.edit_flash.set(false);
798 self.editing.set(true);
799 self.edit_t.set(0.0);
800 CodeTween {
801 tween: self.edit_t.to(1.0, duration),
802 code: self.code.clone(),
803 editing: self.editing.clone(),
804 edit_flash: self.edit_flash.clone(),
805 focus: self.focus.clone(),
806 lang: self.lang,
807 from,
808 target,
809 finished: false,
810 }
811 }
812}
813
814#[must_use = "a tween does nothing unless awaited"]
817pub struct CodeTween {
818 tween: Tween<f64>,
819 code: Signal<String>,
820 editing: Signal<bool>,
821 edit_flash: Signal<bool>,
822 focus: Signal<Vec2>,
823 lang: oxframe_text::Lang,
824 from: String,
825 target: String,
826 finished: bool,
827}
828
829impl CodeTween {
830 pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
832 self.tween = self.tween.ease(ease);
833 self
834 }
835
836 pub fn duration(mut self, duration: impl Seconds) -> Self {
838 self.tween = self.tween.duration(duration);
839 self
840 }
841
842 pub fn flash(self) -> Self {
844 self.edit_flash.set(true);
845 self
846 }
847}
848
849impl Future for CodeTween {
850 type Output = ();
851
852 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
853 let this = self.get_mut();
854 if Pin::new(&mut this.tween).poll(cx).is_pending() {
855 return Poll::Pending;
856 }
857 if !this.finished {
858 this.finished = true;
859 let focus = this.focus.get();
860 let (a, b) = (focus.x.round() as i64, focus.y.round() as i64);
861 if a > 0 || b > 0 {
862 let diff = oxframe_text::diff_code(&this.from, &this.target, this.lang);
863 if let Some((n0, n1)) =
864 oxframe_text::map_focus(&diff, a.max(0) as usize, b.max(0) as usize)
865 {
866 this.focus.set(Vec2::new(n0 as f64, n1 as f64));
867 }
868 }
869 this.code.set(this.target.clone());
870 this.editing.set(false);
871 }
872 Poll::Ready(())
873 }
874}
875kind_signal!(Svg::Svg {
876 width: f64,
877 height: f64
878});
879
880impl Svg {
881 pub fn find(self, id: &str) -> Node {
887 let id = with_graph(|g| g.descendant_named(self.id(), id))
888 .unwrap_or_else(|| panic!("svg has no element with id {id:?}"));
889 Node::from_id(id)
890 }
891
892 pub fn paths(self) -> Vec<Path> {
894 with_graph(|g| {
895 g.descendant_paths(self.id())
896 .into_iter()
897 .map(Path::from_id)
898 .collect()
899 })
900 }
901}
902
903kind_signal!(Img::Img {
904 width: f64,
905 height: f64
906});
907kind_signal!(Layout::Layout {
908 gap: f64,
909 padding: f64,
910 radius: f64,
911 width: Option<f64>,
913 height: Option<f64>,
915});
916kind_signal!(Grid::Grid {
917 width: f64,
918 height: f64,
919 spacing: f64
920});
921
922#[derive(Debug, Clone, Copy, PartialEq, Eq)]
924pub struct View(Node);
925
926impl View {
927 pub fn current() -> Self {
929 Self(Node(SceneGraph::ROOT))
930 }
931
932 pub fn size(self) -> (u32, u32) {
934 with_graph(|g| g.size())
935 }
936
937 pub fn is_portrait(self) -> bool {
939 let (w, h) = self.size();
940 h > w
941 }
942
943 pub fn safe_area(self) -> SafeArea {
947 let (w, h) = self.size();
948 SafeArea::for_frame(f64::from(w), f64::from(h))
949 }
950}
951
952impl Deref for View {
953 type Target = Node;
954 fn deref(&self) -> &Node {
955 &self.0
956 }
957}
958
959impl From<View> for Node {
960 fn from(v: View) -> Node {
961 v.0
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968 use crate::Primitive;
969
970 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
971 let graph = SceneGraph::new(400, 400);
972 graph.enter(|| f(View::current()));
973 graph
974 }
975
976 fn play(
978 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
979 ) -> u32 {
980 let graph = SceneGraph::new(400, 400);
981 graph.enter(|| {
982 let fut = scene_fn(View::current());
983 let mut ex =
984 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
985 let mut frames = 0;
986 while !ex.step() {
987 frames += 1;
988 assert!(frames < 10_000, "scene did not finish");
989 }
990 frames
991 })
992 }
993
994 fn screen(n: impl Into<Node>) -> (f64, f64) {
996 let p = n.into().absolute_position();
997 ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
998 }
999
1000 #[test]
1001 fn camera_pans_zooms_and_turns_its_children_only() {
1002 scene(|view| {
1003 let cam = view.add(Camera::new());
1004 let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
1005 let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
1006 assert_eq!(screen(dot), (100.0, 50.0));
1008 cam.x().set(100.0);
1010 cam.y().set(50.0);
1011 assert_eq!(screen(dot), (0.0, 0.0));
1012 cam.zoom().set(2.0);
1014 let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
1015 assert_eq!(screen(other), (20.0, 0.0));
1016 cam.rotation().set(90.0);
1018 assert_eq!(screen(other), (0.0, -20.0));
1019 assert_eq!(screen(hud), (-300.0, -200.0));
1021 });
1022 }
1023
1024 #[test]
1025 fn camera_center_on_fit_and_reset() {
1026 play(|view| {
1027 let cam = view.add(Camera::new());
1028 let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
1029 let row = cam.add(
1030 Layout::new()
1031 .x(-500.0)
1032 .gap(10.0)
1033 .child(Rect::new().size(50.0))
1034 .child(Rect::new().size(50.0)),
1035 );
1036 Box::pin(async move {
1037 cam.center_on(card, 1.0).await;
1038 assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
1039 assert_eq!(screen(card), (0.0, 0.0));
1040 cam.fit(card, 20.0, 0.0).await;
1042 assert!(
1043 (cam.zoom().get() - 1.8).abs() < 1e-9,
1044 "{}",
1045 cam.zoom().get()
1046 );
1047 row.gap().to(110.0, 0.5).await;
1049 cam.fit(row, 0.0, 0.0).await;
1050 let (w, _) = row.size();
1051 assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
1052 assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
1053 assert!((cam.x().get() + 500.0).abs() < 1e-9);
1054 cam.reset(0.5).await;
1055 assert_eq!(
1056 (
1057 cam.x().get(),
1058 cam.y().get(),
1059 cam.zoom().get(),
1060 cam.rotation().get()
1061 ),
1062 (0.0, 0.0, 1.0, 0.0)
1063 );
1064 })
1065 });
1066 }
1067
1068 #[test]
1069 fn entrance_helpers_start_at_once_and_end_in_place() {
1070 let normal = crate::theme::current().motion.normal;
1071 let expected = (normal * 60.0).round() as u32;
1072 let frames = play(|view| {
1073 let r = view.add(Rect::new().size(100.0).x(50.0));
1074 let pop = r.pop_in();
1075 assert_eq!(r.scale().get(), Vec2::ZERO);
1078 let slide = r.slide_in(Side::Left);
1079 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
1080 assert_eq!(r.opacity().get(), 0.0);
1081 let draw = r.draw_on();
1082 assert_eq!(r.end().get(), 0.0);
1083 Box::pin(async move {
1084 all((pop, slide)).await;
1085 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
1086 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
1087 draw.await;
1088 assert_eq!(r.end().get(), 1.0);
1089 r.fade_out().duration(0.5).await;
1090 assert_eq!(r.opacity().get(), 0.0);
1091 })
1092 });
1093 let slow = crate::theme::current().motion.slow;
1094 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
1095 }
1096
1097 #[test]
1098 fn a_diff_lands_on_the_new_source_and_moves_the_focus() {
1099 play(|view| {
1100 let code = view.add(Code::new().code("a\nkeep\n").focus(2, 2));
1101 Box::pin(async move {
1102 code.insert(1, 0, "NEW\n", 0.2).flash().await;
1103 assert_eq!(code.code().get(), "NEW\na\nkeep\n");
1104 let f = code.focus().get();
1105 assert_eq!((f.x.round() as i32, f.y.round() as i32), (3, 3));
1106 code.replace(((3, 0), (3, 4)), "stay", 0.0).await;
1107 assert_eq!(code.code().get(), "NEW\na\nstay\n");
1108 })
1109 });
1110 }
1111
1112 #[test]
1113 fn add_records_its_call_site_and_scene_bounds() {
1114 let g = scene(|view| {
1115 view.add(Rect::new().size(100.0).name("box"));
1116 view.add(Rect::new().size(10.0).x(100.0));
1117 });
1118 let nodes = g.inspect_nodes();
1119 let named = nodes.iter().find(|n| n.name == "box").unwrap();
1120 assert_eq!(named.kind, "Rect");
1121 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
1122 assert!(
1123 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
1124 "{:?}",
1125 named.bounds
1126 );
1127 let auto = nodes
1128 .iter()
1129 .find(|n| n.name != "box" && n.name != "view")
1130 .unwrap();
1131 assert_eq!(auto.name, auto.source.clone().unwrap());
1132 }
1133
1134 #[test]
1135 fn builders_set_initial_values_and_handles_return_signals() {
1136 scene(|view| {
1137 let r = view.add(
1138 Rect::new()
1139 .size(50.0)
1140 .radius(5.0)
1141 .x(-10.0)
1142 .fill(Color::BLACK),
1143 );
1144 assert_eq!(r.width().get(), 50.0);
1145 assert_eq!(r.height().get(), 50.0);
1146 assert_eq!(r.radius().get(), 5.0);
1147 assert_eq!(r.x().get(), -10.0);
1148 assert_eq!(r.fill().get(), Color::BLACK);
1149 r.x().set(3.0);
1150 assert_eq!(r.x().get(), 3.0);
1151 let c = r.add(Circle::new().size(10.0));
1152 assert_eq!(r.children(), vec![c.node()]);
1153 assert_eq!(view.children(), vec![r.node()]);
1154 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
1155 });
1156 }
1157
1158 #[test]
1159 fn draw_list_composes_transforms() {
1160 let g = scene(|view| {
1161 let grp = view.add(Group::new().x(10.0).scale(2.0));
1162 grp.add(Circle::new().x(5.0));
1163 });
1164 let items = g.draw_list();
1165 assert_eq!(items.len(), 1);
1166 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
1167 }
1168
1169 #[test]
1170 fn scale_takes_a_number_or_a_pair() {
1171 scene(|view| {
1172 let r = view.add(Rect::new().scale(Vec2::new(1.6, 1.6)));
1173 assert_eq!(r.scale().get(), Vec2::new(1.6, 1.6));
1174 let r = view.add(Rect::new().scale(2));
1175 assert_eq!(r.scale().get(), Vec2::new(2.0, 2.0));
1176 r.scale().set(0.5);
1177 assert_eq!(r.scale().get(), Vec2::new(0.5, 0.5));
1178 r.scale().set((2.0, 0.5));
1179 assert_eq!(r.scale().get(), Vec2::new(2.0, 0.5));
1180 drop::<Tween<Vec2>>(r.scale().to(1.1, 0.4));
1181 drop::<Tween<Vec2>>(r.scale().to(Vec2::ZERO, 0.4));
1182 });
1183 }
1184
1185 #[test]
1186 fn layout_places_children_in_a_row() {
1187 let g = scene(|view| {
1188 view.add(
1189 Layout::new()
1190 .gap(10.0)
1191 .padding(5.0)
1192 .child(Rect::new().size(20.0))
1193 .child(Rect::new().width(40.0).height(10.0)),
1194 );
1195 });
1196 let layout = NodeId(1);
1197 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
1198 let centers: Vec<_> = g
1199 .draw_list()
1200 .iter()
1201 .map(|i| i.transform * Point::ZERO)
1202 .collect();
1203 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
1205 }
1206
1207 #[test]
1208 fn column_with_fixed_size_and_alignment() {
1209 let g = scene(|view| {
1210 view.add(
1211 Layout::new()
1212 .column()
1213 .height(100.0)
1214 .width(100.0)
1215 .justify(crate::Justify::Start)
1216 .align(crate::LayoutAlign::Start)
1217 .child(Rect::new().size(20.0)),
1218 );
1219 });
1220 let c = g.draw_list()[0].transform * Point::ZERO;
1221 assert_eq!(c, Point::new(-40.0, -40.0));
1222 }
1223
1224 #[test]
1225 fn text_line_path_and_img_draw() {
1226 let png = {
1227 let mut v = Vec::new();
1228 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
1229 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
1230 .unwrap();
1231 v
1232 };
1233 let g = scene(|view| {
1234 let t = view.add(Txt::new().text("Hi").font_size(40.0));
1235 assert!(t.size().0 > 20.0);
1236 view.add(
1237 Line::new()
1238 .points([(0.0, 0.0), (100.0, 0.0)])
1239 .end_arrow(true),
1240 );
1241 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
1242 let img = view.add(Img::new().src(&png).width(40.0));
1243 assert_eq!(img.height().get(), 20.0);
1244 });
1245 let kinds: Vec<_> = g
1246 .draw_list()
1247 .iter()
1248 .map(|i| match i.prim {
1249 Primitive::Fill { .. } => "fill",
1250 Primitive::Stroke { .. } => "stroke",
1251 Primitive::Image { .. } => "image",
1252 Primitive::PushLayer(_) => "push",
1253 Primitive::PopLayer => "pop",
1254 Primitive::ReadFrame(_) => "frame",
1255 Primitive::BeginEffect(_) => "effect",
1256 Primitive::EndEffect => "end_effect",
1257 })
1258 .collect();
1259 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
1260 }
1261
1262 #[test]
1263 fn trimming_and_pick() {
1264 let g = scene(|view| {
1265 view.add(Rect::new().size(100.0).name("back"));
1266 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
1267 });
1268 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
1269 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
1270 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
1272 }
1273
1274 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
1275 g.draw_list()
1276 .iter()
1277 .map(|i| match i.prim {
1278 Primitive::Fill { .. } => "fill",
1279 Primitive::Stroke { .. } => "stroke",
1280 Primitive::Image { .. } => "image",
1281 Primitive::PushLayer(_) => "push",
1282 Primitive::PopLayer => "pop",
1283 Primitive::ReadFrame(_) => "frame",
1284 Primitive::BeginEffect(_) => "effect",
1285 Primitive::EndEffect => "end_effect",
1286 })
1287 .collect()
1288 }
1289
1290 #[test]
1291 fn layers_only_when_needed_and_balanced() {
1292 let g = scene(|v| {
1294 v.add(Rect::new().opacity(0.5));
1295 });
1296 assert_eq!(kinds(&g), ["fill"]);
1297 let g = scene(|v| {
1299 v.add(
1300 Group::new()
1301 .opacity(0.5)
1302 .child(Rect::new())
1303 .child(Circle::new()),
1304 );
1305 });
1306 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
1307 let g = scene(|v| {
1309 v.add(
1310 Circle::new()
1311 .clip(true)
1312 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
1313 .child(Rect::new()),
1314 );
1315 });
1316 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
1317 }
1318
1319 #[test]
1320 fn z_index_orders_siblings_and_anchor_moves_box() {
1321 let g = scene(|v| {
1322 v.add(Rect::new().name("a").z_index(2.0));
1323 v.add(Rect::new().name("b"));
1324 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
1325 });
1326 let items = g.draw_list();
1327 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
1328 assert_eq!(order, vec![2, 3, 1]);
1329 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
1331 }
1332
1333 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
1334 g.draw_list()
1335 .iter()
1336 .filter_map(|i| match &i.prim {
1337 Primitive::Fill { path, .. } => {
1338 let bb = kurbo::Shape::bounding_box(path);
1339 Some((i.transform * Point::ZERO, bb.width()))
1340 }
1341 _ => None,
1342 })
1343 .collect()
1344 }
1345
1346 #[test]
1347 fn flex_grow_stretch_wrap_and_grid() {
1348 use crate::{Justify, LayoutAlign};
1349 let g = scene(|v| {
1351 v.add(
1352 Layout::new()
1353 .width(200.0)
1354 .justify(Justify::Start)
1355 .child(Rect::new().size(40.0))
1356 .child(Rect::new().size(40.0).grow(1.0)),
1357 );
1358 });
1359 let b = boxes(&g);
1360 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
1361 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
1362
1363 let g = scene(|v| {
1365 v.add(
1366 Layout::new()
1367 .column()
1368 .width(120.0)
1369 .align(LayoutAlign::Stretch)
1370 .child(Rect::new().size(30.0)),
1371 );
1372 });
1373 assert_eq!(boxes(&g)[0].1, 120.0);
1374
1375 let g = scene(|v| {
1377 v.add(
1378 Layout::new()
1379 .width(130.0)
1380 .wrap(true)
1381 .child(Rect::new().size(60.0))
1382 .child(Rect::new().size(60.0))
1383 .child(Rect::new().size(60.0)),
1384 );
1385 });
1386 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
1387 assert_eq!(ys[0], ys[1]);
1388 assert!(ys[2] > ys[0]);
1389
1390 let g = scene(|v| {
1392 v.add(
1393 Layout::new()
1394 .grid(2)
1395 .width(200.0)
1396 .align(LayoutAlign::Stretch)
1397 .child(Rect::new().size(10.0))
1398 .child(Rect::new().size(10.0))
1399 .child(Rect::new().size(10.0)),
1400 );
1401 });
1402 let b = boxes(&g);
1403 assert_eq!(
1404 b.iter().map(|b| b.1).collect::<Vec<_>>(),
1405 vec![100.0, 100.0, 100.0]
1406 );
1407 assert_eq!(b[2].0.x, b[0].0.x);
1408 assert!(b[2].0.y > b[0].0.y);
1409 }
1410
1411 #[test]
1412 fn describe_lists_signals() {
1413 let g = scene(|view| {
1414 view.add(Txt::new().text("abc"));
1415 });
1416 let d = g.describe(NodeId(1));
1417 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
1418 }
1419}
1420
1421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1430pub struct Video(Node);
1431
1432impl Video {
1433 #[allow(clippy::new_ret_no_self)]
1435 pub fn new() -> crate::builder::VideoBuilder {
1436 crate::builder::VideoBuilder::new()
1437 }
1438
1439 pub(crate) fn from_id(id: NodeId) -> Self {
1440 Self(Node(id))
1441 }
1442
1443 pub fn node(self) -> Node {
1445 self.0
1446 }
1447
1448 fn parts(
1449 self,
1450 ) -> (
1451 std::rc::Rc<crate::video::VideoData>,
1452 Signal<f64>,
1453 Signal<f64>,
1454 ) {
1455 with_graph(|g| match &g.node(self.0.id()).kind {
1456 NodeKind::Video {
1457 data, time, rate, ..
1458 } => (data.clone(), time.clone(), rate.clone()),
1459 _ => unreachable!("handle does not point to a Video"),
1460 })
1461 }
1462
1463 pub fn time(self) -> Signal<f64> {
1466 self.parts().1
1467 }
1468
1469 pub fn playback_rate(self) -> Signal<f64> {
1472 self.parts().2
1473 }
1474
1475 pub fn duration(self) -> f64 {
1477 self.parts().0.source.info().duration
1478 }
1479
1480 pub fn is_playing(self) -> bool {
1482 self.parts().0.playback.playing.get()
1483 }
1484
1485 pub fn play(self) {
1488 let (data, time, rate) = self.parts();
1489 let pb = data.playback.clone();
1490 if pb.playing.get() {
1491 return;
1492 }
1493 pb.playing.set(true);
1494 let generation = pb.generation.get().wrapping_add(1);
1495 pb.generation.set(generation);
1496 let duration = data.source.info().duration;
1497 drop(oxframe_core::spawn(async move {
1499 loop {
1500 let live = pb.playing.get() && pb.generation.get() == generation;
1501 sync_sound(&data, &time, &rate, live);
1502 if !live {
1503 break;
1504 }
1505 oxframe_core::next_frame().await;
1506 if !(pb.playing.get() && pb.generation.get() == generation) {
1507 continue;
1508 }
1509 let t = time.get() + rate.get() * oxframe_core::dt();
1510 if duration > 0.0 && t >= duration {
1511 time.set(duration);
1512 pb.playing.set(false);
1513 } else {
1514 time.set(t.max(0.0));
1515 }
1516 }
1517 }));
1518 }
1519
1520 pub fn pause(self) {
1522 let (data, time, rate) = self.parts();
1523 data.playback.playing.set(false);
1524 sync_sound(&data, &time, &rate, false);
1525 }
1526
1527 pub fn seek(self, seconds: f64) {
1529 let (data, time, _) = self.parts();
1530 time.set(seconds.max(0.0));
1531 data.playback.jumped.set(true);
1532 }
1533}
1534
1535fn sync_sound(data: &crate::video::VideoData, time: &Signal<f64>, rate: &Signal<f64>, live: bool) {
1540 let pb = &data.playback;
1541 let now = oxframe_core::time();
1542 let audible =
1543 live && !data.muted && data.source.info().has_audio && (rate.get() - 1.0).abs() < 1e-9;
1544 if let Some((id, at, from)) = pb.segment.get()
1545 && (!audible || pb.jumped.get())
1546 {
1547 oxframe_core::end_sound(id, from + (now - at));
1548 pb.segment.set(None);
1549 }
1550 pb.jumped.set(false);
1551 if audible && pb.segment.get().is_none() {
1552 let from = time.get();
1553 let id = oxframe_core::record_sound(oxframe_core::SoundClip {
1554 file: data.path.to_string_lossy().into_owned(),
1555 at: now,
1556 volume: 1.0,
1557 fade_in: 0.0,
1558 fade_out: 0.0,
1559 trim_start: from,
1560 trim_end: None,
1561 priority: false,
1562 speech: false,
1563 });
1564 pb.segment.set(Some((id, now, from)));
1565 }
1566}
1567
1568impl Deref for Video {
1569 type Target = Node;
1570 fn deref(&self) -> &Node {
1571 &self.0
1572 }
1573}
1574
1575impl From<Video> for Node {
1576 fn from(h: Video) -> Node {
1577 h.0
1578 }
1579}