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 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 source: Option<String>,
177 pub parent: Option<NodeId>,
178 pub children: Vec<NodeId>,
179 pub kind: NodeKind,
180 pub x: Signal<f64>,
181 pub y: Signal<f64>,
182 pub rotation: Signal<f64>,
184 pub scale: Signal<Vec2>,
185 pub opacity: Signal<f64>,
186 pub fill: Signal<Color>,
187 pub stroke: Signal<Color>,
188 pub line_width: Signal<f64>,
189 pub start: Signal<f64>,
191 pub end: Signal<f64>,
192 pub fill_gradient: Signal<Option<Gradient>>,
194 pub stroke_gradient: Signal<Option<Gradient>>,
195 pub dash: Signal<Vec<f64>>,
197 pub dash_offset: Signal<f64>,
198 pub line_cap: Signal<LineCap>,
199 pub line_join: Signal<LineJoin>,
200 pub blur: Signal<f64>,
202 pub shadow_color: Signal<Color>,
203 pub shadow_blur: Signal<f64>,
204 pub shadow_offset: Signal<Vec2>,
205 pub clip: Signal<bool>,
207 pub blend: Signal<BlendMode>,
208 pub z_index: Signal<f64>,
210 pub anchor: Signal<Vec2>,
214 pub grow: Signal<f64>,
216 pub shrink: Signal<f64>,
217 pub basis: Signal<Option<f64>>,
218 pub margin: Signal<f64>,
219 pub align_self: Signal<Option<LayoutAlign>>,
220 pub place: Option<crate::place::Place>,
222 pub placed: Cell<(f64, f64)>,
224}
225
226impl NodeData {
227 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
230 let s = self.scale.get();
231 let a = self.anchor.get();
232 let (x, y) = self.placed.get();
233 Affine::translate(offset + Vec2::new(x, y))
234 * Affine::rotate(self.rotation.get().to_radians())
235 * Affine::scale_non_uniform(s.x, s.y)
236 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
237 }
238
239 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
241 match &self.kind {
242 NodeKind::Txt {
243 text,
244 spans,
245 font_size,
246 weight,
247 style,
248 morph,
249 morph_t,
250 } => {
251 let style = TextStyle {
252 size: font_size.get().max(0.1),
253 weight: weight.get().clamp(1.0, 1000.0),
254 ..style.clone()
255 };
256 if let Some((from, to)) = morph.get() {
257 let a = oxframe_text::layout(&from, &style);
258 let b = oxframe_text::layout(&to, &style);
259 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
260 return Some(TextDraw::Morph { glyphs, size });
261 }
262 let spans = spans.get();
263 Some(TextDraw::Styled(if spans.is_empty() {
264 crate::text::rich(&[RichSpan::new(text.get())], &style)
265 } else {
266 crate::text::rich(&spans, &style)
267 }))
268 }
269 NodeKind::Code {
270 code,
271 lang,
272 theme,
273 font_size,
274 style,
275 focus,
276 dim,
277 } => {
278 let style = TextStyle {
279 size: font_size.get().max(0.1),
280 ..style.clone()
281 };
282 Some(TextDraw::Styled(crate::text::code(
283 &code.get(),
284 *lang,
285 theme,
286 &style,
287 focus.get(),
288 dim.get(),
289 )))
290 }
291 _ => None,
292 }
293 }
294
295 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
297 const TOLERANCE: f64 = 0.1;
298 match &self.kind {
299 NodeKind::Group
300 | NodeKind::Img { .. }
301 | NodeKind::Txt { .. }
302 | NodeKind::Code { .. }
303 | NodeKind::Grid { .. } => None,
304 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
305 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
306 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
307 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
308 Some(if r > 0.0 {
309 rect.to_rounded_rect(r).to_path(TOLERANCE)
310 } else {
311 rect.to_path(TOLERANCE)
312 })
313 }
314 NodeKind::Circle {
315 start_angle,
316 end_angle,
317 pie,
318 ..
319 } => {
320 let r = size.0.max(0.0) / 2.0;
323 let (a0, a1) = (start_angle.get(), end_angle.get());
324 let sweep = (a1 - a0).clamp(-360.0, 360.0);
325 if sweep.abs() >= 360.0 - 1e-9 {
326 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
327 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
328 }
329 let arc = kurbo::Arc::new(
330 (0.0, 0.0),
331 (r, r),
332 (a0 - 90.0).to_radians(),
333 sweep.to_radians(),
334 0.0,
335 );
336 let mut p = BezPath::new();
337 if *pie {
338 p.move_to((0.0, 0.0));
339 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
340 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
341 p.close_path();
342 } else {
343 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
344 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
345 }
346 Some(p)
347 }
348 NodeKind::Polygon { sides, .. } => {
349 let n = sides.get().round().max(3.0) as usize;
350 Some(regular(n, size.0.max(0.0) / 2.0, None))
351 }
352 NodeKind::Star { points, inner, .. } => {
353 let n = points.get().round().max(2.0) as usize;
354 let r = size.0.max(0.0) / 2.0;
355 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
356 }
357 NodeKind::Path { data } => Some(data.get()),
358 NodeKind::Line {
359 points,
360 closed,
361 smooth,
362 ..
363 } => {
364 let pts = points.get();
365 if *smooth && pts.len() > 2 {
366 return Some(catmull_rom(&pts, *closed));
367 }
368 let mut p = BezPath::new();
369 for (i, v) in pts.iter().enumerate() {
370 if i == 0 {
371 p.move_to(v.to_point());
372 } else {
373 p.line_to(v.to_point());
374 }
375 }
376 if *closed && pts.len() > 2 {
377 p.close_path();
378 }
379 Some(p)
380 }
381 }
382 }
383}
384
385fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
387 let count = if inner.is_some() { 2 * n } else { n };
388 let mut p = BezPath::new();
389 for i in 0..count {
390 let radius = match inner {
391 Some(ri) if i % 2 == 1 => ri,
392 _ => r,
393 };
394 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
395 let pt = Point::new(a.cos() * radius, a.sin() * radius);
396 if i == 0 {
397 p.move_to(pt);
398 } else {
399 p.line_to(pt);
400 }
401 }
402 p.close_path();
403 p
404}
405
406fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
408 let n = pts.len();
409 let at = |i: isize| -> Vec2 {
410 if closed {
411 pts[i.rem_euclid(n as isize) as usize]
412 } else {
413 pts[i.clamp(0, n as isize - 1) as usize]
414 }
415 };
416 let mut p = BezPath::new();
417 p.move_to(pts[0].to_point());
418 let segs = if closed { n } else { n - 1 };
419 for i in 0..segs as isize {
420 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
421 let c1 = p1 + (p2 - p0) / 6.0;
422 let c2 = p2 - (p3 - p1) / 6.0;
423 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
424 }
425 if closed {
426 p.close_path();
427 }
428 p
429}
430
431pub(crate) enum TextDraw {
432 Styled(Styled),
433 Morph {
435 glyphs: Vec<(BezPath, f64)>,
436 size: (f64, f64),
437 },
438}
439
440pub struct SceneGraph {
442 nodes: RefCell<Vec<NodeData>>,
443 size: (u32, u32),
444}
445
446impl SceneGraph {
447 pub const ROOT: NodeId = NodeId(0);
449
450 pub fn new(width: u32, height: u32) -> Rc<Self> {
452 let graph = Rc::new(Self {
453 nodes: RefCell::new(Vec::new()),
454 size: (width, height),
455 });
456 graph.insert(
457 None,
458 NodeKind::Group,
459 crate::BaseProps {
460 name: Some("view".into()),
461 ..Default::default()
462 },
463 );
464 graph
465 }
466
467 pub fn size(&self) -> (u32, u32) {
469 self.size
470 }
471
472 pub fn len(&self) -> usize {
474 self.nodes.borrow().len()
475 }
476
477 pub fn is_empty(&self) -> bool {
479 self.len() == 0
480 }
481
482 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
488 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
489 }
490
491 pub(crate) fn next_id(&self) -> u32 {
492 self.len() as u32
493 }
494
495 pub(crate) fn insert(
496 &self,
497 parent: Option<NodeId>,
498 kind: NodeKind,
499 base: crate::BaseProps,
500 ) -> NodeId {
501 let mut nodes = self.nodes.borrow_mut();
502 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
503 let label = format!("{}#{}", kind.name(), id.0);
504 let sig = |field: &str| format!("{label}.{field}");
505 nodes.push(NodeData {
506 id,
507 name: base.name,
508 source: current_add_at().map(|(file, line)| source_label(file, line)),
509 parent,
510 children: Vec::new(),
511 x: Signal::new(base.x).with_name(sig("x")),
512 y: Signal::new(base.y).with_name(sig("y")),
513 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
514 scale: Signal::new(base.scale).with_name(sig("scale")),
515 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
516 fill: Signal::new(base.fill).with_name(sig("fill")),
517 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
518 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
519 start: Signal::new(base.start).with_name(sig("start")),
520 end: Signal::new(base.end).with_name(sig("end")),
521 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
522 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
523 dash: Signal::new(base.dash).with_name(sig("dash")),
524 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
525 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
526 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
527 blur: Signal::new(base.blur).with_name(sig("blur")),
528 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
529 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
530 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
531 clip: Signal::new(base.clip).with_name(sig("clip")),
532 blend: Signal::new(base.blend).with_name(sig("blend")),
533 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
534 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
535 grow: Signal::new(base.grow).with_name(sig("grow")),
536 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
537 basis: Signal::new(base.basis).with_name(sig("basis")),
538 margin: Signal::new(base.margin).with_name(sig("margin")),
539 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
540 place: base.place,
541 placed: Cell::new((base.x, base.y)),
542 kind,
543 });
544 if let Some(p) = parent {
545 nodes[p.0 as usize].children.push(id);
546 }
547 id
548 }
549
550 pub(crate) fn detach(&self, id: NodeId) {
551 let mut nodes = self.nodes.borrow_mut();
552 if let Some(p) = nodes[id.0 as usize].parent.take() {
553 nodes[p.0 as usize].children.retain(|c| *c != id);
554 }
555 }
556
557 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
558 self.detach(id);
559 let mut nodes = self.nodes.borrow_mut();
560 nodes[id.0 as usize].parent = Some(parent);
561 nodes[parent.0 as usize].children.push(id);
562 }
563
564 pub fn measure(&self, id: NodeId) -> (f64, f64) {
566 self.resolve_places();
567 let nodes = self.nodes.borrow();
568 measure(&nodes, id)
569 }
570
571 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
572 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
573 }
574
575 fn resolve_places(&self) {
576 let nodes = self.nodes.borrow();
577 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
578 }
579
580 pub fn draw_list(&self) -> Vec<DrawItem> {
582 self.resolve_places();
583 let nodes = self.nodes.borrow();
584 let mut out = Vec::new();
585 visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
586 out
587 }
588
589 pub fn pick(&self, p: Point) -> Option<NodeId> {
591 self.draw_list()
592 .iter()
593 .rev()
594 .find(|item| item.node != Self::ROOT && item.hit(p))
595 .map(|item| item.node)
596 }
597
598 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
600 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
603 return Some(item.transform);
604 }
605 let nodes = self.nodes.borrow();
606 let mut tf = Affine::IDENTITY;
607 let mut cur = Some(id);
608 while let Some(c) = cur {
609 let n = nodes.get(c.0 as usize)?;
610 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
611 cur = n.parent;
612 }
613 Some(tf)
614 }
615
616 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
618 let nodes = self.nodes.borrow();
619 let mut out = Vec::new();
620 let mut stack = vec![Self::ROOT];
621 while let Some(id) = stack.pop() {
622 let n = &nodes[id.0 as usize];
623 out.push((id, n.parent, n.kind.name(), n.name.clone()));
624 stack.extend(n.children.iter().rev());
625 }
626 out
627 }
628
629 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
631 let nodes = self.nodes.borrow();
632 let Some(n) = nodes.get(id.0 as usize) else {
633 return Vec::new();
634 };
635 let f = |v: f64| format!("{v:.3}");
636 let s = n.scale.get();
637 let mut out = vec![
638 ("x".into(), f(n.x.get())),
639 ("y".into(), f(n.y.get())),
640 ("rotation".into(), f(n.rotation.get())),
641 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
642 ("opacity".into(), f(n.opacity.get())),
643 ("fill".into(), n.fill.get().to_string()),
644 ("stroke".into(), n.stroke.get().to_string()),
645 ("line_width".into(), f(n.line_width.get())),
646 ("start".into(), f(n.start.get())),
647 ("end".into(), f(n.end.get())),
648 ];
649 let (w, h) = measure(&nodes, id);
650 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
651 match &n.kind {
652 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
653 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
654 NodeKind::Txt {
655 text,
656 font_size,
657 weight,
658 ..
659 } => {
660 out.push(("text".into(), text.get()));
661 out.push(("font_size".into(), f(font_size.get())));
662 out.push(("weight".into(), f(weight.get())));
663 }
664 NodeKind::Line { points, .. } => {
665 out.push(("points".into(), points.get().len().to_string()))
666 }
667 NodeKind::Layout { gap, padding, .. } => {
668 out.push(("gap".into(), f(gap.get())));
669 out.push(("padding".into(), f(padding.get())));
670 }
671 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
672 _ => {}
673 }
674 out
675 }
676
677 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
682 let list = self.draw_list();
683 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
684 let mut order = 0u32;
685 for item in &list {
686 let slot = &mut drawn[item.node.0 as usize];
687 if slot.is_none() {
688 *slot = Some((item.transform, order));
689 order += 1;
690 }
691 }
692 let nodes = self.nodes.borrow();
693 let mut out = Vec::new();
694 let mut stack = vec![Self::ROOT];
695 while let Some(id) = stack.pop() {
696 let n = &nodes[id.0 as usize];
697 stack.extend(n.children.iter().rev());
698 let (w, h) = measure(&nodes, id);
699 let (tf, order) = drawn[id.0 as usize]
700 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
701 let (text, text_overflow) = text_info(n);
702 out.push(InspectNode {
703 id: id.0,
704 parent: n.parent.map(|p| p.0),
705 kind: n.kind.name(),
706 name: n.name.clone().unwrap_or_else(|| {
707 n.source
708 .clone()
709 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
710 }),
711 source: n.source.clone(),
712 bounds: aabb(tf, w, h),
713 opacity: n.opacity.get().clamp(0.0, 1.0),
714 order,
715 text,
716 fill: n.fill.get(),
717 text_overflow,
718 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
719 });
720 }
721 out
722 }
723}
724
725#[derive(Debug, Clone, PartialEq)]
727pub struct InspectNode {
728 pub id: u32,
730 pub parent: Option<u32>,
732 pub kind: &'static str,
734 pub name: String,
736 pub source: Option<String>,
738 pub bounds: [f64; 4],
741 pub opacity: f64,
743 pub order: u32,
745 pub text: Option<String>,
747 pub fill: Color,
749 pub text_overflow: f64,
752 pub parent_kind: Option<&'static str>,
754}
755
756thread_local! {
757 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
758}
759
760pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
761 ADD_AT.with(|c| c.replace(Some((file, line))))
762}
763
764pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
765 ADD_AT.with(|c| c.set(prev));
766}
767
768fn current_add_at() -> Option<(&'static str, u32)> {
769 ADD_AT.with(|c| c.get())
770}
771
772fn source_label(file: &str, line: u32) -> String {
774 let file = file.replace('\\', "/");
775 let rel = file
776 .rfind("/src/")
777 .map_or(file.as_str(), |i| &file[i + 1..]);
778 format!("{rel}:{line}")
779}
780
781fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
782 let pts = [
783 (-w / 2.0, -h / 2.0),
784 (w / 2.0, -h / 2.0),
785 (w / 2.0, h / 2.0),
786 (-w / 2.0, h / 2.0),
787 ];
788 let mut b = [
789 f64::INFINITY,
790 f64::INFINITY,
791 f64::NEG_INFINITY,
792 f64::NEG_INFINITY,
793 ];
794 for (x, y) in pts {
795 let p = tf * Point::new(x, y);
796 b[0] = b[0].min(p.x);
797 b[1] = b[1].min(p.y);
798 b[2] = b[2].max(p.x);
799 b[3] = b[3].max(p.y);
800 }
801 b
802}
803
804fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
805 let mut chain = Vec::new();
806 let mut cur = Some(id);
807 while let Some(c) = cur {
808 chain.push(c);
809 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
810 }
811 let mut tf = Affine::IDENTITY;
812 for c in chain.into_iter().rev() {
813 let Some(n) = nodes.get(c.0 as usize) else {
814 break;
815 };
816 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
817 }
818 tf
819}
820
821fn text_info(n: &NodeData) -> (Option<String>, f64) {
822 let (body, max_width) = match &n.kind {
823 NodeKind::Txt {
824 text, spans, style, ..
825 } => {
826 let spans = spans.get();
827 let body = if spans.is_empty() {
828 text.get()
829 } else {
830 spans.into_iter().map(|s| s.text).collect()
831 };
832 (body, style.max_width)
833 }
834 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
835 _ => return (None, 0.0),
836 };
837 let overflow = match (max_width, n.text_draw()) {
838 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
839 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
840 _ => 0.0,
841 };
842 (Some(body), overflow)
843}
844
845pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
846 let n = &nodes[id.0 as usize];
847 match &n.kind {
848 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
849 NodeKind::Circle { size, .. }
850 | NodeKind::Polygon { size, .. }
851 | NodeKind::Star { size, .. } => (size.get(), size.get()),
852 NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
853 (width.get(), height.get())
854 }
855 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
856 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
857 Some(TextDraw::Morph { size, .. }) => size,
858 None => (0.0, 0.0),
859 },
860 NodeKind::Path { .. } | NodeKind::Line { .. } => {
861 let bb = n
862 .outline((0.0, 0.0))
863 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
864 (bb.width(), bb.height())
865 }
866 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
867 NodeKind::Group => {
868 let mut bb: Option<kurbo::Rect> = None;
869 for &c in &n.children {
870 let child = &nodes[c.0 as usize];
871 let (w, h) = measure(nodes, c);
872 let r = child
873 .local_transform(Vec2::ZERO, (w, h))
874 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
875 bb = Some(bb.map_or(r, |b| b.union(r)));
876 }
877 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
878 }
879 }
880}
881
882fn resizable(kind: &NodeKind) -> bool {
884 matches!(
885 kind,
886 NodeKind::Rect { .. }
887 | NodeKind::Layout { .. }
888 | NodeKind::Img { .. }
889 | NodeKind::Grid { .. }
890 )
891}
892
893fn visit(
894 nodes: &[NodeData],
895 id: NodeId,
896 parent_tf: Affine,
897 placement: Option<FlexBox>,
898 flex: Option<&FlexMap>,
899 out: &mut Vec<DrawItem>,
900) {
901 let n = &nodes[id.0 as usize];
902 let opacity = n.opacity.get().clamp(0.0, 1.0);
903 if opacity <= 0.0 {
904 return;
905 }
906 let size = match placement {
907 Some(b) if resizable(&n.kind) => {
908 let s = n.scale.get();
909 (
910 b.size.0 / s.x.abs().max(1e-9),
911 b.size.1 / s.y.abs().max(1e-9),
912 )
913 }
914 _ => measure(nodes, id),
915 };
916 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
917 let tf = parent_tf * n.local_transform(offset, size);
918
919 let blend = n.blend.get();
922 let blur = n.blur.get().max(0.0);
923 let shadow_color = n.shadow_color.get();
924 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
925 color: shadow_color,
926 blur: n.shadow_blur.get().max(0.0),
927 offset: n.shadow_offset.get(),
928 });
929 let layered = blend != BlendMode::Normal
930 || blur > 0.0
931 || shadow.is_some()
932 || (opacity < 1.0 && !n.children.is_empty());
933 if layered {
934 out.push(DrawItem {
935 node: id,
936 transform: tf,
937 prim: Primitive::PushLayer(Layer {
938 opacity,
939 blend,
940 clip: None,
941 blur,
942 shadow,
943 }),
944 });
945 }
946 let paint_opacity = if layered { 1.0 } else { opacity };
947 for prim in crate::draw::primitives(n, size, paint_opacity) {
948 out.push(DrawItem {
949 node: id,
950 transform: tf,
951 prim,
952 });
953 }
954
955 if !n.children.is_empty() {
956 let clip = n.clip.get().then(|| {
958 n.outline(size).unwrap_or_else(|| {
959 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
960 .to_path(0.1)
961 })
962 });
963 let clipped = clip.is_some();
964 if let Some(path) = clip {
965 out.push(DrawItem {
966 node: id,
967 transform: tf,
968 prim: Primitive::PushLayer(Layer::clip(path)),
969 });
970 }
971 let own;
974 let map: Option<&FlexMap> = match (&n.kind, flex) {
975 (NodeKind::Layout { .. }, Some(m)) => Some(m),
976 (NodeKind::Layout { .. }, None) => {
977 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
978 Some(&own)
979 }
980 _ => None,
981 };
982 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
983 order.sort_by(|a, b| {
985 let za = nodes[a.1.0 as usize].z_index.get();
986 let zb = nodes[b.1.0 as usize].z_index.get();
987 za.total_cmp(&zb)
988 });
989 for (_, c) in order {
990 let placement = map.and_then(|m| m.get(&c).copied());
991 visit(nodes, c, tf, placement, map, out);
992 }
993 if clipped {
994 out.push(DrawItem {
995 node: id,
996 transform: tf,
997 prim: Primitive::PopLayer,
998 });
999 }
1000 }
1001 if layered {
1002 out.push(DrawItem {
1003 node: id,
1004 transform: tf,
1005 prim: Primitive::PopLayer,
1006 });
1007 }
1008}
1009
1010impl DrawItem {
1011 fn hit(&self, p: Point) -> bool {
1012 let local = self.transform.inverse() * p;
1013 match &self.prim {
1014 Primitive::Fill { path, .. } => {
1015 (is_text_like(path) && path.bounding_box().contains(local))
1016 || closed(path).contains(local)
1017 }
1018 Primitive::Stroke { path, style, .. } => {
1019 let tol = (style.width / 2.0)
1020 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1021 path.segments().any(|s| {
1022 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1023 })
1024 }
1025 Primitive::Image { rect, .. } => rect.contains(local),
1026 Primitive::PushLayer(_) | Primitive::PopLayer => false,
1027 }
1028 }
1029}
1030
1031fn closed(path: &BezPath) -> BezPath {
1033 let mut out = BezPath::new();
1034 let mut open = false;
1035 for el in path.elements() {
1036 match el {
1037 kurbo::PathEl::MoveTo(_) => {
1038 if open {
1039 out.close_path();
1040 }
1041 open = true;
1042 }
1043 kurbo::PathEl::ClosePath => open = false,
1044 _ => {}
1045 }
1046 out.push(*el);
1047 }
1048 if open {
1049 out.close_path();
1050 }
1051 out
1052}
1053
1054fn is_text_like(path: &BezPath) -> bool {
1057 path.elements()
1058 .iter()
1059 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1060 .count()
1061 > 3
1062}
1063
1064thread_local! {
1065 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1066}
1067
1068impl SceneGraph {
1069 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1071 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1072 struct Restore(Option<Rc<SceneGraph>>);
1073 impl Drop for Restore {
1074 fn drop(&mut self) {
1075 let prev = self.0.take();
1076 CURRENT.with(|c| *c.borrow_mut() = prev);
1077 }
1078 }
1079 let _restore = Restore(prev);
1080 f()
1081 }
1082}
1083
1084pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1085 CURRENT.with(|c| {
1086 let c = c.borrow();
1087 f(c.as_ref()
1088 .expect("oxframe: node handles can only be used while a scene is running"))
1089 })
1090}