1use std::cell::{Ref, RefCell};
2use std::rc::Rc;
3
4use kurbo::{Affine, BezPath, Point, Shape, Vec2};
5use oxframe_core::{BlendMode, Color, Gradient, Signal};
6use oxframe_text::{Lang, TextStyle};
7
8use crate::text::{CodeTheme, RichSpan, Styled};
9
10use crate::draw::{DrawItem, Layer, LineCap, LineJoin, Primitive, Shadow};
11use crate::flex::{FlexBox, FlexMap};
12use crate::image::ImageSource;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct NodeId(
18 pub u32,
20);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Direction {
25 #[default]
27 Row,
28 Column,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum LayoutAlign {
35 Start,
37 #[default]
39 Center,
40 End,
42 Stretch,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Justify {
49 Start,
51 #[default]
53 Center,
54 End,
56 SpaceBetween,
58 SpaceAround,
60 SpaceEvenly,
62}
63
64#[doc(hidden)]
66pub enum NodeKind {
67 Group,
68 Rect {
69 width: Signal<f64>,
70 height: Signal<f64>,
71 radius: Signal<f64>,
72 },
73 Circle {
74 size: Signal<f64>,
75 start_angle: Signal<f64>,
77 end_angle: Signal<f64>,
78 pie: bool,
80 },
81 Polygon {
82 sides: Signal<f64>,
83 size: Signal<f64>,
84 },
85 Star {
86 points: Signal<f64>,
87 size: Signal<f64>,
88 inner: Signal<f64>,
90 },
91 Path {
92 data: Signal<BezPath>,
93 },
94 Line {
95 points: Signal<Vec<Vec2>>,
96 closed: bool,
97 smooth: bool,
99 start_arrow: bool,
100 end_arrow: bool,
101 arrow_size: Signal<f64>,
102 },
103 Txt {
104 text: Signal<String>,
105 spans: Signal<Vec<RichSpan>>,
107 font_size: Signal<f64>,
108 weight: Signal<f64>,
109 style: TextStyle,
110 morph: Signal<Option<(String, String)>>,
112 morph_t: Signal<f64>,
113 },
114 Code {
115 code: Signal<String>,
116 lang: Lang,
117 theme: CodeTheme,
118 font_size: Signal<f64>,
119 style: TextStyle,
120 focus: Signal<Vec2>,
122 dim: Signal<f64>,
124 },
125 Img {
126 source: ImageSource,
127 width: Signal<f64>,
128 height: Signal<f64>,
129 },
130 Layout {
131 direction: Direction,
132 align: LayoutAlign,
133 justify: Justify,
134 wrap: bool,
136 columns: Option<u16>,
138 gap: Signal<f64>,
139 padding: Signal<f64>,
140 width: Signal<Option<f64>>,
141 height: Signal<Option<f64>>,
142 radius: Signal<f64>,
143 },
144 Grid {
145 width: Signal<f64>,
146 height: Signal<f64>,
147 spacing: Signal<f64>,
148 },
149}
150
151impl NodeKind {
152 pub fn name(&self) -> &'static str {
153 match self {
154 Self::Group => "Group",
155 Self::Rect { .. } => "Rect",
156 Self::Circle { .. } => "Circle",
157 Self::Polygon { .. } => "Polygon",
158 Self::Star { .. } => "Star",
159 Self::Path { .. } => "Path",
160 Self::Line { .. } => "Line",
161 Self::Txt { .. } => "Txt",
162 Self::Code { .. } => "Code",
163 Self::Img { .. } => "Img",
164 Self::Layout { .. } => "Layout",
165 Self::Grid { .. } => "Grid",
166 }
167 }
168}
169
170#[doc(hidden)]
172pub struct NodeData {
173 pub id: NodeId,
174 pub name: Option<String>,
175 pub parent: Option<NodeId>,
176 pub children: Vec<NodeId>,
177 pub kind: NodeKind,
178 pub x: Signal<f64>,
179 pub y: Signal<f64>,
180 pub rotation: Signal<f64>,
182 pub scale: Signal<Vec2>,
183 pub opacity: Signal<f64>,
184 pub fill: Signal<Color>,
185 pub stroke: Signal<Color>,
186 pub line_width: Signal<f64>,
187 pub start: Signal<f64>,
189 pub end: Signal<f64>,
190 pub fill_gradient: Signal<Option<Gradient>>,
192 pub stroke_gradient: Signal<Option<Gradient>>,
193 pub dash: Signal<Vec<f64>>,
195 pub dash_offset: Signal<f64>,
196 pub line_cap: Signal<LineCap>,
197 pub line_join: Signal<LineJoin>,
198 pub blur: Signal<f64>,
200 pub shadow_color: Signal<Color>,
201 pub shadow_blur: Signal<f64>,
202 pub shadow_offset: Signal<Vec2>,
203 pub clip: Signal<bool>,
205 pub blend: Signal<BlendMode>,
206 pub z_index: Signal<f64>,
208 pub anchor: Signal<Vec2>,
212 pub grow: Signal<f64>,
214 pub shrink: Signal<f64>,
215 pub basis: Signal<Option<f64>>,
216 pub margin: Signal<f64>,
217 pub align_self: Signal<Option<LayoutAlign>>,
218}
219
220impl NodeData {
221 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
224 let s = self.scale.get();
225 let a = self.anchor.get();
226 Affine::translate(offset + Vec2::new(self.x.get(), self.y.get()))
227 * Affine::rotate(self.rotation.get().to_radians())
228 * Affine::scale_non_uniform(s.x, s.y)
229 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
230 }
231
232 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
234 match &self.kind {
235 NodeKind::Txt {
236 text,
237 spans,
238 font_size,
239 weight,
240 style,
241 morph,
242 morph_t,
243 } => {
244 let style = TextStyle {
245 size: font_size.get().max(0.1),
246 weight: weight.get().clamp(1.0, 1000.0),
247 ..style.clone()
248 };
249 if let Some((from, to)) = morph.get() {
250 let a = oxframe_text::layout(&from, &style);
251 let b = oxframe_text::layout(&to, &style);
252 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
253 return Some(TextDraw::Morph { glyphs, size });
254 }
255 let spans = spans.get();
256 Some(TextDraw::Styled(if spans.is_empty() {
257 crate::text::rich(&[RichSpan::new(text.get())], &style)
258 } else {
259 crate::text::rich(&spans, &style)
260 }))
261 }
262 NodeKind::Code {
263 code,
264 lang,
265 theme,
266 font_size,
267 style,
268 focus,
269 dim,
270 } => {
271 let style = TextStyle {
272 size: font_size.get().max(0.1),
273 ..style.clone()
274 };
275 Some(TextDraw::Styled(crate::text::code(
276 &code.get(),
277 *lang,
278 theme,
279 &style,
280 focus.get(),
281 dim.get(),
282 )))
283 }
284 _ => None,
285 }
286 }
287
288 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
290 const TOLERANCE: f64 = 0.1;
291 match &self.kind {
292 NodeKind::Group
293 | NodeKind::Img { .. }
294 | NodeKind::Txt { .. }
295 | NodeKind::Code { .. }
296 | NodeKind::Grid { .. } => None,
297 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
298 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
299 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
300 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
301 Some(if r > 0.0 {
302 rect.to_rounded_rect(r).to_path(TOLERANCE)
303 } else {
304 rect.to_path(TOLERANCE)
305 })
306 }
307 NodeKind::Circle {
308 start_angle,
309 end_angle,
310 pie,
311 ..
312 } => {
313 let r = size.0.max(0.0) / 2.0;
316 let (a0, a1) = (start_angle.get(), end_angle.get());
317 let sweep = (a1 - a0).clamp(-360.0, 360.0);
318 if sweep.abs() >= 360.0 - 1e-9 {
319 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
320 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
321 }
322 let arc = kurbo::Arc::new(
323 (0.0, 0.0),
324 (r, r),
325 (a0 - 90.0).to_radians(),
326 sweep.to_radians(),
327 0.0,
328 );
329 let mut p = BezPath::new();
330 if *pie {
331 p.move_to((0.0, 0.0));
332 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
333 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
334 p.close_path();
335 } else {
336 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
337 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
338 }
339 Some(p)
340 }
341 NodeKind::Polygon { sides, .. } => {
342 let n = sides.get().round().max(3.0) as usize;
343 Some(regular(n, size.0.max(0.0) / 2.0, None))
344 }
345 NodeKind::Star { points, inner, .. } => {
346 let n = points.get().round().max(2.0) as usize;
347 let r = size.0.max(0.0) / 2.0;
348 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
349 }
350 NodeKind::Path { data } => Some(data.get()),
351 NodeKind::Line {
352 points,
353 closed,
354 smooth,
355 ..
356 } => {
357 let pts = points.get();
358 if *smooth && pts.len() > 2 {
359 return Some(catmull_rom(&pts, *closed));
360 }
361 let mut p = BezPath::new();
362 for (i, v) in pts.iter().enumerate() {
363 if i == 0 {
364 p.move_to(v.to_point());
365 } else {
366 p.line_to(v.to_point());
367 }
368 }
369 if *closed && pts.len() > 2 {
370 p.close_path();
371 }
372 Some(p)
373 }
374 }
375 }
376}
377
378fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
380 let count = if inner.is_some() { 2 * n } else { n };
381 let mut p = BezPath::new();
382 for i in 0..count {
383 let radius = match inner {
384 Some(ri) if i % 2 == 1 => ri,
385 _ => r,
386 };
387 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
388 let pt = Point::new(a.cos() * radius, a.sin() * radius);
389 if i == 0 {
390 p.move_to(pt);
391 } else {
392 p.line_to(pt);
393 }
394 }
395 p.close_path();
396 p
397}
398
399fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
401 let n = pts.len();
402 let at = |i: isize| -> Vec2 {
403 if closed {
404 pts[i.rem_euclid(n as isize) as usize]
405 } else {
406 pts[i.clamp(0, n as isize - 1) as usize]
407 }
408 };
409 let mut p = BezPath::new();
410 p.move_to(pts[0].to_point());
411 let segs = if closed { n } else { n - 1 };
412 for i in 0..segs as isize {
413 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
414 let c1 = p1 + (p2 - p0) / 6.0;
415 let c2 = p2 - (p3 - p1) / 6.0;
416 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
417 }
418 if closed {
419 p.close_path();
420 }
421 p
422}
423
424pub(crate) enum TextDraw {
425 Styled(Styled),
426 Morph {
428 glyphs: Vec<(BezPath, f64)>,
429 size: (f64, f64),
430 },
431}
432
433pub struct SceneGraph {
435 nodes: RefCell<Vec<NodeData>>,
436 size: (u32, u32),
437}
438
439impl SceneGraph {
440 pub const ROOT: NodeId = NodeId(0);
442
443 pub fn new(width: u32, height: u32) -> Rc<Self> {
445 let graph = Rc::new(Self {
446 nodes: RefCell::new(Vec::new()),
447 size: (width, height),
448 });
449 graph.insert(
450 None,
451 NodeKind::Group,
452 crate::BaseProps {
453 name: Some("view".into()),
454 ..Default::default()
455 },
456 );
457 graph
458 }
459
460 pub fn size(&self) -> (u32, u32) {
462 self.size
463 }
464
465 pub fn len(&self) -> usize {
467 self.nodes.borrow().len()
468 }
469
470 pub fn is_empty(&self) -> bool {
472 self.len() == 0
473 }
474
475 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
481 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
482 }
483
484 pub(crate) fn next_id(&self) -> u32 {
485 self.len() as u32
486 }
487
488 pub(crate) fn insert(
489 &self,
490 parent: Option<NodeId>,
491 kind: NodeKind,
492 base: crate::BaseProps,
493 ) -> NodeId {
494 let mut nodes = self.nodes.borrow_mut();
495 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
496 let label = format!("{}#{}", kind.name(), id.0);
497 let sig = |field: &str| format!("{label}.{field}");
498 nodes.push(NodeData {
499 id,
500 name: base.name,
501 parent,
502 children: Vec::new(),
503 x: Signal::new(base.x).with_name(sig("x")),
504 y: Signal::new(base.y).with_name(sig("y")),
505 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
506 scale: Signal::new(base.scale).with_name(sig("scale")),
507 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
508 fill: Signal::new(base.fill).with_name(sig("fill")),
509 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
510 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
511 start: Signal::new(base.start).with_name(sig("start")),
512 end: Signal::new(base.end).with_name(sig("end")),
513 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
514 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
515 dash: Signal::new(base.dash).with_name(sig("dash")),
516 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
517 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
518 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
519 blur: Signal::new(base.blur).with_name(sig("blur")),
520 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
521 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
522 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
523 clip: Signal::new(base.clip).with_name(sig("clip")),
524 blend: Signal::new(base.blend).with_name(sig("blend")),
525 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
526 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
527 grow: Signal::new(base.grow).with_name(sig("grow")),
528 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
529 basis: Signal::new(base.basis).with_name(sig("basis")),
530 margin: Signal::new(base.margin).with_name(sig("margin")),
531 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
532 kind,
533 });
534 if let Some(p) = parent {
535 nodes[p.0 as usize].children.push(id);
536 }
537 id
538 }
539
540 pub(crate) fn detach(&self, id: NodeId) {
541 let mut nodes = self.nodes.borrow_mut();
542 if let Some(p) = nodes[id.0 as usize].parent.take() {
543 nodes[p.0 as usize].children.retain(|c| *c != id);
544 }
545 }
546
547 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
548 self.detach(id);
549 let mut nodes = self.nodes.borrow_mut();
550 nodes[id.0 as usize].parent = Some(parent);
551 nodes[parent.0 as usize].children.push(id);
552 }
553
554 pub fn measure(&self, id: NodeId) -> (f64, f64) {
556 let nodes = self.nodes.borrow();
557 measure(&nodes, id)
558 }
559
560 pub fn draw_list(&self) -> Vec<DrawItem> {
562 let nodes = self.nodes.borrow();
563 let mut out = Vec::new();
564 visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
565 out
566 }
567
568 pub fn pick(&self, p: Point) -> Option<NodeId> {
570 self.draw_list()
571 .iter()
572 .rev()
573 .find(|item| item.node != Self::ROOT && item.hit(p))
574 .map(|item| item.node)
575 }
576
577 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
579 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
582 return Some(item.transform);
583 }
584 let nodes = self.nodes.borrow();
585 let mut tf = Affine::IDENTITY;
586 let mut cur = Some(id);
587 while let Some(c) = cur {
588 let n = nodes.get(c.0 as usize)?;
589 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
590 cur = n.parent;
591 }
592 Some(tf)
593 }
594
595 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
597 let nodes = self.nodes.borrow();
598 let mut out = Vec::new();
599 let mut stack = vec![Self::ROOT];
600 while let Some(id) = stack.pop() {
601 let n = &nodes[id.0 as usize];
602 out.push((id, n.parent, n.kind.name(), n.name.clone()));
603 stack.extend(n.children.iter().rev());
604 }
605 out
606 }
607
608 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
610 let nodes = self.nodes.borrow();
611 let Some(n) = nodes.get(id.0 as usize) else {
612 return Vec::new();
613 };
614 let f = |v: f64| format!("{v:.3}");
615 let s = n.scale.get();
616 let mut out = vec![
617 ("x".into(), f(n.x.get())),
618 ("y".into(), f(n.y.get())),
619 ("rotation".into(), f(n.rotation.get())),
620 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
621 ("opacity".into(), f(n.opacity.get())),
622 ("fill".into(), n.fill.get().to_string()),
623 ("stroke".into(), n.stroke.get().to_string()),
624 ("line_width".into(), f(n.line_width.get())),
625 ("start".into(), f(n.start.get())),
626 ("end".into(), f(n.end.get())),
627 ];
628 let (w, h) = measure(&nodes, id);
629 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
630 match &n.kind {
631 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
632 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
633 NodeKind::Txt {
634 text,
635 font_size,
636 weight,
637 ..
638 } => {
639 out.push(("text".into(), text.get()));
640 out.push(("font_size".into(), f(font_size.get())));
641 out.push(("weight".into(), f(weight.get())));
642 }
643 NodeKind::Line { points, .. } => {
644 out.push(("points".into(), points.get().len().to_string()))
645 }
646 NodeKind::Layout { gap, padding, .. } => {
647 out.push(("gap".into(), f(gap.get())));
648 out.push(("padding".into(), f(padding.get())));
649 }
650 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
651 _ => {}
652 }
653 out
654 }
655}
656
657fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
658 let n = &nodes[id.0 as usize];
659 match &n.kind {
660 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
661 NodeKind::Circle { size, .. }
662 | NodeKind::Polygon { size, .. }
663 | NodeKind::Star { size, .. } => (size.get(), size.get()),
664 NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
665 (width.get(), height.get())
666 }
667 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
668 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
669 Some(TextDraw::Morph { size, .. }) => size,
670 None => (0.0, 0.0),
671 },
672 NodeKind::Path { .. } | NodeKind::Line { .. } => {
673 let bb = n
674 .outline((0.0, 0.0))
675 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
676 (bb.width(), bb.height())
677 }
678 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
679 NodeKind::Group => {
680 let mut bb: Option<kurbo::Rect> = None;
681 for &c in &n.children {
682 let child = &nodes[c.0 as usize];
683 let (w, h) = measure(nodes, c);
684 let r = child
685 .local_transform(Vec2::ZERO, (w, h))
686 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
687 bb = Some(bb.map_or(r, |b| b.union(r)));
688 }
689 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
690 }
691 }
692}
693
694fn resizable(kind: &NodeKind) -> bool {
696 matches!(
697 kind,
698 NodeKind::Rect { .. }
699 | NodeKind::Layout { .. }
700 | NodeKind::Img { .. }
701 | NodeKind::Grid { .. }
702 )
703}
704
705fn visit(
706 nodes: &[NodeData],
707 id: NodeId,
708 parent_tf: Affine,
709 placement: Option<FlexBox>,
710 flex: Option<&FlexMap>,
711 out: &mut Vec<DrawItem>,
712) {
713 let n = &nodes[id.0 as usize];
714 let opacity = n.opacity.get().clamp(0.0, 1.0);
715 if opacity <= 0.0 {
716 return;
717 }
718 let size = match placement {
719 Some(b) if resizable(&n.kind) => {
720 let s = n.scale.get();
721 (
722 b.size.0 / s.x.abs().max(1e-9),
723 b.size.1 / s.y.abs().max(1e-9),
724 )
725 }
726 _ => measure(nodes, id),
727 };
728 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
729 let tf = parent_tf * n.local_transform(offset, size);
730
731 let blend = n.blend.get();
734 let blur = n.blur.get().max(0.0);
735 let shadow_color = n.shadow_color.get();
736 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
737 color: shadow_color,
738 blur: n.shadow_blur.get().max(0.0),
739 offset: n.shadow_offset.get(),
740 });
741 let layered = blend != BlendMode::Normal
742 || blur > 0.0
743 || shadow.is_some()
744 || (opacity < 1.0 && !n.children.is_empty());
745 if layered {
746 out.push(DrawItem {
747 node: id,
748 transform: tf,
749 prim: Primitive::PushLayer(Layer {
750 opacity,
751 blend,
752 clip: None,
753 blur,
754 shadow,
755 }),
756 });
757 }
758 let paint_opacity = if layered { 1.0 } else { opacity };
759 for prim in crate::draw::primitives(n, size, paint_opacity) {
760 out.push(DrawItem {
761 node: id,
762 transform: tf,
763 prim,
764 });
765 }
766
767 if !n.children.is_empty() {
768 let clip = n.clip.get().then(|| {
770 n.outline(size).unwrap_or_else(|| {
771 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
772 .to_path(0.1)
773 })
774 });
775 let clipped = clip.is_some();
776 if let Some(path) = clip {
777 out.push(DrawItem {
778 node: id,
779 transform: tf,
780 prim: Primitive::PushLayer(Layer::clip(path)),
781 });
782 }
783 let own;
786 let map: Option<&FlexMap> = match (&n.kind, flex) {
787 (NodeKind::Layout { .. }, Some(m)) => Some(m),
788 (NodeKind::Layout { .. }, None) => {
789 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
790 Some(&own)
791 }
792 _ => None,
793 };
794 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
795 order.sort_by(|a, b| {
797 let za = nodes[a.1.0 as usize].z_index.get();
798 let zb = nodes[b.1.0 as usize].z_index.get();
799 za.total_cmp(&zb)
800 });
801 for (_, c) in order {
802 let placement = map.and_then(|m| m.get(&c).copied());
803 visit(nodes, c, tf, placement, map, out);
804 }
805 if clipped {
806 out.push(DrawItem {
807 node: id,
808 transform: tf,
809 prim: Primitive::PopLayer,
810 });
811 }
812 }
813 if layered {
814 out.push(DrawItem {
815 node: id,
816 transform: tf,
817 prim: Primitive::PopLayer,
818 });
819 }
820}
821
822impl DrawItem {
823 fn hit(&self, p: Point) -> bool {
824 let local = self.transform.inverse() * p;
825 match &self.prim {
826 Primitive::Fill { path, .. } => {
827 (is_text_like(path) && path.bounding_box().contains(local))
828 || closed(path).contains(local)
829 }
830 Primitive::Stroke { path, style, .. } => {
831 let tol = (style.width / 2.0)
832 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
833 path.segments().any(|s| {
834 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
835 })
836 }
837 Primitive::Image { rect, .. } => rect.contains(local),
838 Primitive::PushLayer(_) | Primitive::PopLayer => false,
839 }
840 }
841}
842
843fn closed(path: &BezPath) -> BezPath {
845 let mut out = BezPath::new();
846 let mut open = false;
847 for el in path.elements() {
848 match el {
849 kurbo::PathEl::MoveTo(_) => {
850 if open {
851 out.close_path();
852 }
853 open = true;
854 }
855 kurbo::PathEl::ClosePath => open = false,
856 _ => {}
857 }
858 out.push(*el);
859 }
860 if open {
861 out.close_path();
862 }
863 out
864}
865
866fn is_text_like(path: &BezPath) -> bool {
869 path.elements()
870 .iter()
871 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
872 .count()
873 > 3
874}
875
876thread_local! {
877 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
878}
879
880impl SceneGraph {
881 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
883 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
884 struct Restore(Option<Rc<SceneGraph>>);
885 impl Drop for Restore {
886 fn drop(&mut self) {
887 let prev = self.0.take();
888 CURRENT.with(|c| *c.borrow_mut() = prev);
889 }
890 }
891 let _restore = Restore(prev);
892 f()
893 }
894}
895
896pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
897 CURRENT.with(|c| {
898 let c = c.borrow();
899 f(c.as_ref()
900 .expect("oxframe: node handles can only be used while a scene is running"))
901 })
902}