Skip to main content

gpui_rhai/
geometry.rs

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    /// Construct validated logical geometry.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`GeometryError::InvalidBounds`] for non-finite values or
24    /// negative sizes.
25    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/// Finite two-dimensional affine transform using column-vector coordinates.
55///
56/// Points map as `x' = m11*x + m21*y + tx` and
57/// `y' = m12*x + m22*y + ty`.
58#[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    /// Construct one validated affine matrix.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`GeometryError::InvalidTransform`] for non-finite members.
89    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    /// Construct a translation.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`GeometryError::InvalidTransform`] for non-finite offsets.
117    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    /// Construct an axis-aligned scale.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`GeometryError::InvalidTransform`] for non-finite scales.
126    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    /// Construct a rotation in degrees.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`GeometryError::InvalidTransform`] for a non-finite angle.
135    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    /// Construct independent X/Y skews in degrees.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`GeometryError::InvalidTransform`] for invalid angles.
149    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    /// Apply `self`, then `next`.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`GeometryError::InvalidTransform`] if composition overflows.
168    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    /// Rebase this transform around an origin.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`GeometryError::InvalidTransform`] for invalid coordinates.
186    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    /// Build bidirectional presented coordinates from committed element facts.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`GeometryError::InvalidTransform`] if derived scale or
295    /// translation overflows.
296    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    // Keep direct NodeId dependencies separate: detaching a ref must not detach
338    // an independent direct read of the same node by the same contribution.
339    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            // GPUI 0.2.2 exposes an element offset but no public arbitrary
524            // subtree scale transform. Keep visual/hit geometry honest and
525            // snap size while animating position.
526            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    /// Read committed geometry and register one exact component dependency.
707    ///
708    /// # Errors
709    ///
710    /// Returns [`GeometryError::Unavailable`] before first prepaint or after
711    /// unmount.
712    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    /// Replace the logical-ref observation targets for one committed view scope.
763    /// The snapshot is authoritative even when no binding currently exists.
764    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}