1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use oxframe_core::{BlendMode, Color, Gradient, Signal, Vec2};
5
6use crate::builder::{
7 self, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder, LayoutBuilder,
8 LineBuilder, NodeBuilder, PathBuilder, PolygonBuilder, RectBuilder, StarBuilder, TxtBuilder,
9};
10use crate::draw::{LineCap, LineJoin};
11use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
12use crate::text::RichSpan;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct Node(NodeId);
17
18macro_rules! base_signal {
19 ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
20 $(
21 $(#[$doc])*
22 #[doc = ""]
23 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
24 pub fn $field(self) -> Signal<$ty> {
25 with_graph(|g| g.node(self.0).$field.clone())
26 }
27 )*
28 };
29}
30
31impl Node {
32 pub fn id(self) -> NodeId {
34 self.0
35 }
36
37 base_signal! {
38 x: f64,
39 y: f64,
40 rotation: f64,
42 scale: Vec2,
43 opacity: f64,
44 fill: Color,
45 stroke: Color,
46 line_width: f64,
47 start: f64,
49 end: f64,
51 fill_gradient: Option<Gradient>,
52 stroke_gradient: Option<Gradient>,
53 dash: Vec<f64>,
54 dash_offset: f64,
55 line_cap: LineCap,
56 line_join: LineJoin,
57 blur: f64,
58 shadow_color: Color,
59 shadow_blur: f64,
60 shadow_offset: Vec2,
61 clip: bool,
62 blend: BlendMode,
63 z_index: f64,
64 anchor: Vec2,
65 grow: f64,
66 shrink: f64,
67 basis: Option<f64>,
68 margin: f64,
69 align_self: Option<crate::LayoutAlign>,
70 }
71
72 #[allow(clippy::should_implement_trait)] pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
75 builder::add(self.0, child)
76 }
77
78 pub fn remove(self) {
80 with_graph(|g| g.detach(self.0));
81 }
82
83 pub fn reparent(self, parent: impl Into<Node>) {
85 let parent = parent.into();
86 with_graph(|g| g.reparent(self.0, parent.0));
87 }
88
89 pub fn children(self) -> Vec<Node> {
91 with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
92 }
93
94 pub fn size(self) -> (f64, f64) {
96 with_graph(|g| g.measure(self.0))
97 }
98
99 pub fn absolute_position(self) -> Vec2 {
101 with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
102 }
103}
104
105macro_rules! typed_handle {
106 ($name:ident, $builder:ident) => {
107 #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
108 #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
109 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110 pub struct $name(Node);
111
112 impl $name {
113 #[allow(clippy::new_ret_no_self)]
115 pub fn new() -> $builder {
116 $builder::default()
117 }
118
119 pub(crate) fn from_id(id: NodeId) -> Self {
120 Self(Node(id))
121 }
122
123 pub fn node(self) -> Node {
125 self.0
126 }
127 }
128
129 impl Deref for $name {
130 type Target = Node;
131 fn deref(&self) -> &Node {
132 &self.0
133 }
134 }
135
136 impl From<$name> for Node {
137 fn from(h: $name) -> Node {
138 h.0
139 }
140 }
141 };
142}
143
144typed_handle!(Group, GroupBuilder);
145typed_handle!(Rect, RectBuilder);
146typed_handle!(Circle, CircleBuilder);
147typed_handle!(Path, PathBuilder);
148typed_handle!(Line, LineBuilder);
149typed_handle!(Txt, TxtBuilder);
150typed_handle!(Img, ImgBuilder);
151typed_handle!(Layout, LayoutBuilder);
152typed_handle!(Grid, GridBuilder);
153typed_handle!(Polygon, PolygonBuilder);
154typed_handle!(Star, StarBuilder);
155typed_handle!(Code, CodeBuilder);
156
157macro_rules! kind_signal {
158 ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
159 impl $handle {
160 $(
161 $(#[$doc])*
162 #[doc = ""]
163 #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
164 pub fn $field(self) -> Signal<$ty> {
165 with_graph(|g| match &g.node(self.id()).kind {
166 NodeKind::$kind { $field, .. } => $field.clone(),
167 _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
168 })
169 }
170 )*
171 }
172 };
173}
174
175kind_signal!(Rect::Rect {
176 width: f64,
177 height: f64,
178 radius: f64
179});
180kind_signal!(Circle::Circle {
181 size: f64,
183 start_angle: f64,
184 end_angle: f64,
185});
186kind_signal!(Polygon::Polygon {
187 sides: f64,
188 size: f64
189});
190kind_signal!(Star::Star {
191 points: f64,
192 size: f64,
193 inner: f64
194});
195kind_signal!(Path::Path { data: BezPath });
196kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
197kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
198kind_signal!(Code::Code {
199 code: String,
200 font_size: f64,
201 focus: Vec2,
203 dim: f64,
205});
206
207impl Txt {
208 pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
211 let to = text.into();
212 let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
213 NodeKind::Txt {
214 text,
215 morph,
216 morph_t,
217 ..
218 } => (text.clone(), morph.clone(), morph_t.clone()),
219 _ => unreachable!("handle does not point to a Txt"),
220 });
221 Box::pin(async move {
222 morph.set(Some((txt.get(), to.clone())));
223 t.set(0.0);
224 t.to(1.0, duration).await;
225 txt.set(to);
226 morph.set(None);
227 })
228 }
229}
230
231impl Code {
232 pub fn focus_lines(
234 self,
235 first: usize,
236 last: usize,
237 duration: f64,
238 ) -> oxframe_core::Tween<Vec2> {
239 self.focus()
240 .to(Vec2::new(first as f64, last as f64), duration)
241 }
242}
243kind_signal!(Img::Img {
244 width: f64,
245 height: f64
246});
247kind_signal!(Layout::Layout {
248 gap: f64,
249 padding: f64,
250 radius: f64,
251 width: Option<f64>,
253 height: Option<f64>,
255});
256kind_signal!(Grid::Grid {
257 width: f64,
258 height: f64,
259 spacing: f64
260});
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub struct View(Node);
265
266impl View {
267 pub fn current() -> Self {
269 Self(Node(SceneGraph::ROOT))
270 }
271
272 pub fn size(self) -> (u32, u32) {
274 with_graph(|g| g.size())
275 }
276}
277
278impl Deref for View {
279 type Target = Node;
280 fn deref(&self) -> &Node {
281 &self.0
282 }
283}
284
285impl From<View> for Node {
286 fn from(v: View) -> Node {
287 v.0
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::Primitive;
295
296 fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
297 let graph = SceneGraph::new(400, 400);
298 graph.enter(|| f(View::current()));
299 graph
300 }
301
302 #[test]
303 fn builders_set_initial_values_and_handles_return_signals() {
304 scene(|view| {
305 let r = view.add(
306 Rect::new()
307 .size(50.0)
308 .radius(5.0)
309 .x(-10.0)
310 .fill(Color::BLACK),
311 );
312 assert_eq!(r.width().get(), 50.0);
313 assert_eq!(r.height().get(), 50.0);
314 assert_eq!(r.radius().get(), 5.0);
315 assert_eq!(r.x().get(), -10.0);
316 assert_eq!(r.fill().get(), Color::BLACK);
317 r.x().set(3.0);
318 assert_eq!(r.x().get(), 3.0);
319 let c = r.add(Circle::new().size(10.0));
320 assert_eq!(r.children(), vec![c.node()]);
321 assert_eq!(view.children(), vec![r.node()]);
322 assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
323 });
324 }
325
326 #[test]
327 fn draw_list_composes_transforms() {
328 let g = scene(|view| {
329 let grp = view.add(Group::new().x(10.0).scale(2.0));
330 grp.add(Circle::new().x(5.0));
331 });
332 let items = g.draw_list();
333 assert_eq!(items.len(), 1);
334 assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
335 }
336
337 #[test]
338 fn layout_places_children_in_a_row() {
339 let g = scene(|view| {
340 view.add(
341 Layout::new()
342 .gap(10.0)
343 .padding(5.0)
344 .child(Rect::new().size(20.0))
345 .child(Rect::new().width(40.0).height(10.0)),
346 );
347 });
348 let layout = NodeId(1);
349 assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
350 let centers: Vec<_> = g
351 .draw_list()
352 .iter()
353 .map(|i| i.transform * Point::ZERO)
354 .collect();
355 assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
357 }
358
359 #[test]
360 fn column_with_fixed_size_and_alignment() {
361 let g = scene(|view| {
362 view.add(
363 Layout::new()
364 .column()
365 .height(100.0)
366 .width(100.0)
367 .justify(crate::Justify::Start)
368 .align(crate::LayoutAlign::Start)
369 .child(Rect::new().size(20.0)),
370 );
371 });
372 let c = g.draw_list()[0].transform * Point::ZERO;
373 assert_eq!(c, Point::new(-40.0, -40.0));
374 }
375
376 #[test]
377 fn text_line_path_and_img_draw() {
378 let png = {
379 let mut v = Vec::new();
380 image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
381 .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
382 .unwrap();
383 v
384 };
385 let g = scene(|view| {
386 let t = view.add(Txt::new().text("Hi").font_size(40.0));
387 assert!(t.size().0 > 20.0);
388 view.add(
389 Line::new()
390 .points([(0.0, 0.0), (100.0, 0.0)])
391 .end_arrow(true),
392 );
393 view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
394 let img = view.add(Img::new().src(&png).width(40.0));
395 assert_eq!(img.height().get(), 20.0);
396 });
397 let kinds: Vec<_> = g
398 .draw_list()
399 .iter()
400 .map(|i| match i.prim {
401 Primitive::Fill { .. } => "fill",
402 Primitive::Stroke { .. } => "stroke",
403 Primitive::Image { .. } => "image",
404 Primitive::PushLayer(_) => "push",
405 Primitive::PopLayer => "pop",
406 })
407 .collect();
408 assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
409 }
410
411 #[test]
412 fn trimming_and_pick() {
413 let g = scene(|view| {
414 view.add(Rect::new().size(100.0).name("back"));
415 view.add(Circle::new().size(40.0).x(30.0).end(0.5));
416 });
417 assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
418 assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
419 assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
421 }
422
423 fn kinds(g: &SceneGraph) -> Vec<&'static str> {
424 g.draw_list()
425 .iter()
426 .map(|i| match i.prim {
427 Primitive::Fill { .. } => "fill",
428 Primitive::Stroke { .. } => "stroke",
429 Primitive::Image { .. } => "image",
430 Primitive::PushLayer(_) => "push",
431 Primitive::PopLayer => "pop",
432 })
433 .collect()
434 }
435
436 #[test]
437 fn layers_only_when_needed_and_balanced() {
438 let g = scene(|v| {
440 v.add(Rect::new().opacity(0.5));
441 });
442 assert_eq!(kinds(&g), ["fill"]);
443 let g = scene(|v| {
445 v.add(
446 Group::new()
447 .opacity(0.5)
448 .child(Rect::new())
449 .child(Circle::new()),
450 );
451 });
452 assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
453 let g = scene(|v| {
455 v.add(
456 Circle::new()
457 .clip(true)
458 .shadow(Color::BLACK, 4.0, (2.0, 2.0))
459 .child(Rect::new()),
460 );
461 });
462 assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
463 }
464
465 #[test]
466 fn z_index_orders_siblings_and_anchor_moves_box() {
467 let g = scene(|v| {
468 v.add(Rect::new().name("a").z_index(2.0));
469 v.add(Rect::new().name("b"));
470 v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
471 });
472 let items = g.draw_list();
473 let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
474 assert_eq!(order, vec![2, 3, 1]);
475 assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
477 }
478
479 fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
480 g.draw_list()
481 .iter()
482 .filter_map(|i| match &i.prim {
483 Primitive::Fill { path, .. } => {
484 let bb = kurbo::Shape::bounding_box(path);
485 Some((i.transform * Point::ZERO, bb.width()))
486 }
487 _ => None,
488 })
489 .collect()
490 }
491
492 #[test]
493 fn flex_grow_stretch_wrap_and_grid() {
494 use crate::{Justify, LayoutAlign};
495 let g = scene(|v| {
497 v.add(
498 Layout::new()
499 .width(200.0)
500 .justify(Justify::Start)
501 .child(Rect::new().size(40.0))
502 .child(Rect::new().size(40.0).grow(1.0)),
503 );
504 });
505 let b = boxes(&g);
506 assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
507 assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
508
509 let g = scene(|v| {
511 v.add(
512 Layout::new()
513 .column()
514 .width(120.0)
515 .align(LayoutAlign::Stretch)
516 .child(Rect::new().size(30.0)),
517 );
518 });
519 assert_eq!(boxes(&g)[0].1, 120.0);
520
521 let g = scene(|v| {
523 v.add(
524 Layout::new()
525 .width(130.0)
526 .wrap(true)
527 .child(Rect::new().size(60.0))
528 .child(Rect::new().size(60.0))
529 .child(Rect::new().size(60.0)),
530 );
531 });
532 let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
533 assert_eq!(ys[0], ys[1]);
534 assert!(ys[2] > ys[0]);
535
536 let g = scene(|v| {
538 v.add(
539 Layout::new()
540 .grid(2)
541 .width(200.0)
542 .align(LayoutAlign::Stretch)
543 .child(Rect::new().size(10.0))
544 .child(Rect::new().size(10.0))
545 .child(Rect::new().size(10.0)),
546 );
547 });
548 let b = boxes(&g);
549 assert_eq!(
550 b.iter().map(|b| b.1).collect::<Vec<_>>(),
551 vec![100.0, 100.0, 100.0]
552 );
553 assert_eq!(b[2].0.x, b[0].0.x);
554 assert!(b[2].0.y > b[0].0.y);
555 }
556
557 #[test]
558 fn describe_lists_signals() {
559 let g = scene(|view| {
560 view.add(Txt::new().text("abc"));
561 });
562 let d = g.describe(NodeId(1));
563 assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
564 }
565}