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 focus_span: Vec2,
610 dim: f64,
612});
613
614impl Txt {
615 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
618 let to = text.into();
619 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
620 NodeKind::Txt {
621 text,
622 morph,
623 morph_t,
624 ..
625 } => (text.clone(), morph.clone(), morph_t.clone()),
626 _ => unreachable!("handle does not point to a Txt"),
627 });
628 Box::pin(async move {
629 morph.set(Some((txt.get(), to.clone())));
630 t.set(0.0);
631 t.to(1.0, duration).await;
632 txt.set(to);
633 morph.set(None);
634 })
635 }
636}
637
638impl Code {
639 pub fn code(self) -> CodeText {
647 with_graph(|g| match &g.node(self.id()).kind {
648 NodeKind::Code {
649 code,
650 edit_from,
651 edit_to,
652 editing,
653 edit_t,
654 edit_flash,
655 focus,
656 lang,
657 ..
658 } => CodeText {
659 code: code.clone(),
660 edit_from: edit_from.clone(),
661 edit_to: edit_to.clone(),
662 editing: editing.clone(),
663 edit_t: edit_t.clone(),
664 edit_flash: edit_flash.clone(),
665 focus: focus.clone(),
666 lang: *lang,
667 },
668 _ => unreachable!("handle does not point to a Code"),
669 })
670 }
671
672 pub fn focus_lines(
678 self,
679 first: usize,
680 last: usize,
681 duration: f64,
682 ) -> oxframe_core::Tween<Vec2> {
683 self.focus()
684 .to(Vec2::new(first as f64, last as f64), duration)
685 }
686
687 pub fn focus_text(self, text: &str, duration: f64) -> oxframe_core::Tween<Vec2> {
699 let target = if text.is_empty() {
700 Vec2::ZERO
701 } else {
702 let src = self.code().get();
703 let at = src
704 .find(text)
705 .unwrap_or_else(|| panic!("code.focus_text: {text:?} is not in the source"));
706 let start = src[..at].chars().count();
707 Vec2::new(start as f64, (start + text.chars().count()) as f64)
708 };
709 let span = self.focus_span();
710 if span.get() == Vec2::ZERO && target != Vec2::ZERO {
712 span.set(Vec2::new(target.x, target.x));
713 }
714 span.to(target, duration)
715 }
716
717 pub fn insert(
724 self,
725 line: usize,
726 col: usize,
727 text: impl Into<String>,
728 duration: impl Seconds,
729 ) -> CodeTween {
730 let text = text.into();
731 let src = self.code().get();
732 let next = oxframe_text::insert_at(&src, line, col, &text)
733 .unwrap_or_else(|e| panic!("code.insert: {e}"));
734 self.code().to(next, duration)
735 }
736
737 pub fn remove(
746 self,
747 range: ((usize, usize), (usize, usize)),
748 duration: impl Seconds,
749 ) -> CodeTween {
750 self.rewrite(range, String::new(), duration, "code.remove")
751 }
752
753 pub fn replace(
760 self,
761 range: ((usize, usize), (usize, usize)),
762 text: impl Into<String>,
763 duration: impl Seconds,
764 ) -> CodeTween {
765 self.rewrite(range, text.into(), duration, "code.replace")
766 }
767
768 fn rewrite(
769 self,
770 ((l0, c0), (l1, c1)): ((usize, usize), (usize, usize)),
771 text: String,
772 duration: impl Seconds,
773 what: &str,
774 ) -> CodeTween {
775 let src = self.code().get();
776 let next = oxframe_text::replace_range(
777 &src,
778 oxframe_text::Loc { line: l0, col: c0 },
779 oxframe_text::Loc { line: l1, col: c1 },
780 &text,
781 )
782 .unwrap_or_else(|e| panic!("{what}: {e}"));
783 self.code().to(next, duration)
784 }
785}
786
787#[derive(Clone)]
793pub struct CodeText {
794 code: Signal<String>,
795 edit_from: Signal<String>,
796 edit_to: Signal<String>,
797 editing: Signal<bool>,
798 edit_t: Signal<f64>,
799 edit_flash: Signal<bool>,
800 focus: Signal<Vec2>,
801 lang: oxframe_text::Lang,
802}
803
804impl std::ops::Deref for CodeText {
805 type Target = Signal<String>;
806 fn deref(&self) -> &Signal<String> {
807 &self.code
808 }
809}
810
811impl CodeText {
812 pub fn set(&self, value: impl Into<String>) {
814 self.editing.set(false);
815 self.code.set(value);
816 }
817
818 pub fn bind(&self, f: impl Fn() -> String + 'static) {
820 self.editing.set(false);
821 self.code.bind(f);
822 }
823
824 pub fn to(&self, target: impl Into<String>, duration: impl Seconds) -> CodeTween {
826 let target = target.into();
827 let from = self.code.get();
828 self.edit_from.set(from.clone());
829 self.edit_to.set(target.clone());
830 self.edit_flash.set(false);
831 self.editing.set(true);
832 self.edit_t.set(0.0);
833 CodeTween {
834 tween: self.edit_t.to(1.0, duration),
835 code: self.code.clone(),
836 editing: self.editing.clone(),
837 edit_flash: self.edit_flash.clone(),
838 focus: self.focus.clone(),
839 lang: self.lang,
840 from,
841 target,
842 finished: false,
843 }
844 }
845}
846
847#[must_use = "a tween does nothing unless awaited"]
850pub struct CodeTween {
851 tween: Tween<f64>,
852 code: Signal<String>,
853 editing: Signal<bool>,
854 edit_flash: Signal<bool>,
855 focus: Signal<Vec2>,
856 lang: oxframe_text::Lang,
857 from: String,
858 target: String,
859 finished: bool,
860}
861
862impl CodeTween {
863 pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
865 self.tween = self.tween.ease(ease);
866 self
867 }
868
869 pub fn duration(mut self, duration: impl Seconds) -> Self {
871 self.tween = self.tween.duration(duration);
872 self
873 }
874
875 pub fn flash(self) -> Self {
877 self.edit_flash.set(true);
878 self
879 }
880}
881
882impl Future for CodeTween {
883 type Output = ();
884
885 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
886 let this = self.get_mut();
887 if Pin::new(&mut this.tween).poll(cx).is_pending() {
888 return Poll::Pending;
889 }
890 if !this.finished {
891 this.finished = true;
892 let focus = this.focus.get();
893 let (a, b) = (focus.x.round() as i64, focus.y.round() as i64);
894 if a > 0 || b > 0 {
895 let diff = oxframe_text::diff_code(&this.from, &this.target, this.lang);
896 if let Some((n0, n1)) =
897 oxframe_text::map_focus(&diff, a.max(0) as usize, b.max(0) as usize)
898 {
899 this.focus.set(Vec2::new(n0 as f64, n1 as f64));
900 }
901 }
902 this.code.set(this.target.clone());
903 this.editing.set(false);
904 }
905 Poll::Ready(())
906 }
907}
908kind_signal!(Svg::Svg {
909 width: f64,
910 height: f64
911});
912
913impl Svg {
914 pub fn find(self, id: &str) -> Node {
920 let id = with_graph(|g| g.descendant_named(self.id(), id))
921 .unwrap_or_else(|| panic!("svg has no element with id {id:?}"));
922 Node::from_id(id)
923 }
924
925 pub fn paths(self) -> Vec<Path> {
927 with_graph(|g| {
928 g.descendant_paths(self.id())
929 .into_iter()
930 .map(Path::from_id)
931 .collect()
932 })
933 }
934}
935
936kind_signal!(Img::Img {
937 width: f64,
938 height: f64
939});
940kind_signal!(Layout::Layout {
941 gap: f64,
942 padding: f64,
943 radius: f64,
944 width: Option<f64>,
946 height: Option<f64>,
948});
949kind_signal!(Grid::Grid {
950 width: f64,
951 height: f64,
952 spacing: f64
953});
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
957pub struct View(Node);
958
959impl View {
960 pub fn current() -> Self {
962 Self(Node(SceneGraph::ROOT))
963 }
964
965 pub fn size(self) -> (u32, u32) {
967 with_graph(|g| g.size())
968 }
969
970 pub fn is_portrait(self) -> bool {
972 let (w, h) = self.size();
973 h > w
974 }
975
976 pub fn safe_area(self) -> SafeArea {
980 let (w, h) = self.size();
981 SafeArea::for_frame(f64::from(w), f64::from(h))
982 }
983}
984
985impl Deref for View {
986 type Target = Node;
987 fn deref(&self) -> &Node {
988 &self.0
989 }
990}
991
992impl From<View> for Node {
993 fn from(v: View) -> Node {
994 v.0
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001 use crate::Primitive;
1002
1003 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
1004 let graph = SceneGraph::new(400, 400);
1005 graph.enter(|| f(View::current()));
1006 graph
1007 }
1008
1009 fn play(
1011 scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
1012 ) -> u32 {
1013 let graph = SceneGraph::new(400, 400);
1014 graph.enter(|| {
1015 let fut = scene_fn(View::current());
1016 let mut ex =
1017 oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
1018 let mut frames = 0;
1019 while !ex.step() {
1020 frames += 1;
1021 assert!(frames < 10_000, "scene did not finish");
1022 }
1023 frames
1024 })
1025 }
1026
1027 fn screen(n: impl Into<Node>) -> (f64, f64) {
1029 let p = n.into().absolute_position();
1030 ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
1031 }
1032
1033 #[test]
1034 fn camera_pans_zooms_and_turns_its_children_only() {
1035 scene(|view| {
1036 let cam = view.add(Camera::new());
1037 let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
1038 let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
1039 assert_eq!(screen(dot), (100.0, 50.0));
1041 cam.x().set(100.0);
1043 cam.y().set(50.0);
1044 assert_eq!(screen(dot), (0.0, 0.0));
1045 cam.zoom().set(2.0);
1047 let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
1048 assert_eq!(screen(other), (20.0, 0.0));
1049 cam.rotation().set(90.0);
1051 assert_eq!(screen(other), (0.0, -20.0));
1052 assert_eq!(screen(hud), (-300.0, -200.0));
1054 });
1055 }
1056
1057 #[test]
1058 fn camera_center_on_fit_and_reset() {
1059 play(|view| {
1060 let cam = view.add(Camera::new());
1061 let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
1062 let row = cam.add(
1063 Layout::new()
1064 .x(-500.0)
1065 .gap(10.0)
1066 .child(Rect::new().size(50.0))
1067 .child(Rect::new().size(50.0)),
1068 );
1069 Box::pin(async move {
1070 cam.center_on(card, 1.0).await;
1071 assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
1072 assert_eq!(screen(card), (0.0, 0.0));
1073 cam.fit(card, 20.0, 0.0).await;
1075 assert!(
1076 (cam.zoom().get() - 1.8).abs() < 1e-9,
1077 "{}",
1078 cam.zoom().get()
1079 );
1080 row.gap().to(110.0, 0.5).await;
1082 cam.fit(row, 0.0, 0.0).await;
1083 let (w, _) = row.size();
1084 assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
1085 assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
1086 assert!((cam.x().get() + 500.0).abs() < 1e-9);
1087 cam.reset(0.5).await;
1088 assert_eq!(
1089 (
1090 cam.x().get(),
1091 cam.y().get(),
1092 cam.zoom().get(),
1093 cam.rotation().get()
1094 ),
1095 (0.0, 0.0, 1.0, 0.0)
1096 );
1097 })
1098 });
1099 }
1100
1101 #[test]
1102 fn entrance_helpers_start_at_once_and_end_in_place() {
1103 let normal = crate::theme::current().motion.normal;
1104 let expected = (normal * 60.0).round() as u32;
1105 let frames = play(|view| {
1106 let r = view.add(Rect::new().size(100.0).x(50.0));
1107 let pop = r.pop_in();
1108 assert_eq!(r.scale().get(), Vec2::ZERO);
1111 let slide = r.slide_in(Side::Left);
1112 assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
1113 assert_eq!(r.opacity().get(), 0.0);
1114 let draw = r.draw_on();
1115 assert_eq!(r.end().get(), 0.0);
1116 Box::pin(async move {
1117 all((pop, slide)).await;
1118 assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
1119 assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
1120 draw.await;
1121 assert_eq!(r.end().get(), 1.0);
1122 r.fade_out().duration(0.5).await;
1123 assert_eq!(r.opacity().get(), 0.0);
1124 })
1125 });
1126 let slow = crate::theme::current().motion.slow;
1127 assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
1128 }
1129
1130 #[test]
1131 fn a_diff_lands_on_the_new_source_and_moves_the_focus() {
1132 play(|view| {
1133 let code = view.add(Code::new().code("a\nkeep\n").focus(2, 2));
1134 Box::pin(async move {
1135 code.insert(1, 0, "NEW\n", 0.2).flash().await;
1136 assert_eq!(code.code().get(), "NEW\na\nkeep\n");
1137 let f = code.focus().get();
1138 assert_eq!((f.x.round() as i32, f.y.round() as i32), (3, 3));
1139 code.replace(((3, 0), (3, 4)), "stay", 0.0).await;
1140 assert_eq!(code.code().get(), "NEW\na\nstay\n");
1141 })
1142 });
1143 }
1144
1145 #[test]
1146 fn add_records_its_call_site_and_scene_bounds() {
1147 let g = scene(|view| {
1148 view.add(Rect::new().size(100.0).name("box"));
1149 view.add(Rect::new().size(10.0).x(100.0));
1150 });
1151 let nodes = g.inspect_nodes();
1152 let named = nodes.iter().find(|n| n.name == "box").unwrap();
1153 assert_eq!(named.kind, "Rect");
1154 assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
1155 assert!(
1156 (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
1157 "{:?}",
1158 named.bounds
1159 );
1160 let auto = nodes
1161 .iter()
1162 .find(|n| n.name != "box" && n.name != "view")
1163 .unwrap();
1164 assert_eq!(auto.name, auto.source.clone().unwrap());
1165 }
1166
1167 #[test]
1168 fn builders_set_initial_values_and_handles_return_signals() {
1169 scene(|view| {
1170 let r = view.add(
1171 Rect::new()
1172 .size(50.0)
1173 .radius(5.0)
1174 .x(-10.0)
1175 .fill(Color::BLACK),
1176 );
1177 assert_eq!(r.width().get(), 50.0);
1178 assert_eq!(r.height().get(), 50.0);
1179 assert_eq!(r.radius().get(), 5.0);
1180 assert_eq!(r.x().get(), -10.0);
1181 assert_eq!(r.fill().get(), Color::BLACK);
1182 r.x().set(3.0);
1183 assert_eq!(r.x().get(), 3.0);
1184 let c = r.add(Circle::new().size(10.0));
1185 assert_eq!(r.children(), vec![c.node()]);
1186 assert_eq!(view.children(), vec![r.node()]);
1187 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
1188 });
1189 }
1190
1191 #[test]
1192 fn draw_list_composes_transforms() {
1193 let g = scene(|view| {
1194 let grp = view.add(Group::new().x(10.0).scale(2.0));
1195 grp.add(Circle::new().x(5.0));
1196 });
1197 let items = g.draw_list();
1198 assert_eq!(items.len(), 1);
1199 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
1200 }
1201
1202 #[test]
1203 fn scale_takes_a_number_or_a_pair() {
1204 scene(|view| {
1205 let r = view.add(Rect::new().scale(Vec2::new(1.6, 1.6)));
1206 assert_eq!(r.scale().get(), Vec2::new(1.6, 1.6));
1207 let r = view.add(Rect::new().scale(2));
1208 assert_eq!(r.scale().get(), Vec2::new(2.0, 2.0));
1209 r.scale().set(0.5);
1210 assert_eq!(r.scale().get(), Vec2::new(0.5, 0.5));
1211 r.scale().set((2.0, 0.5));
1212 assert_eq!(r.scale().get(), Vec2::new(2.0, 0.5));
1213 drop::<Tween<Vec2>>(r.scale().to(1.1, 0.4));
1214 drop::<Tween<Vec2>>(r.scale().to(Vec2::ZERO, 0.4));
1215 });
1216 }
1217
1218 #[test]
1219 fn layout_places_children_in_a_row() {
1220 let g = scene(|view| {
1221 view.add(
1222 Layout::new()
1223 .gap(10.0)
1224 .padding(5.0)
1225 .child(Rect::new().size(20.0))
1226 .child(Rect::new().width(40.0).height(10.0)),
1227 );
1228 });
1229 let layout = NodeId(1);
1230 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
1231 let centers: Vec<_> = g
1232 .draw_list()
1233 .iter()
1234 .map(|i| i.transform * Point::ZERO)
1235 .collect();
1236 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
1238 }
1239
1240 #[test]
1241 fn column_with_fixed_size_and_alignment() {
1242 let g = scene(|view| {
1243 view.add(
1244 Layout::new()
1245 .column()
1246 .height(100.0)
1247 .width(100.0)
1248 .justify(crate::Justify::Start)
1249 .align(crate::LayoutAlign::Start)
1250 .child(Rect::new().size(20.0)),
1251 );
1252 });
1253 let c = g.draw_list()[0].transform * Point::ZERO;
1254 assert_eq!(c, Point::new(-40.0, -40.0));
1255 }
1256
1257 #[test]
1258 fn text_line_path_and_img_draw() {
1259 let png = {
1260 let mut v = Vec::new();
1261 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
1262 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
1263 .unwrap();
1264 v
1265 };
1266 let g = scene(|view| {
1267 let t = view.add(Txt::new().text("Hi").font_size(40.0));
1268 assert!(t.size().0 > 20.0);
1269 view.add(
1270 Line::new()
1271 .points([(0.0, 0.0), (100.0, 0.0)])
1272 .end_arrow(true),
1273 );
1274 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
1275 let img = view.add(Img::new().src(&png).width(40.0));
1276 assert_eq!(img.height().get(), 20.0);
1277 });
1278 let kinds: Vec<_> = g
1279 .draw_list()
1280 .iter()
1281 .map(|i| match i.prim {
1282 Primitive::Fill { .. } => "fill",
1283 Primitive::Stroke { .. } => "stroke",
1284 Primitive::Image { .. } => "image",
1285 Primitive::PushLayer(_) => "push",
1286 Primitive::PopLayer => "pop",
1287 Primitive::ReadFrame(_) => "frame",
1288 Primitive::BeginEffect(_) => "effect",
1289 Primitive::EndEffect => "end_effect",
1290 })
1291 .collect();
1292 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
1293 }
1294
1295 #[test]
1296 fn trimming_and_pick() {
1297 let g = scene(|view| {
1298 view.add(Rect::new().size(100.0).name("back"));
1299 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
1300 });
1301 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
1302 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
1303 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
1305 }
1306
1307 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
1308 g.draw_list()
1309 .iter()
1310 .map(|i| match i.prim {
1311 Primitive::Fill { .. } => "fill",
1312 Primitive::Stroke { .. } => "stroke",
1313 Primitive::Image { .. } => "image",
1314 Primitive::PushLayer(_) => "push",
1315 Primitive::PopLayer => "pop",
1316 Primitive::ReadFrame(_) => "frame",
1317 Primitive::BeginEffect(_) => "effect",
1318 Primitive::EndEffect => "end_effect",
1319 })
1320 .collect()
1321 }
1322
1323 #[test]
1324 fn layers_only_when_needed_and_balanced() {
1325 let g = scene(|v| {
1327 v.add(Rect::new().opacity(0.5));
1328 });
1329 assert_eq!(kinds(&g), ["fill"]);
1330 let g = scene(|v| {
1332 v.add(
1333 Group::new()
1334 .opacity(0.5)
1335 .child(Rect::new())
1336 .child(Circle::new()),
1337 );
1338 });
1339 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
1340 let g = scene(|v| {
1342 v.add(
1343 Circle::new()
1344 .clip(true)
1345 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
1346 .child(Rect::new()),
1347 );
1348 });
1349 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
1350 }
1351
1352 #[test]
1353 fn z_index_orders_siblings_and_anchor_moves_box() {
1354 let g = scene(|v| {
1355 v.add(Rect::new().name("a").z_index(2.0));
1356 v.add(Rect::new().name("b"));
1357 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
1358 });
1359 let items = g.draw_list();
1360 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
1361 assert_eq!(order, vec![2, 3, 1]);
1362 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
1364 }
1365
1366 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
1367 g.draw_list()
1368 .iter()
1369 .filter_map(|i| match &i.prim {
1370 Primitive::Fill { path, .. } => {
1371 let bb = kurbo::Shape::bounding_box(path);
1372 Some((i.transform * Point::ZERO, bb.width()))
1373 }
1374 _ => None,
1375 })
1376 .collect()
1377 }
1378
1379 #[test]
1380 fn flex_grow_stretch_wrap_and_grid() {
1381 use crate::{Justify, LayoutAlign};
1382 let g = scene(|v| {
1384 v.add(
1385 Layout::new()
1386 .width(200.0)
1387 .justify(Justify::Start)
1388 .child(Rect::new().size(40.0))
1389 .child(Rect::new().size(40.0).grow(1.0)),
1390 );
1391 });
1392 let b = boxes(&g);
1393 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
1394 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
1395
1396 let g = scene(|v| {
1398 v.add(
1399 Layout::new()
1400 .column()
1401 .width(120.0)
1402 .align(LayoutAlign::Stretch)
1403 .child(Rect::new().size(30.0)),
1404 );
1405 });
1406 assert_eq!(boxes(&g)[0].1, 120.0);
1407
1408 let g = scene(|v| {
1410 v.add(
1411 Layout::new()
1412 .width(130.0)
1413 .wrap(true)
1414 .child(Rect::new().size(60.0))
1415 .child(Rect::new().size(60.0))
1416 .child(Rect::new().size(60.0)),
1417 );
1418 });
1419 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
1420 assert_eq!(ys[0], ys[1]);
1421 assert!(ys[2] > ys[0]);
1422
1423 let g = scene(|v| {
1425 v.add(
1426 Layout::new()
1427 .grid(2)
1428 .width(200.0)
1429 .align(LayoutAlign::Stretch)
1430 .child(Rect::new().size(10.0))
1431 .child(Rect::new().size(10.0))
1432 .child(Rect::new().size(10.0)),
1433 );
1434 });
1435 let b = boxes(&g);
1436 assert_eq!(
1437 b.iter().map(|b| b.1).collect::<Vec<_>>(),
1438 vec![100.0, 100.0, 100.0]
1439 );
1440 assert_eq!(b[2].0.x, b[0].0.x);
1441 assert!(b[2].0.y > b[0].0.y);
1442 }
1443
1444 #[test]
1445 fn describe_lists_signals() {
1446 let g = scene(|view| {
1447 view.add(Txt::new().text("abc"));
1448 });
1449 let d = g.describe(NodeId(1));
1450 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
1451 }
1452}
1453
1454#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1463pub struct Video(Node);
1464
1465impl Video {
1466 #[allow(clippy::new_ret_no_self)]
1468 pub fn new() -> crate::builder::VideoBuilder {
1469 crate::builder::VideoBuilder::new()
1470 }
1471
1472 pub(crate) fn from_id(id: NodeId) -> Self {
1473 Self(Node(id))
1474 }
1475
1476 pub fn node(self) -> Node {
1478 self.0
1479 }
1480
1481 fn parts(
1482 self,
1483 ) -> (
1484 std::rc::Rc<crate::video::VideoData>,
1485 Signal<f64>,
1486 Signal<f64>,
1487 ) {
1488 with_graph(|g| match &g.node(self.0.id()).kind {
1489 NodeKind::Video {
1490 data, time, rate, ..
1491 } => (data.clone(), time.clone(), rate.clone()),
1492 _ => unreachable!("handle does not point to a Video"),
1493 })
1494 }
1495
1496 pub fn time(self) -> Signal<f64> {
1499 self.parts().1
1500 }
1501
1502 pub fn playback_rate(self) -> Signal<f64> {
1505 self.parts().2
1506 }
1507
1508 pub fn duration(self) -> f64 {
1510 self.parts().0.source.info().duration
1511 }
1512
1513 pub fn is_playing(self) -> bool {
1515 self.parts().0.playback.playing.get()
1516 }
1517
1518 pub fn play(self) {
1521 let (data, time, rate) = self.parts();
1522 let pb = data.playback.clone();
1523 if pb.playing.get() {
1524 return;
1525 }
1526 pb.playing.set(true);
1527 let generation = pb.generation.get().wrapping_add(1);
1528 pb.generation.set(generation);
1529 let duration = data.source.info().duration;
1530 drop(oxframe_core::spawn(async move {
1532 loop {
1533 let live = pb.playing.get() && pb.generation.get() == generation;
1534 sync_sound(&data, &time, &rate, live);
1535 if !live {
1536 break;
1537 }
1538 oxframe_core::next_frame().await;
1539 if !(pb.playing.get() && pb.generation.get() == generation) {
1540 continue;
1541 }
1542 let t = time.get() + rate.get() * oxframe_core::dt();
1543 if duration > 0.0 && t >= duration {
1544 time.set(duration);
1545 pb.playing.set(false);
1546 } else {
1547 time.set(t.max(0.0));
1548 }
1549 }
1550 }));
1551 }
1552
1553 pub fn pause(self) {
1555 let (data, time, rate) = self.parts();
1556 data.playback.playing.set(false);
1557 sync_sound(&data, &time, &rate, false);
1558 }
1559
1560 pub fn seek(self, seconds: f64) {
1562 let (data, time, _) = self.parts();
1563 time.set(seconds.max(0.0));
1564 data.playback.jumped.set(true);
1565 }
1566}
1567
1568fn sync_sound(data: &crate::video::VideoData, time: &Signal<f64>, rate: &Signal<f64>, live: bool) {
1573 let pb = &data.playback;
1574 let now = oxframe_core::time();
1575 let audible =
1576 live && !data.muted && data.source.info().has_audio && (rate.get() - 1.0).abs() < 1e-9;
1577 if let Some((id, at, from)) = pb.segment.get()
1578 && (!audible || pb.jumped.get())
1579 {
1580 oxframe_core::end_sound(id, from + (now - at));
1581 pb.segment.set(None);
1582 }
1583 pb.jumped.set(false);
1584 if audible && pb.segment.get().is_none() {
1585 let from = time.get();
1586 let id = oxframe_core::record_sound(oxframe_core::SoundClip {
1587 file: data.path.to_string_lossy().into_owned(),
1588 at: now,
1589 volume: 1.0,
1590 fade_in: 0.0,
1591 fade_out: 0.0,
1592 trim_start: from,
1593 trim_end: None,
1594 priority: false,
1595 speech: false,
1596 });
1597 pb.segment.set(Some((id, now, from)));
1598 }
1599}
1600
1601impl Deref for Video {
1602 type Target = Node;
1603 fn deref(&self) -> &Node {
1604 &self.0
1605 }
1606}
1607
1608impl From<Video> for Node {
1609 fn from(h: Video) -> Node {
1610 h.0
1611 }
1612}