1use std::cell::{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 Camera {
71 zoom: Signal<f64>,
72 },
73 Rect {
74 width: Signal<f64>,
75 height: Signal<f64>,
76 radius: Signal<f64>,
77 },
78 Circle {
79 size: Signal<f64>,
80 start_angle: Signal<f64>,
82 end_angle: Signal<f64>,
83 pie: bool,
85 },
86 Polygon {
87 sides: Signal<f64>,
88 size: Signal<f64>,
89 },
90 Star {
91 points: Signal<f64>,
92 size: Signal<f64>,
93 inner: Signal<f64>,
95 },
96 Path {
97 data: Signal<BezPath>,
98 },
99 Line {
100 points: Signal<Vec<Vec2>>,
101 closed: bool,
102 smooth: bool,
104 start_arrow: bool,
105 end_arrow: bool,
106 arrow_size: Signal<f64>,
107 },
108 Txt {
109 text: Signal<String>,
110 spans: Signal<Vec<RichSpan>>,
112 font_size: Signal<f64>,
113 weight: Signal<f64>,
114 style: TextStyle,
115 morph: Signal<Option<(String, String)>>,
117 morph_t: Signal<f64>,
118 },
119 Code {
120 code: Signal<String>,
121 lang: Lang,
122 theme: CodeTheme,
123 font_size: Signal<f64>,
124 style: TextStyle,
125 focus: Signal<Vec2>,
127 dim: Signal<f64>,
129 },
130 Img {
131 source: ImageSource,
132 width: Signal<f64>,
133 height: Signal<f64>,
134 },
135 Layout {
136 direction: Direction,
137 align: LayoutAlign,
138 justify: Justify,
139 wrap: bool,
141 columns: Option<u16>,
143 gap: Signal<f64>,
144 padding: Signal<f64>,
145 width: Signal<Option<f64>>,
146 height: Signal<Option<f64>>,
147 radius: Signal<f64>,
148 },
149 Grid {
150 width: Signal<f64>,
151 height: Signal<f64>,
152 spacing: Signal<f64>,
153 },
154}
155
156impl NodeKind {
157 pub fn name(&self) -> &'static str {
158 match self {
159 Self::Group => "Group",
160 Self::Camera { .. } => "Camera",
161 Self::Rect { .. } => "Rect",
162 Self::Circle { .. } => "Circle",
163 Self::Polygon { .. } => "Polygon",
164 Self::Star { .. } => "Star",
165 Self::Path { .. } => "Path",
166 Self::Line { .. } => "Line",
167 Self::Txt { .. } => "Txt",
168 Self::Code { .. } => "Code",
169 Self::Img { .. } => "Img",
170 Self::Layout { .. } => "Layout",
171 Self::Grid { .. } => "Grid",
172 }
173 }
174}
175
176#[doc(hidden)]
178pub struct NodeData {
179 pub id: NodeId,
180 pub name: Option<String>,
181 pub source: Option<String>,
183 pub parent: Option<NodeId>,
184 pub children: Vec<NodeId>,
185 pub kind: NodeKind,
186 pub x: Signal<f64>,
187 pub y: Signal<f64>,
188 pub rotation: Signal<f64>,
190 pub scale: Signal<Vec2>,
191 pub opacity: Signal<f64>,
192 pub fill: Signal<Color>,
193 pub stroke: Signal<Color>,
194 pub line_width: Signal<f64>,
195 pub start: Signal<f64>,
197 pub end: Signal<f64>,
198 pub fill_gradient: Signal<Option<Gradient>>,
200 pub stroke_gradient: Signal<Option<Gradient>>,
201 pub dash: Signal<Vec<f64>>,
203 pub dash_offset: Signal<f64>,
204 pub line_cap: Signal<LineCap>,
205 pub line_join: Signal<LineJoin>,
206 pub blur: Signal<f64>,
208 pub shadow_color: Signal<Color>,
209 pub shadow_blur: Signal<f64>,
210 pub shadow_offset: Signal<Vec2>,
211 pub clip: Signal<bool>,
213 pub blend: Signal<BlendMode>,
214 pub z_index: Signal<f64>,
216 pub anchor: Signal<Vec2>,
220 pub grow: Signal<f64>,
222 pub shrink: Signal<f64>,
223 pub basis: Signal<Option<f64>>,
224 pub margin: Signal<f64>,
225 pub align_self: Signal<Option<LayoutAlign>>,
226 pub place: Option<crate::place::Place>,
228 pub placed: Cell<(f64, f64)>,
230}
231
232impl NodeData {
233 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
236 if let NodeKind::Camera { zoom } = &self.kind {
237 let (x, y) = self.placed.get();
241 return Affine::translate(offset)
242 * Affine::rotate(-self.rotation.get().to_radians())
243 * Affine::scale(zoom.get().max(1e-6))
244 * Affine::translate((-x, -y));
245 }
246 let s = self.scale.get();
247 let a = self.anchor.get();
248 let (x, y) = self.placed.get();
249 Affine::translate(offset + Vec2::new(x, y))
250 * Affine::rotate(self.rotation.get().to_radians())
251 * Affine::scale_non_uniform(s.x, s.y)
252 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
253 }
254
255 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
257 match &self.kind {
258 NodeKind::Txt {
259 text,
260 spans,
261 font_size,
262 weight,
263 style,
264 morph,
265 morph_t,
266 } => {
267 let style = TextStyle {
268 size: font_size.get().max(0.1),
269 weight: weight.get().clamp(1.0, 1000.0),
270 ..style.clone()
271 };
272 if let Some((from, to)) = morph.get() {
273 let a = oxframe_text::layout(&from, &style);
274 let b = oxframe_text::layout(&to, &style);
275 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
276 return Some(TextDraw::Morph { glyphs, size });
277 }
278 let spans = spans.get();
279 Some(TextDraw::Styled(if spans.is_empty() {
280 crate::text::rich(&[RichSpan::new(text.get())], &style)
281 } else {
282 crate::text::rich(&spans, &style)
283 }))
284 }
285 NodeKind::Code {
286 code,
287 lang,
288 theme,
289 font_size,
290 style,
291 focus,
292 dim,
293 } => {
294 let style = TextStyle {
295 size: font_size.get().max(0.1),
296 ..style.clone()
297 };
298 Some(TextDraw::Styled(crate::text::code(
299 &code.get(),
300 *lang,
301 theme,
302 &style,
303 focus.get(),
304 dim.get(),
305 )))
306 }
307 _ => None,
308 }
309 }
310
311 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
313 const TOLERANCE: f64 = 0.1;
314 match &self.kind {
315 NodeKind::Group
316 | NodeKind::Camera { .. }
317 | NodeKind::Img { .. }
318 | NodeKind::Txt { .. }
319 | NodeKind::Code { .. }
320 | NodeKind::Grid { .. } => None,
321 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
322 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
323 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
324 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
325 Some(if r > 0.0 {
326 rect.to_rounded_rect(r).to_path(TOLERANCE)
327 } else {
328 rect.to_path(TOLERANCE)
329 })
330 }
331 NodeKind::Circle {
332 start_angle,
333 end_angle,
334 pie,
335 ..
336 } => {
337 let r = size.0.max(0.0) / 2.0;
340 let (a0, a1) = (start_angle.get(), end_angle.get());
341 let sweep = (a1 - a0).clamp(-360.0, 360.0);
342 if sweep.abs() >= 360.0 - 1e-9 {
343 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
344 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
345 }
346 let arc = kurbo::Arc::new(
347 (0.0, 0.0),
348 (r, r),
349 (a0 - 90.0).to_radians(),
350 sweep.to_radians(),
351 0.0,
352 );
353 let mut p = BezPath::new();
354 if *pie {
355 p.move_to((0.0, 0.0));
356 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
357 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
358 p.close_path();
359 } else {
360 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
361 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
362 }
363 Some(p)
364 }
365 NodeKind::Polygon { sides, .. } => {
366 let n = sides.get().round().max(3.0) as usize;
367 Some(regular(n, size.0.max(0.0) / 2.0, None))
368 }
369 NodeKind::Star { points, inner, .. } => {
370 let n = points.get().round().max(2.0) as usize;
371 let r = size.0.max(0.0) / 2.0;
372 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
373 }
374 NodeKind::Path { data } => Some(data.get()),
375 NodeKind::Line {
376 points,
377 closed,
378 smooth,
379 ..
380 } => {
381 let pts = points.get();
382 if *smooth && pts.len() > 2 {
383 return Some(catmull_rom(&pts, *closed));
384 }
385 let mut p = BezPath::new();
386 for (i, v) in pts.iter().enumerate() {
387 if i == 0 {
388 p.move_to(v.to_point());
389 } else {
390 p.line_to(v.to_point());
391 }
392 }
393 if *closed && pts.len() > 2 {
394 p.close_path();
395 }
396 Some(p)
397 }
398 }
399 }
400}
401
402fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
404 let count = if inner.is_some() { 2 * n } else { n };
405 let mut p = BezPath::new();
406 for i in 0..count {
407 let radius = match inner {
408 Some(ri) if i % 2 == 1 => ri,
409 _ => r,
410 };
411 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
412 let pt = Point::new(a.cos() * radius, a.sin() * radius);
413 if i == 0 {
414 p.move_to(pt);
415 } else {
416 p.line_to(pt);
417 }
418 }
419 p.close_path();
420 p
421}
422
423fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
425 let n = pts.len();
426 let at = |i: isize| -> Vec2 {
427 if closed {
428 pts[i.rem_euclid(n as isize) as usize]
429 } else {
430 pts[i.clamp(0, n as isize - 1) as usize]
431 }
432 };
433 let mut p = BezPath::new();
434 p.move_to(pts[0].to_point());
435 let segs = if closed { n } else { n - 1 };
436 for i in 0..segs as isize {
437 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
438 let c1 = p1 + (p2 - p0) / 6.0;
439 let c2 = p2 - (p3 - p1) / 6.0;
440 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
441 }
442 if closed {
443 p.close_path();
444 }
445 p
446}
447
448pub(crate) enum TextDraw {
449 Styled(Styled),
450 Morph {
452 glyphs: Vec<(BezPath, f64)>,
453 size: (f64, f64),
454 },
455}
456
457pub struct SceneGraph {
459 nodes: RefCell<Vec<NodeData>>,
460 size: (u32, u32),
461}
462
463impl SceneGraph {
464 pub const ROOT: NodeId = NodeId(0);
466
467 pub fn new(width: u32, height: u32) -> Rc<Self> {
469 let graph = Rc::new(Self {
470 nodes: RefCell::new(Vec::new()),
471 size: (width, height),
472 });
473 graph.insert(
474 None,
475 NodeKind::Group,
476 crate::BaseProps {
477 name: Some("view".into()),
478 ..Default::default()
479 },
480 );
481 graph
482 }
483
484 pub fn size(&self) -> (u32, u32) {
486 self.size
487 }
488
489 pub fn len(&self) -> usize {
491 self.nodes.borrow().len()
492 }
493
494 pub fn is_empty(&self) -> bool {
496 self.len() == 0
497 }
498
499 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
505 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
506 }
507
508 pub(crate) fn next_id(&self) -> u32 {
509 self.len() as u32
510 }
511
512 pub(crate) fn insert(
513 &self,
514 parent: Option<NodeId>,
515 kind: NodeKind,
516 base: crate::BaseProps,
517 ) -> NodeId {
518 let mut nodes = self.nodes.borrow_mut();
519 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
520 let label = format!("{}#{}", kind.name(), id.0);
521 let sig = |field: &str| format!("{label}.{field}");
522 nodes.push(NodeData {
523 id,
524 name: base.name,
525 source: current_add_at().map(|(file, line)| source_label(file, line)),
526 parent,
527 children: Vec::new(),
528 x: Signal::new(base.x).with_name(sig("x")),
529 y: Signal::new(base.y).with_name(sig("y")),
530 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
531 scale: Signal::new(base.scale).with_name(sig("scale")),
532 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
533 fill: Signal::new(base.fill).with_name(sig("fill")),
534 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
535 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
536 start: Signal::new(base.start).with_name(sig("start")),
537 end: Signal::new(base.end).with_name(sig("end")),
538 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
539 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
540 dash: Signal::new(base.dash).with_name(sig("dash")),
541 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
542 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
543 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
544 blur: Signal::new(base.blur).with_name(sig("blur")),
545 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
546 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
547 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
548 clip: Signal::new(base.clip).with_name(sig("clip")),
549 blend: Signal::new(base.blend).with_name(sig("blend")),
550 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
551 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
552 grow: Signal::new(base.grow).with_name(sig("grow")),
553 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
554 basis: Signal::new(base.basis).with_name(sig("basis")),
555 margin: Signal::new(base.margin).with_name(sig("margin")),
556 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
557 place: base.place,
558 placed: Cell::new((base.x, base.y)),
559 kind,
560 });
561 if let Some(p) = parent {
562 nodes[p.0 as usize].children.push(id);
563 }
564 id
565 }
566
567 pub(crate) fn detach(&self, id: NodeId) {
568 let mut nodes = self.nodes.borrow_mut();
569 if let Some(p) = nodes[id.0 as usize].parent.take() {
570 nodes[p.0 as usize].children.retain(|c| *c != id);
571 }
572 }
573
574 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
575 self.detach(id);
576 let mut nodes = self.nodes.borrow_mut();
577 nodes[id.0 as usize].parent = Some(parent);
578 nodes[parent.0 as usize].children.push(id);
579 }
580
581 pub fn measure(&self, id: NodeId) -> (f64, f64) {
583 self.resolve_places();
584 let nodes = self.nodes.borrow();
585 measure(&nodes, id)
586 }
587
588 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
589 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
590 }
591
592 fn resolve_places(&self) {
593 let nodes = self.nodes.borrow();
594 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
595 }
596
597 pub fn draw_list(&self) -> Vec<DrawItem> {
599 self.resolve_places();
600 let nodes = self.nodes.borrow();
601 let mut out = Vec::new();
602 visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
603 out
604 }
605
606 pub fn pick(&self, p: Point) -> Option<NodeId> {
608 self.draw_list()
609 .iter()
610 .rev()
611 .find(|item| item.node != Self::ROOT && item.hit(p))
612 .map(|item| item.node)
613 }
614
615 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
617 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
620 return Some(item.transform);
621 }
622 let nodes = self.nodes.borrow();
623 let mut tf = Affine::IDENTITY;
624 let mut cur = Some(id);
625 while let Some(c) = cur {
626 let n = nodes.get(c.0 as usize)?;
627 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
628 cur = n.parent;
629 }
630 Some(tf)
631 }
632
633 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
637 let node_tf = self.world_transform(id)?;
638 let frame_tf = self.world_transform(frame)?;
639 let (w, h) = measure(&self.nodes.borrow(), id);
640 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
641 Some(kurbo::Rect::new(x0, y0, x1, y1))
642 }
643
644 pub fn frame_size(&self) -> (f64, f64) {
646 (f64::from(self.size.0), f64::from(self.size.1))
647 }
648
649 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
651 let nodes = self.nodes.borrow();
652 let mut out = Vec::new();
653 let mut stack = vec![Self::ROOT];
654 while let Some(id) = stack.pop() {
655 let n = &nodes[id.0 as usize];
656 out.push((id, n.parent, n.kind.name(), n.name.clone()));
657 stack.extend(n.children.iter().rev());
658 }
659 out
660 }
661
662 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
664 let nodes = self.nodes.borrow();
665 let Some(n) = nodes.get(id.0 as usize) else {
666 return Vec::new();
667 };
668 let f = |v: f64| format!("{v:.3}");
669 let s = n.scale.get();
670 let mut out = vec![
671 ("x".into(), f(n.x.get())),
672 ("y".into(), f(n.y.get())),
673 ("rotation".into(), f(n.rotation.get())),
674 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
675 ("opacity".into(), f(n.opacity.get())),
676 ("fill".into(), n.fill.get().to_string()),
677 ("stroke".into(), n.stroke.get().to_string()),
678 ("line_width".into(), f(n.line_width.get())),
679 ("start".into(), f(n.start.get())),
680 ("end".into(), f(n.end.get())),
681 ];
682 let (w, h) = measure(&nodes, id);
683 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
684 match &n.kind {
685 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
686 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
687 NodeKind::Txt {
688 text,
689 font_size,
690 weight,
691 ..
692 } => {
693 out.push(("text".into(), text.get()));
694 out.push(("font_size".into(), f(font_size.get())));
695 out.push(("weight".into(), f(weight.get())));
696 }
697 NodeKind::Line { points, .. } => {
698 out.push(("points".into(), points.get().len().to_string()))
699 }
700 NodeKind::Layout { gap, padding, .. } => {
701 out.push(("gap".into(), f(gap.get())));
702 out.push(("padding".into(), f(padding.get())));
703 }
704 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
705 _ => {}
706 }
707 out
708 }
709
710 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
715 let list = self.draw_list();
716 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
717 let mut order = 0u32;
718 for item in &list {
719 let slot = &mut drawn[item.node.0 as usize];
720 if slot.is_none() {
721 *slot = Some((item.transform, order));
722 order += 1;
723 }
724 }
725 let nodes = self.nodes.borrow();
726 let mut out = Vec::new();
727 let mut stack = vec![Self::ROOT];
728 while let Some(id) = stack.pop() {
729 let n = &nodes[id.0 as usize];
730 stack.extend(n.children.iter().rev());
731 let (w, h) = measure(&nodes, id);
732 let (tf, order) = drawn[id.0 as usize]
733 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
734 let (text, text_overflow) = text_info(n);
735 out.push(InspectNode {
736 id: id.0,
737 parent: n.parent.map(|p| p.0),
738 kind: n.kind.name(),
739 name: n.name.clone().unwrap_or_else(|| {
740 n.source
741 .clone()
742 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
743 }),
744 source: n.source.clone(),
745 bounds: aabb(tf, w, h),
746 opacity: n.opacity.get().clamp(0.0, 1.0),
747 order,
748 text,
749 fill: n.fill.get(),
750 text_overflow,
751 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
752 in_camera: {
753 let mut up = n.parent;
754 let mut found = false;
755 while let Some(p) = up {
756 let pn = &nodes[p.0 as usize];
757 if matches!(pn.kind, NodeKind::Camera { .. }) {
758 found = true;
759 break;
760 }
761 up = pn.parent;
762 }
763 found
764 },
765 });
766 }
767 out
768 }
769}
770
771#[derive(Debug, Clone, PartialEq)]
773pub struct InspectNode {
774 pub id: u32,
776 pub parent: Option<u32>,
778 pub kind: &'static str,
780 pub name: String,
782 pub source: Option<String>,
784 pub bounds: [f64; 4],
787 pub opacity: f64,
789 pub order: u32,
791 pub text: Option<String>,
793 pub fill: Color,
795 pub text_overflow: f64,
798 pub parent_kind: Option<&'static str>,
800 pub in_camera: bool,
803}
804
805thread_local! {
806 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
807}
808
809pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
810 ADD_AT.with(|c| c.replace(Some((file, line))))
811}
812
813pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
814 ADD_AT.with(|c| c.set(prev));
815}
816
817fn current_add_at() -> Option<(&'static str, u32)> {
818 ADD_AT.with(|c| c.get())
819}
820
821fn source_label(file: &str, line: u32) -> String {
823 let file = file.replace('\\', "/");
824 let rel = file
825 .rfind("/src/")
826 .map_or(file.as_str(), |i| &file[i + 1..]);
827 format!("{rel}:{line}")
828}
829
830fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
831 let pts = [
832 (-w / 2.0, -h / 2.0),
833 (w / 2.0, -h / 2.0),
834 (w / 2.0, h / 2.0),
835 (-w / 2.0, h / 2.0),
836 ];
837 let mut b = [
838 f64::INFINITY,
839 f64::INFINITY,
840 f64::NEG_INFINITY,
841 f64::NEG_INFINITY,
842 ];
843 for (x, y) in pts {
844 let p = tf * Point::new(x, y);
845 b[0] = b[0].min(p.x);
846 b[1] = b[1].min(p.y);
847 b[2] = b[2].max(p.x);
848 b[3] = b[3].max(p.y);
849 }
850 b
851}
852
853fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
854 let mut chain = Vec::new();
855 let mut cur = Some(id);
856 while let Some(c) = cur {
857 chain.push(c);
858 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
859 }
860 let mut tf = Affine::IDENTITY;
861 for c in chain.into_iter().rev() {
862 let Some(n) = nodes.get(c.0 as usize) else {
863 break;
864 };
865 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
866 }
867 tf
868}
869
870fn text_info(n: &NodeData) -> (Option<String>, f64) {
871 let (body, max_width) = match &n.kind {
872 NodeKind::Txt {
873 text, spans, style, ..
874 } => {
875 let spans = spans.get();
876 let body = if spans.is_empty() {
877 text.get()
878 } else {
879 spans.into_iter().map(|s| s.text).collect()
880 };
881 (body, style.max_width)
882 }
883 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
884 _ => return (None, 0.0),
885 };
886 let overflow = match (max_width, n.text_draw()) {
887 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
888 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
889 _ => 0.0,
890 };
891 (Some(body), overflow)
892}
893
894pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
895 let n = &nodes[id.0 as usize];
896 match &n.kind {
897 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
898 NodeKind::Circle { size, .. }
899 | NodeKind::Polygon { size, .. }
900 | NodeKind::Star { size, .. } => (size.get(), size.get()),
901 NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
902 (width.get(), height.get())
903 }
904 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
905 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
906 Some(TextDraw::Morph { size, .. }) => size,
907 None => (0.0, 0.0),
908 },
909 NodeKind::Path { .. } | NodeKind::Line { .. } => {
910 let bb = n
911 .outline((0.0, 0.0))
912 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
913 (bb.width(), bb.height())
914 }
915 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
916 NodeKind::Camera { .. } => (0.0, 0.0),
918 NodeKind::Group => {
919 let mut bb: Option<kurbo::Rect> = None;
920 for &c in &n.children {
921 let child = &nodes[c.0 as usize];
922 let (w, h) = measure(nodes, c);
923 let r = child
924 .local_transform(Vec2::ZERO, (w, h))
925 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
926 bb = Some(bb.map_or(r, |b| b.union(r)));
927 }
928 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
929 }
930 }
931}
932
933fn resizable(kind: &NodeKind) -> bool {
935 matches!(
936 kind,
937 NodeKind::Rect { .. }
938 | NodeKind::Layout { .. }
939 | NodeKind::Img { .. }
940 | NodeKind::Grid { .. }
941 )
942}
943
944fn visit(
945 nodes: &[NodeData],
946 id: NodeId,
947 parent_tf: Affine,
948 placement: Option<FlexBox>,
949 flex: Option<&FlexMap>,
950 out: &mut Vec<DrawItem>,
951) {
952 let n = &nodes[id.0 as usize];
953 let opacity = n.opacity.get().clamp(0.0, 1.0);
954 if opacity <= 0.0 {
955 return;
956 }
957 let size = match placement {
958 Some(b) if resizable(&n.kind) => {
959 let s = n.scale.get();
960 (
961 b.size.0 / s.x.abs().max(1e-9),
962 b.size.1 / s.y.abs().max(1e-9),
963 )
964 }
965 _ => measure(nodes, id),
966 };
967 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
968 let tf = parent_tf * n.local_transform(offset, size);
969
970 let blend = n.blend.get();
973 let blur = n.blur.get().max(0.0);
974 let shadow_color = n.shadow_color.get();
975 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
976 color: shadow_color,
977 blur: n.shadow_blur.get().max(0.0),
978 offset: n.shadow_offset.get(),
979 });
980 let layered = blend != BlendMode::Normal
981 || blur > 0.0
982 || shadow.is_some()
983 || (opacity < 1.0 && !n.children.is_empty());
984 if layered {
985 out.push(DrawItem {
986 node: id,
987 transform: tf,
988 prim: Primitive::PushLayer(Layer {
989 opacity,
990 blend,
991 clip: None,
992 blur,
993 shadow,
994 }),
995 });
996 }
997 let paint_opacity = if layered { 1.0 } else { opacity };
998 for prim in crate::draw::primitives(n, size, paint_opacity) {
999 out.push(DrawItem {
1000 node: id,
1001 transform: tf,
1002 prim,
1003 });
1004 }
1005
1006 if !n.children.is_empty() {
1007 let clip = n.clip.get().then(|| {
1009 n.outline(size).unwrap_or_else(|| {
1010 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1011 .to_path(0.1)
1012 })
1013 });
1014 let clipped = clip.is_some();
1015 if let Some(path) = clip {
1016 out.push(DrawItem {
1017 node: id,
1018 transform: tf,
1019 prim: Primitive::PushLayer(Layer::clip(path)),
1020 });
1021 }
1022 let own;
1025 let map: Option<&FlexMap> = match (&n.kind, flex) {
1026 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1027 (NodeKind::Layout { .. }, None) => {
1028 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1029 Some(&own)
1030 }
1031 _ => None,
1032 };
1033 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1034 order.sort_by(|a, b| {
1036 let za = nodes[a.1.0 as usize].z_index.get();
1037 let zb = nodes[b.1.0 as usize].z_index.get();
1038 za.total_cmp(&zb)
1039 });
1040 for (_, c) in order {
1041 let placement = map.and_then(|m| m.get(&c).copied());
1042 visit(nodes, c, tf, placement, map, out);
1043 }
1044 if clipped {
1045 out.push(DrawItem {
1046 node: id,
1047 transform: tf,
1048 prim: Primitive::PopLayer,
1049 });
1050 }
1051 }
1052 if layered {
1053 out.push(DrawItem {
1054 node: id,
1055 transform: tf,
1056 prim: Primitive::PopLayer,
1057 });
1058 }
1059}
1060
1061impl DrawItem {
1062 fn hit(&self, p: Point) -> bool {
1063 let local = self.transform.inverse() * p;
1064 match &self.prim {
1065 Primitive::Fill { path, .. } => {
1066 (is_text_like(path) && path.bounding_box().contains(local))
1067 || closed(path).contains(local)
1068 }
1069 Primitive::Stroke { path, style, .. } => {
1070 let tol = (style.width / 2.0)
1071 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1072 path.segments().any(|s| {
1073 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1074 })
1075 }
1076 Primitive::Image { rect, .. } => rect.contains(local),
1077 Primitive::PushLayer(_) | Primitive::PopLayer => false,
1078 }
1079 }
1080}
1081
1082fn closed(path: &BezPath) -> BezPath {
1084 let mut out = BezPath::new();
1085 let mut open = false;
1086 for el in path.elements() {
1087 match el {
1088 kurbo::PathEl::MoveTo(_) => {
1089 if open {
1090 out.close_path();
1091 }
1092 open = true;
1093 }
1094 kurbo::PathEl::ClosePath => open = false,
1095 _ => {}
1096 }
1097 out.push(*el);
1098 }
1099 if open {
1100 out.close_path();
1101 }
1102 out
1103}
1104
1105fn is_text_like(path: &BezPath) -> bool {
1108 path.elements()
1109 .iter()
1110 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1111 .count()
1112 > 3
1113}
1114
1115thread_local! {
1116 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1117}
1118
1119impl SceneGraph {
1120 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1122 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1123 struct Restore(Option<Rc<SceneGraph>>);
1124 impl Drop for Restore {
1125 fn drop(&mut self) {
1126 let prev = self.0.take();
1127 CURRENT.with(|c| *c.borrow_mut() = prev);
1128 }
1129 }
1130 let _restore = Restore(prev);
1131 f()
1132 }
1133}
1134
1135pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1136 CURRENT.with(|c| {
1137 let c = c.borrow();
1138 f(c.as_ref()
1139 .expect("oxframe: node handles can only be used while a scene is running"))
1140 })
1141}