1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::rc::Rc;
4use std::time::{Duration, Instant};
5
6use thiserror::Error;
7
8use crate::{ComponentInstancePath, NodeId, UiValue};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
11pub struct GeometryBounds {
12 pub x: f64,
13 pub y: f64,
14 pub width: f64,
15 pub height: f64,
16}
17
18impl GeometryBounds {
19 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Result<Self, GeometryError> {
26 if [x, y, width, height].into_iter().all(f64::is_finite) && width >= 0.0 && height >= 0.0 {
27 Ok(Self {
28 x,
29 y,
30 width,
31 height,
32 })
33 } else {
34 Err(GeometryError::InvalidBounds {
35 x,
36 y,
37 width,
38 height,
39 })
40 }
41 }
42
43 #[must_use]
44 pub fn into_value(self) -> UiValue {
45 UiValue::Map(BTreeMap::from([
46 ("x".to_owned(), UiValue::Float(self.x)),
47 ("y".to_owned(), UiValue::Float(self.y)),
48 ("width".to_owned(), UiValue::Float(self.width)),
49 ("height".to_owned(), UiValue::Float(self.height)),
50 ]))
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct Affine2D {
60 m11: f64,
61 m12: f64,
62 m21: f64,
63 m22: f64,
64 tx: f64,
65 ty: f64,
66}
67
68impl Default for Affine2D {
69 fn default() -> Self {
70 Self::IDENTITY
71 }
72}
73
74impl Affine2D {
75 pub const IDENTITY: Self = Self {
76 m11: 1.0,
77 m12: 0.0,
78 m21: 0.0,
79 m22: 1.0,
80 tx: 0.0,
81 ty: 0.0,
82 };
83
84 pub fn new(
90 m11: f64,
91 m12: f64,
92 m21: f64,
93 m22: f64,
94 tx: f64,
95 ty: f64,
96 ) -> Result<Self, GeometryError> {
97 let transform = Self {
98 m11,
99 m12,
100 m21,
101 m22,
102 tx,
103 ty,
104 };
105 if transform.is_finite() {
106 Ok(transform)
107 } else {
108 Err(GeometryError::InvalidTransform)
109 }
110 }
111
112 pub fn translation(x: f64, y: f64) -> Result<Self, GeometryError> {
118 Self::new(1.0, 0.0, 0.0, 1.0, x, y)
119 }
120
121 pub fn scale(x: f64, y: f64) -> Result<Self, GeometryError> {
127 Self::new(x, 0.0, 0.0, y, 0.0, 0.0)
128 }
129
130 pub fn rotation_degrees(degrees: f64) -> Result<Self, GeometryError> {
136 if !degrees.is_finite() {
137 return Err(GeometryError::InvalidTransform);
138 }
139 let radians = degrees.to_radians();
140 let (sin, cos) = radians.sin_cos();
141 Self::new(cos, sin, -sin, cos, 0.0, 0.0)
142 }
143
144 pub fn skew_degrees(x: f64, y: f64) -> Result<Self, GeometryError> {
150 if !x.is_finite() || !y.is_finite() {
151 return Err(GeometryError::InvalidTransform);
152 }
153 Self::new(
154 1.0,
155 y.to_radians().tan(),
156 x.to_radians().tan(),
157 1.0,
158 0.0,
159 0.0,
160 )
161 }
162
163 pub fn then(self, next: Self) -> Result<Self, GeometryError> {
169 Self::new(
170 next.m11.mul_add(self.m11, next.m21 * self.m12),
171 next.m12.mul_add(self.m11, next.m22 * self.m12),
172 next.m11.mul_add(self.m21, next.m21 * self.m22),
173 next.m12.mul_add(self.m21, next.m22 * self.m22),
174 next.m11
175 .mul_add(self.tx, next.m21.mul_add(self.ty, next.tx)),
176 next.m12
177 .mul_add(self.tx, next.m22.mul_add(self.ty, next.ty)),
178 )
179 }
180
181 pub fn around(self, origin: (f64, f64)) -> Result<Self, GeometryError> {
187 Self::translation(-origin.0, -origin.1)?
188 .then(self)?
189 .then(Self::translation(origin.0, origin.1)?)
190 }
191
192 #[must_use]
193 pub fn components(self) -> (f64, f64, f64, f64, f64, f64) {
194 (self.m11, self.m12, self.m21, self.m22, self.tx, self.ty)
195 }
196
197 #[must_use]
198 pub fn map_point(self, point: (f64, f64)) -> (f64, f64) {
199 (
200 self.m11
201 .mul_add(point.0, self.m21.mul_add(point.1, self.tx)),
202 self.m12
203 .mul_add(point.0, self.m22.mul_add(point.1, self.ty)),
204 )
205 }
206
207 #[must_use]
208 pub fn inverse(self) -> Option<Self> {
209 let scale = self
210 .m11
211 .abs()
212 .max(self.m12.abs())
213 .max(self.m21.abs())
214 .max(self.m22.abs());
215 if scale == 0.0 || !scale.is_finite() {
216 return None;
217 }
218 let (a, b, c, d) = (
219 self.m11 / scale,
220 self.m12 / scale,
221 self.m21 / scale,
222 self.m22 / scale,
223 );
224 let determinant = a.mul_add(d, -(b * c));
225 if !determinant.is_finite() || determinant.abs() <= f64::EPSILON {
226 return None;
227 }
228 let m11 = (d / determinant) / scale;
229 let m12 = (-b / determinant) / scale;
230 let m21 = (-c / determinant) / scale;
231 let m22 = (a / determinant) / scale;
232 Self::new(
233 m11,
234 m12,
235 m21,
236 m22,
237 -(m11.mul_add(self.tx, m21 * self.ty)),
238 -(m12.mul_add(self.tx, m22 * self.ty)),
239 )
240 .ok()
241 }
242
243 #[must_use]
244 pub fn transform_bounds(self, bounds: GeometryBounds) -> GeometryBounds {
245 let points = [
246 self.map_point((bounds.x, bounds.y)),
247 self.map_point((bounds.x + bounds.width, bounds.y)),
248 self.map_point((bounds.x, bounds.y + bounds.height)),
249 self.map_point((bounds.x + bounds.width, bounds.y + bounds.height)),
250 ];
251 let (min_x, max_x) = points
252 .iter()
253 .map(|point| point.0)
254 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), value| {
255 (min.min(value), max.max(value))
256 });
257 let (min_y, max_y) = points
258 .iter()
259 .map(|point| point.1)
260 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), value| {
261 (min.min(value), max.max(value))
262 });
263 GeometryBounds {
264 x: min_x,
265 y: min_y,
266 width: max_x - min_x,
267 height: max_y - min_y,
268 }
269 }
270
271 #[must_use]
272 pub fn is_finite(self) -> bool {
273 [self.m11, self.m12, self.m21, self.m22, self.tx, self.ty]
274 .into_iter()
275 .all(f64::is_finite)
276 }
277}
278
279#[derive(Clone, Copy, Debug, PartialEq)]
280pub struct PresentedGeometry {
281 pub local: GeometryBounds,
282 pub layout: GeometryBounds,
283 pub visual: GeometryBounds,
284 pub local_to_window: Affine2D,
285 pub window_to_local: Option<Affine2D>,
286 pub clip: Option<GeometryBounds>,
287}
288
289impl PresentedGeometry {
290 pub fn from_element(geometry: ElementGeometry) -> Result<Self, GeometryError> {
297 let scale_x = if geometry.layout.width.abs() <= f64::EPSILON {
298 1.0
299 } else {
300 geometry.visual.width / geometry.layout.width
301 };
302 let scale_y = if geometry.layout.height.abs() <= f64::EPSILON {
303 1.0
304 } else {
305 geometry.visual.height / geometry.layout.height
306 };
307 let local_to_window = Affine2D::scale(scale_x, scale_y)?
308 .then(Affine2D::translation(geometry.visual.x, geometry.visual.y)?)?;
309 Ok(Self {
310 local: GeometryBounds {
311 x: 0.0,
312 y: 0.0,
313 width: geometry.layout.width,
314 height: geometry.layout.height,
315 },
316 layout: geometry.layout,
317 visual: geometry.visual,
318 local_to_window,
319 window_to_local: local_to_window.inverse(),
320 clip: geometry.clip,
321 })
322 }
323}
324
325#[derive(Clone, Copy, Debug, Default, PartialEq)]
326pub struct ElementGeometry {
327 pub layout: GeometryBounds,
328 pub visual: GeometryBounds,
329 pub clip: Option<GeometryBounds>,
330}
331
332#[derive(Clone, Debug, Default)]
333struct GeometryState {
334 committed: BTreeMap<NodeId, ElementGeometry>,
335 presented: BTreeSet<NodeId>,
336 readers: BTreeMap<NodeId, BTreeSet<crate::read_dependency::ReadDependency>>,
337 ref_readers: crate::element_ref::ElementRefGeometryBindings,
340 dirty: BTreeSet<ComponentInstancePath>,
341 layout_motion: BTreeMap<NodeId, LayoutMotionState>,
342 shared_layout: BTreeMap<(String, String), (NodeId, GeometryBounds)>,
343 motion_progress: BTreeMap<(NodeId, crate::MotionProperty), f64>,
344 motion_trigger_targets: BTreeMap<(NodeId, crate::MotionProgressDriver), bool>,
345 motion_triggers: BTreeMap<(NodeId, crate::MotionProgressDriver), TriggerMotionState>,
346 canvas_transforms: BTreeMap<NodeId, CanvasMotionTransform>,
347 canvas_drawables: BTreeMap<NodeId, ElementGeometry>,
348 element_offsets: BTreeMap<NodeId, (f64, f64)>,
349}
350
351#[derive(Clone, Debug)]
352struct LayoutMotionState {
353 from: GeometryBounds,
354 to: GeometryBounds,
355 started: Instant,
356 duration: Duration,
357 easing: crate::MotionEasing,
358}
359
360#[derive(Clone, Debug)]
361struct TriggerMotionState {
362 from: f64,
363 to: f64,
364 started: Instant,
365 duration: Duration,
366}
367
368#[derive(Clone, Copy, Debug, PartialEq)]
369pub(crate) struct LayoutMotionSample {
370 pub offset_x: f64,
371 pub offset_y: f64,
372 pub scale_x: f64,
373 pub scale_y: f64,
374 pub active: bool,
375}
376
377impl Default for LayoutMotionSample {
378 fn default() -> Self {
379 Self {
380 offset_x: 0.0,
381 offset_y: 0.0,
382 scale_x: 1.0,
383 scale_y: 1.0,
384 active: false,
385 }
386 }
387}
388
389#[derive(Clone, Copy, Debug, Default, PartialEq)]
390pub(crate) struct TriggerMotionSample {
391 pub value: f64,
392 pub active: bool,
393}
394
395#[derive(Clone, Copy)]
396pub(crate) struct LayoutMotionRequest<'a> {
397 pub shared: Option<(&'a str, &'a str)>,
398 pub duration: Duration,
399 pub easing: crate::MotionEasing,
400 pub preference: crate::MotionPreference,
401 pub now: Instant,
402}
403
404#[derive(Clone, Copy, Debug, PartialEq)]
405pub(crate) struct CanvasMotionTransform {
406 pub rotate: f64,
407 pub scale_x: f64,
408 pub scale_y: f64,
409 pub skew_x: f64,
410 pub skew_y: f64,
411 pub path_progress: Option<f64>,
412}
413
414impl Default for CanvasMotionTransform {
415 fn default() -> Self {
416 Self {
417 rotate: 0.0,
418 scale_x: 1.0,
419 scale_y: 1.0,
420 skew_x: 0.0,
421 skew_y: 0.0,
422 path_progress: None,
423 }
424 }
425}
426
427#[derive(Clone, Debug, Default)]
428pub struct GeometryRegistry {
429 inner: Rc<RefCell<Rc<GeometryState>>>,
430}
431
432impl GeometryRegistry {
433 #[must_use]
434 pub fn new() -> Self {
435 Self::default()
436 }
437
438 #[must_use]
439 pub fn len(&self) -> usize {
440 self.inner.borrow().committed.len()
441 }
442
443 #[must_use]
444 pub fn is_empty(&self) -> bool {
445 self.inner.borrow().committed.is_empty()
446 }
447
448 pub fn update(&self, node: NodeId, geometry: ElementGeometry) -> bool {
449 let mut current = self.inner.borrow_mut();
450 let state = Rc::make_mut(&mut current);
451 state.presented.insert(node);
452 if state.committed.get(&node) == Some(&geometry) {
453 return false;
454 }
455 state.committed.insert(node, geometry);
456 let owners = geometry_read_owners(state, node);
457 state.dirty.extend(owners);
458 true
459 }
460
461 pub(crate) fn sample_layout_motion(
462 &self,
463 node: NodeId,
464 layout: GeometryBounds,
465 request: LayoutMotionRequest<'_>,
466 ) -> LayoutMotionSample {
467 let mut current = self.inner.borrow_mut();
468 let state = Rc::make_mut(&mut current);
469 let shared_previous = request.shared.and_then(|(group, id)| {
470 state
471 .shared_layout
472 .get(&(group.to_owned(), id.to_owned()))
473 .copied()
474 .filter(|(previous, _)| *previous != node)
475 .map(|(_, bounds)| bounds)
476 });
477 let previous = state
478 .committed
479 .get(&node)
480 .map(|geometry| geometry.layout)
481 .or(shared_previous);
482 let target_changed = state
483 .layout_motion
484 .get(&node)
485 .is_none_or(|motion| motion.to != layout);
486 if let Some(previous) = previous
487 && previous != layout
488 && target_changed
489 && request.duration > Duration::ZERO
490 {
491 let from = state
492 .layout_motion
493 .get(&node)
494 .map_or(previous, |motion| sample_layout_bounds(motion, request.now));
495 state.layout_motion.insert(
496 node,
497 LayoutMotionState {
498 from,
499 to: layout,
500 started: request.now,
501 duration: request.duration,
502 easing: request.easing,
503 },
504 );
505 }
506 if let Some((group, id)) = request.shared {
507 state
508 .shared_layout
509 .insert((group.to_owned(), id.to_owned()), (node, layout));
510 }
511 if request.preference != crate::MotionPreference::Normal {
512 state.layout_motion.remove(&node);
513 return LayoutMotionSample::default();
514 }
515 let Some(motion) = state.layout_motion.get(&node) else {
516 return LayoutMotionSample::default();
517 };
518 let sampled = sample_layout_bounds(motion, request.now);
519 let done = request.now.saturating_duration_since(motion.started) >= motion.duration;
520 let result = LayoutMotionSample {
521 offset_x: sampled.x - layout.x,
522 offset_y: sampled.y - layout.y,
523 scale_x: 1.0,
527 scale_y: 1.0,
528 active: !done,
529 };
530 if done {
531 state.layout_motion.remove(&node);
532 }
533 result
534 }
535
536 pub(crate) fn update_motion_progress(
537 &self,
538 node: NodeId,
539 property: crate::MotionProperty,
540 value: f64,
541 ) -> bool {
542 let mut current = self.inner.borrow_mut();
543 let state = Rc::make_mut(&mut current);
544 let key = (node, property);
545 if state
546 .motion_progress
547 .get(&key)
548 .is_some_and(|previous| (*previous - value).abs() <= f64::EPSILON)
549 {
550 false
551 } else {
552 state.motion_progress.insert(key, value);
553 true
554 }
555 }
556
557 pub(crate) fn motion_progress(
558 &self,
559 node: NodeId,
560 property: crate::MotionProperty,
561 ) -> Option<f64> {
562 self.inner
563 .borrow()
564 .motion_progress
565 .get(&(node, property))
566 .copied()
567 }
568
569 pub(crate) fn delay_motion(&self, delay: Duration) {
570 if delay.is_zero() {
571 return;
572 }
573 let mut current = self.inner.borrow_mut();
574 let state = Rc::make_mut(&mut current);
575 for motion in state.layout_motion.values_mut() {
576 motion.started = motion.started.checked_add(delay).unwrap_or(motion.started);
577 }
578 for motion in state.motion_triggers.values_mut() {
579 motion.started = motion.started.checked_add(delay).unwrap_or(motion.started);
580 }
581 }
582
583 pub(crate) fn set_motion_trigger(
584 &self,
585 node: NodeId,
586 driver: crate::MotionProgressDriver,
587 active: bool,
588 ) {
589 Rc::make_mut(&mut self.inner.borrow_mut())
590 .motion_trigger_targets
591 .insert((node, driver), active);
592 }
593
594 pub(crate) fn update_canvas_transform(&self, node: NodeId, transform: CanvasMotionTransform) {
595 Rc::make_mut(&mut self.inner.borrow_mut())
596 .canvas_transforms
597 .insert(node, transform);
598 }
599
600 pub(crate) fn canvas_transform(&self, node: NodeId) -> CanvasMotionTransform {
601 self.inner
602 .borrow()
603 .canvas_transforms
604 .get(&node)
605 .copied()
606 .unwrap_or_default()
607 }
608
609 pub(crate) fn record_element_offset(&self, node: NodeId, offset: (f64, f64)) {
610 Rc::make_mut(&mut self.inner.borrow_mut())
611 .element_offsets
612 .insert(node, offset);
613 }
614
615 pub(crate) fn update_canvas_drawable(
616 &self,
617 node: NodeId,
618 visual: GeometryBounds,
619 offset: (f64, f64),
620 ) {
621 let mut current = self.inner.borrow_mut();
622 let state = Rc::make_mut(&mut current);
623 let Some(outer) = state.committed.get(&node) else {
624 return;
625 };
626 let base_offset = state
627 .element_offsets
628 .get(&node)
629 .copied()
630 .unwrap_or_default();
631 let geometry = ElementGeometry {
632 layout: GeometryBounds {
633 x: visual.x - (offset.0 - base_offset.0),
634 y: visual.y - (offset.1 - base_offset.1),
635 ..visual
636 },
637 visual,
638 clip: outer.clip,
639 };
640 if state.canvas_drawables.get(&node) != Some(&geometry) {
641 state.canvas_drawables.insert(node, geometry);
642 let owners = geometry_read_owners(state, node);
643 state.dirty.extend(owners);
644 }
645 }
646
647 pub(crate) fn canvas_drawable(&self, node: NodeId) -> Option<ElementGeometry> {
648 self.inner.borrow().canvas_drawables.get(&node).copied()
649 }
650
651 pub(crate) fn sample_motion_trigger(
652 &self,
653 node: NodeId,
654 binding: &crate::MotionProgressBinding,
655 preference: crate::MotionPreference,
656 now: Instant,
657 ) -> TriggerMotionSample {
658 let mut current = self.inner.borrow_mut();
659 let state = Rc::make_mut(&mut current);
660 let key = (node, binding.driver);
661 let target = if state
662 .motion_trigger_targets
663 .get(&key)
664 .copied()
665 .unwrap_or(false)
666 {
667 1.0
668 } else {
669 0.0
670 };
671 if preference != crate::MotionPreference::Normal {
672 state.motion_triggers.remove(&key);
673 return TriggerMotionSample {
674 value: crate::motion::sample_progress_source(&binding.source, target),
675 active: false,
676 };
677 }
678 let duration = crate::motion::progress_source_duration(&binding.source);
679 let trigger = state
680 .motion_triggers
681 .entry(key)
682 .or_insert(TriggerMotionState {
683 from: 0.0,
684 to: 0.0,
685 started: now,
686 duration,
687 });
688 let sampled = sample_trigger_progress(trigger, now);
689 if (trigger.to - target).abs() > f64::EPSILON {
690 *trigger = TriggerMotionState {
691 from: sampled,
692 to: target,
693 started: now,
694 duration,
695 };
696 } else {
697 trigger.duration = duration;
698 }
699 let progress = sample_trigger_progress(trigger, now);
700 TriggerMotionSample {
701 value: crate::motion::sample_progress_source(&binding.source, progress),
702 active: (progress - trigger.to).abs() > 0.000_1,
703 }
704 }
705
706 pub fn read(
713 &self,
714 node: NodeId,
715 reader: &ComponentInstancePath,
716 ) -> Result<ElementGeometry, GeometryError> {
717 let mut current = self.inner.borrow_mut();
718 let state = Rc::make_mut(&mut current);
719 let geometry = state
720 .committed
721 .get(&node)
722 .copied()
723 .ok_or(GeometryError::Unavailable(node))?;
724 state
725 .readers
726 .entry(node)
727 .or_default()
728 .insert(crate::read_dependency::ReadDependency::component(reader));
729 Ok(geometry)
730 }
731
732 #[cfg(test)]
733 pub(crate) fn read_tracked(
734 &self,
735 node: NodeId,
736 reader: impl Into<crate::read_dependency::ReadDependency>,
737 ) -> Option<ElementGeometry> {
738 let mut current = self.inner.borrow_mut();
739 let state = Rc::make_mut(&mut current);
740 state.readers.entry(node).or_default().insert(reader.into());
741 state.committed.get(&node).copied()
742 }
743
744 pub(crate) fn read_ref_tracked(
745 &self,
746 reference: &crate::ElementRefId,
747 node: NodeId,
748 reader: impl Into<crate::read_dependency::ReadDependency>,
749 ) -> Option<ElementGeometry> {
750 let mut current = self.inner.borrow_mut();
751 let state = Rc::make_mut(&mut current);
752 state
753 .ref_readers
754 .entry(node)
755 .or_default()
756 .entry(reference.clone())
757 .or_default()
758 .insert(reader.into());
759 state.committed.get(&node).copied()
760 }
761
762 pub(crate) fn sync_ref_readers(
765 &self,
766 scope: &ComponentInstancePath,
767 bindings: crate::element_ref::ElementRefGeometryBindings,
768 ) {
769 let mut current = self.inner.borrow_mut();
770 let state = Rc::make_mut(&mut current);
771 let previous = state.ref_readers.clone();
772 for refs in state.ref_readers.values_mut() {
773 refs.retain(|reference, _| !reference.component().is_within(scope));
774 }
775 state.ref_readers.retain(|_, refs| !refs.is_empty());
776 for (node, refs) in bindings {
777 for (reference, readers) in refs {
778 if state.committed.contains_key(&node) {
779 let previous_readers =
780 previous.get(&node).and_then(|refs| refs.get(&reference));
781 state.dirty.extend(
782 readers
783 .iter()
784 .filter(|reader| {
785 previous_readers.is_none_or(|previous| !previous.contains(*reader))
786 })
787 .map(|reader| reader.owner.clone()),
788 );
789 }
790 state
791 .ref_readers
792 .entry(node)
793 .or_default()
794 .insert(reference, readers);
795 }
796 }
797 }
798
799 pub(crate) fn get(&self, node: NodeId) -> Option<ElementGeometry> {
800 self.inner.borrow().committed.get(&node).copied()
801 }
802
803 pub(crate) fn reset_contribution(&self, reader: &crate::read_dependency::ReadDependency) {
804 let mut current = self.inner.borrow_mut();
805 retain_geometry_readers(Rc::make_mut(&mut current), |existing| existing != reader);
806 }
807 pub(crate) fn retain_contributions(
808 &self,
809 scope: &ComponentInstancePath,
810 active: &BTreeSet<crate::read_dependency::ReadContribution>,
811 ) {
812 let mut current = self.inner.borrow_mut();
813 retain_geometry_readers(Rc::make_mut(&mut current), |reader| {
814 reader.retained_in_contribution_scope(scope, active)
815 });
816 }
817
818 pub(crate) fn retain_reader_scope(
819 &self,
820 scope: &ComponentInstancePath,
821 active: &BTreeSet<ComponentInstancePath>,
822 ) {
823 let mut current = self.inner.borrow_mut();
824 let state = Rc::make_mut(&mut current);
825 retain_geometry_readers(state, |reader| {
826 reader.retained_in_owner_scope(scope, active)
827 });
828 for refs in state.ref_readers.values_mut() {
829 refs.retain(|reference, _| {
830 let provider = reference.component();
831 !provider.is_within(scope) || provider == scope || active.contains(provider)
832 });
833 }
834 state.ref_readers.retain(|_, refs| !refs.is_empty());
835 state
836 .dirty
837 .retain(|owner| !owner.is_within(scope) || owner == scope || active.contains(owner));
838 }
839
840 pub(crate) fn remove_scope(&self, scope: &ComponentInstancePath) {
841 let mut current = self.inner.borrow_mut();
842 let state = Rc::make_mut(&mut current);
843 retain_geometry_readers(state, |reader| !reader.owner.is_within(scope));
844 for refs in state.ref_readers.values_mut() {
845 refs.retain(|reference, _| !reference.component().is_within(scope));
846 }
847 state.ref_readers.retain(|_, refs| !refs.is_empty());
848 state.dirty.retain(|owner| !owner.is_within(scope));
849 }
850
851 pub(crate) fn presented(&self, node: NodeId) -> Result<PresentedGeometry, GeometryError> {
852 self.get(node)
853 .ok_or(GeometryError::Unavailable(node))
854 .and_then(PresentedGeometry::from_element)
855 }
856
857 pub(crate) fn begin_frame(&self) {
858 let mut current = self.inner.borrow_mut();
859 let state = Rc::make_mut(&mut current);
860 state
861 .shared_layout
862 .retain(|_, (node, _)| state.presented.contains(node));
863 state.presented.clear();
864 }
865
866 pub(crate) fn finish_frame(&self) {
867 let mut current = self.inner.borrow_mut();
868 let state = Rc::make_mut(&mut current);
869 state
870 .shared_layout
871 .retain(|_, (node, _)| state.presented.contains(node));
872 }
873
874 pub(crate) fn is_presented(&self, node: NodeId) -> bool {
875 self.inner.borrow().presented.contains(&node)
876 }
877
878 pub(crate) fn retain_nodes(&self, active: &BTreeSet<NodeId>) {
879 let mut current = self.inner.borrow_mut();
880 let state = Rc::make_mut(&mut current);
881 state.committed.retain(|node, _| active.contains(node));
882 state.presented.retain(|node| active.contains(node));
883 state.readers.retain(|node, _| active.contains(node));
884 state.ref_readers.retain(|node, _| active.contains(node));
885 state.layout_motion.retain(|node, _| active.contains(node));
886 state
887 .motion_progress
888 .retain(|(node, _), _| active.contains(node));
889 state
890 .motion_trigger_targets
891 .retain(|(node, _), _| active.contains(node));
892 state
893 .motion_triggers
894 .retain(|(node, _), _| active.contains(node));
895 state
896 .canvas_transforms
897 .retain(|node, _| active.contains(node));
898 state
899 .canvas_drawables
900 .retain(|node, _| active.contains(node));
901 state
902 .element_offsets
903 .retain(|node, _| active.contains(node));
904 }
905
906 pub(crate) fn retain_shared_layout_ids(&self, active: &BTreeSet<(String, String)>) {
907 Rc::make_mut(&mut self.inner.borrow_mut())
908 .shared_layout
909 .retain(|identity, _| active.contains(identity));
910 }
911
912 pub(crate) fn take_dirty(&self) -> BTreeSet<ComponentInstancePath> {
913 std::mem::take(&mut Rc::make_mut(&mut self.inner.borrow_mut()).dirty)
914 }
915
916 pub(crate) fn snapshot(&self) -> GeometrySnapshot {
917 GeometrySnapshot(Rc::clone(&self.inner.borrow()))
918 }
919
920 pub(crate) fn restore(&self, snapshot: GeometrySnapshot) {
921 *self.inner.borrow_mut() = snapshot.0;
922 }
923}
924
925fn geometry_read_owners(state: &GeometryState, node: NodeId) -> BTreeSet<ComponentInstancePath> {
926 state
927 .readers
928 .get(&node)
929 .into_iter()
930 .flatten()
931 .chain(
932 state
933 .ref_readers
934 .get(&node)
935 .into_iter()
936 .flat_map(BTreeMap::values)
937 .flatten(),
938 )
939 .map(|reader| reader.owner.clone())
940 .collect()
941}
942
943fn retain_geometry_readers(
944 state: &mut GeometryState,
945 predicate: impl Fn(&crate::read_dependency::ReadDependency) -> bool,
946) {
947 crate::read_dependency::retain_readers(&mut state.readers, &predicate);
948 for refs in state.ref_readers.values_mut() {
949 crate::read_dependency::retain_readers(refs, &predicate);
950 }
951 state.ref_readers.retain(|_, refs| !refs.is_empty());
952}
953
954fn sample_layout_bounds(motion: &LayoutMotionState, now: Instant) -> GeometryBounds {
955 let progress = if motion.duration.is_zero() {
956 1.0
957 } else {
958 (now.saturating_duration_since(motion.started).as_secs_f64()
959 / motion.duration.as_secs_f64())
960 .clamp(0.0, 1.0)
961 };
962 let progress = match motion.easing {
963 crate::MotionEasing::Linear => progress,
964 crate::MotionEasing::EaseIn => progress * progress,
965 crate::MotionEasing::EaseOut => 1.0 - (1.0 - progress).powi(2),
966 crate::MotionEasing::EaseInOut if progress < 0.5 => 2.0 * progress * progress,
967 crate::MotionEasing::EaseInOut => 1.0 - (-2.0 * progress + 2.0).powi(2) / 2.0,
968 };
969 let interpolate = |from: f64, to: f64| from + (to - from) * progress;
970 GeometryBounds {
971 x: interpolate(motion.from.x, motion.to.x),
972 y: interpolate(motion.from.y, motion.to.y),
973 width: interpolate(motion.from.width, motion.to.width),
974 height: interpolate(motion.from.height, motion.to.height),
975 }
976}
977
978fn sample_trigger_progress(motion: &TriggerMotionState, now: Instant) -> f64 {
979 let progress = if motion.duration.is_zero() {
980 1.0
981 } else {
982 (now.saturating_duration_since(motion.started).as_secs_f64()
983 / motion.duration.as_secs_f64())
984 .clamp(0.0, 1.0)
985 };
986 motion.from + (motion.to - motion.from) * progress
987}
988
989#[derive(Clone, Debug)]
990pub(crate) struct GeometrySnapshot(Rc<GeometryState>);
991
992#[derive(Clone, Debug, Error, PartialEq)]
993pub enum GeometryError {
994 #[error("invalid geometry x={x}, y={y}, width={width}, height={height}")]
995 InvalidBounds {
996 x: f64,
997 y: f64,
998 width: f64,
999 height: f64,
1000 },
1001 #[error("affine transform members must be finite")]
1002 InvalidTransform,
1003 #[error("geometry for retained node {0} is not committed")]
1004 Unavailable(NodeId),
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010
1011 #[test]
1012 fn affine_inverse_is_scale_relative_and_rejects_singular_matrices() {
1013 for scale in [1e-200, 1e-10, 1e-5, 1.0, 1e5, 1e200] {
1014 let transform = Affine2D::scale(scale, scale * 2.0)
1015 .unwrap()
1016 .then(Affine2D::rotation_degrees(31.0).unwrap())
1017 .unwrap();
1018 let inverse = transform.inverse().unwrap();
1019 for point in [(0.0, 0.0), (1.0, 2.0), (-10.0, 40.0)] {
1020 let restored = inverse.map_point(transform.map_point(point));
1021 assert!((restored.0 - point.0).abs() < 1e-10);
1022 assert!((restored.1 - point.1).abs() < 1e-10);
1023 }
1024 }
1025 assert!(Affine2D::scale(0.0, 1.0).unwrap().inverse().is_none());
1026 assert!(
1027 Affine2D::new(1.0, 2.0, 2.0, 4.0, 0.0, 0.0)
1028 .unwrap()
1029 .inverse()
1030 .is_none()
1031 );
1032 }
1033
1034 #[test]
1035 fn affine_composition_inverse_and_bounds_share_one_coordinate_fact() {
1036 let transform = Affine2D::scale(2.0, 3.0)
1037 .unwrap()
1038 .then(Affine2D::rotation_degrees(90.0).unwrap())
1039 .unwrap()
1040 .then(Affine2D::translation(10.0, 20.0).unwrap())
1041 .unwrap();
1042 let mapped = transform.map_point((1.0, 2.0));
1043 assert!((mapped.0 - 4.0).abs() < 0.000_001);
1044 assert!((mapped.1 - 22.0).abs() < 0.000_001);
1045 let restored = transform.inverse().unwrap().map_point(mapped);
1046 assert!((restored.0 - 1.0).abs() < 0.000_001);
1047 assert!((restored.1 - 2.0).abs() < 0.000_001);
1048
1049 let rotated = Affine2D::rotation_degrees(90.0)
1050 .unwrap()
1051 .transform_bounds(GeometryBounds::new(0.0, 0.0, 10.0, 5.0).unwrap());
1052 assert!((rotated.x + 5.0).abs() < 0.000_001);
1053 assert!(rotated.y.abs() < 0.000_001);
1054 assert!((rotated.width - 5.0).abs() < 0.000_001);
1055 assert!((rotated.height - 10.0).abs() < 0.000_001);
1056 }
1057
1058 #[test]
1059 fn presented_geometry_maps_local_and_window_coordinates_both_ways() {
1060 let presented = PresentedGeometry::from_element(ElementGeometry {
1061 layout: GeometryBounds::new(10.0, 20.0, 100.0, 50.0).unwrap(),
1062 visual: GeometryBounds::new(30.0, 40.0, 200.0, 25.0).unwrap(),
1063 clip: Some(GeometryBounds::new(0.0, 0.0, 300.0, 200.0).unwrap()),
1064 })
1065 .unwrap();
1066 let window = presented.local_to_window.map_point((25.0, 10.0));
1067 assert_eq!(window, (80.0, 45.0));
1068 let local = presented.window_to_local.unwrap().map_point(window);
1069 assert!((local.0 - 25.0).abs() < 0.000_001);
1070 assert!((local.1 - 10.0).abs() < 0.000_001);
1071 }
1072
1073 #[test]
1074 fn changed_geometry_invalidates_exact_readers_and_snapshot_restores() {
1075 let mut tree = crate::RetainedUiTree::new();
1076 tree.reconcile(crate::UiNode::text("field")).unwrap();
1077 let node = tree.root_id().unwrap();
1078 let reader = ComponentInstancePath::root("Panel", "main");
1079 let registry = GeometryRegistry::new();
1080 let initial = ElementGeometry {
1081 layout: GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap(),
1082 visual: GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap(),
1083 clip: None,
1084 };
1085 registry.update(node, initial);
1086 assert!(registry.is_presented(node));
1087 registry.begin_frame();
1088 assert!(!registry.is_presented(node));
1089 assert!(!registry.update(node, initial));
1090 assert!(registry.is_presented(node));
1091 assert_eq!(registry.read(node, &reader).unwrap(), initial);
1092 let snapshot = registry.snapshot();
1093 registry.update(
1094 node,
1095 ElementGeometry {
1096 layout: GeometryBounds::new(0.0, 0.0, 120.0, 20.0).unwrap(),
1097 visual: GeometryBounds::new(0.0, 0.0, 120.0, 20.0).unwrap(),
1098 clip: None,
1099 },
1100 );
1101 assert!(registry.take_dirty().contains(&reader));
1102 registry.restore(snapshot);
1103 assert_eq!(registry.read(node, &reader).unwrap(), initial);
1104 }
1105
1106 #[test]
1107 fn layout_and_trigger_motion_sample_from_committed_native_state() {
1108 let mut tree = crate::RetainedUiTree::new();
1109 tree.reconcile(crate::UiNode::text("target")).unwrap();
1110 let node = tree.root_id().unwrap();
1111 let registry = GeometryRegistry::new();
1112 let start = Instant::now();
1113 let old = GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap();
1114 registry.update(
1115 node,
1116 ElementGeometry {
1117 layout: old,
1118 visual: old,
1119 clip: None,
1120 },
1121 );
1122 let next = GeometryBounds::new(100.0, 40.0, 200.0, 40.0).unwrap();
1123 let initial = registry.sample_layout_motion(
1124 node,
1125 next,
1126 LayoutMotionRequest {
1127 shared: None,
1128 duration: Duration::from_millis(100),
1129 easing: crate::MotionEasing::Linear,
1130 preference: crate::MotionPreference::Normal,
1131 now: start,
1132 },
1133 );
1134 assert!((initial.offset_x + 100.0).abs() < 0.01);
1135 let middle = registry.sample_layout_motion(
1136 node,
1137 next,
1138 LayoutMotionRequest {
1139 shared: None,
1140 duration: Duration::from_millis(100),
1141 easing: crate::MotionEasing::Linear,
1142 preference: crate::MotionPreference::Normal,
1143 now: start + Duration::from_millis(50),
1144 },
1145 );
1146 assert!((middle.offset_x + 50.0).abs() < 0.01);
1147
1148 let binding = crate::MotionProgressBinding::new(
1149 crate::MotionProgressDriver::Hover,
1150 crate::MotionSource::Transition(crate::MotionTransition::new(
1151 crate::MotionProperty::Opacity,
1152 0.5,
1153 1.0,
1154 100,
1155 )),
1156 )
1157 .unwrap();
1158 registry.set_motion_trigger(node, crate::MotionProgressDriver::Hover, true);
1159 let sample =
1160 registry.sample_motion_trigger(node, &binding, crate::MotionPreference::Normal, start);
1161 assert!(sample.active);
1162 let sample = registry.sample_motion_trigger(
1163 node,
1164 &binding,
1165 crate::MotionPreference::Normal,
1166 start + Duration::from_millis(100),
1167 );
1168 assert!((sample.value - 1.0).abs() < 0.01);
1169 }
1170
1171 #[test]
1172 fn shared_layout_history_expires_after_its_last_presented_frame() {
1173 let mut tree = crate::RetainedUiTree::new();
1174 tree.reconcile(crate::UiNode::text("card")).unwrap();
1175 let node = tree.root_id().unwrap();
1176 let registry = GeometryRegistry::new();
1177 let bounds = GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap();
1178 registry.update(
1179 node,
1180 ElementGeometry {
1181 layout: bounds,
1182 visual: bounds,
1183 clip: None,
1184 },
1185 );
1186 registry.sample_layout_motion(
1187 node,
1188 bounds,
1189 LayoutMotionRequest {
1190 shared: Some(("cards", "alpha")),
1191 duration: Duration::from_millis(100),
1192 easing: crate::MotionEasing::Linear,
1193 preference: crate::MotionPreference::Normal,
1194 now: Instant::now(),
1195 },
1196 );
1197 assert_eq!(registry.inner.borrow().shared_layout.len(), 1);
1198 registry.begin_frame();
1199 assert_eq!(registry.inner.borrow().shared_layout.len(), 1);
1200 registry.begin_frame();
1201 assert!(registry.inner.borrow().shared_layout.is_empty());
1202 }
1203}