Skip to main content

lgui_core/core/component/
component_state.rs

1use std::{
2    any::Any,
3    cell::{Cell, RefCell},
4    collections::{HashMap, HashSet},
5};
6
7use super::{ComponentId, CompositingLayerSpec, UiAction, UiId, UiScope};
8
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
10pub struct ComponentActionOutcome {
11    pub handled: bool,
12    pub changed: bool,
13    pub events: Vec<UiAction>,
14}
15
16impl ComponentActionOutcome {
17    pub const fn ignored() -> Self {
18        Self {
19            handled: false,
20            changed: false,
21            events: Vec::new(),
22        }
23    }
24
25    pub const fn handled(changed: bool) -> Self {
26        Self {
27            handled: true,
28            changed,
29            events: Vec::new(),
30        }
31    }
32
33    pub fn emit(mut self, event: UiAction) -> Self {
34        self.events.push(event);
35        self
36    }
37}
38
39impl From<bool> for ComponentActionOutcome {
40    fn from(changed: bool) -> Self {
41        if changed {
42            Self::handled(true)
43        } else {
44            Self::ignored()
45        }
46    }
47}
48
49pub trait ComponentState: Any {
50    fn as_any(&self) -> &dyn Any;
51
52    fn as_any_mut(&mut self) -> &mut dyn Any;
53
54    fn handle_action(&mut self, _action: &UiAction) -> ComponentActionOutcome {
55        ComponentActionOutcome::ignored()
56    }
57
58    fn advance(&mut self, _elapsed_ms: f32) -> bool {
59        false
60    }
61
62    fn wants_frame(&self) -> bool {
63        false
64    }
65
66    fn frame_interval_ms(&self) -> u64 {
67        16
68    }
69
70    fn take_route_invalidation(&mut self) -> bool {
71        false
72    }
73}
74
75/// Application-owned animation state for a retained compositing layer.
76///
77/// The framework advances this state and applies the resulting composition properties directly
78/// to the retained node. Static layer children are not rebuilt when only the spec changes.
79pub trait CompositingLayerAnimation: Clone + Default + 'static {
80    fn advance(&mut self, elapsed_ms: f32) -> bool;
81
82    fn compositing_layer_spec(&self) -> CompositingLayerSpec;
83
84    fn wants_frame(&self) -> bool {
85        true
86    }
87
88    fn frame_interval_ms(&self) -> u64 {
89        16
90    }
91}
92
93#[derive(Clone, Default)]
94struct CompositingLayerAnimationState<T>(T);
95
96impl<T> ComponentState for CompositingLayerAnimationState<T>
97where
98    T: CompositingLayerAnimation,
99{
100    fn as_any(&self) -> &dyn Any {
101        self
102    }
103
104    fn as_any_mut(&mut self) -> &mut dyn Any {
105        self
106    }
107
108    fn advance(&mut self, elapsed_ms: f32) -> bool {
109        self.0.advance(elapsed_ms)
110    }
111
112    fn wants_frame(&self) -> bool {
113        self.0.wants_frame()
114    }
115
116    fn frame_interval_ms(&self) -> u64 {
117        self.0.frame_interval_ms()
118    }
119}
120
121type CompositingLayerProjection = fn(&dyn ComponentState) -> CompositingLayerSpec;
122
123#[derive(Clone)]
124pub(crate) enum ComponentStateBinding {
125    Component {
126        owner: ComponentId,
127        invalidation_id: UiId,
128    },
129    CompositingLayer {
130        target_id: UiId,
131        project: CompositingLayerProjection,
132    },
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub(crate) enum RetainedNodeUpdate {
137    CompositingLayer(CompositingLayerSpec),
138}
139
140pub(crate) struct ComponentStateInvalidation {
141    pub state_id: UiId,
142    pub target_id: UiId,
143    pub owner: Option<ComponentId>,
144    pub frame_interval_ms: u64,
145    pub retained_update: Option<RetainedNodeUpdate>,
146}
147
148struct ComponentStateEntry {
149    state: Box<dyn ComponentState>,
150    binding: Option<ComponentStateBinding>,
151}
152
153#[derive(Default)]
154pub struct ComponentStateStore {
155    states: RefCell<HashMap<UiId, ComponentStateEntry>>,
156    seen: RefCell<HashSet<UiId>>,
157    rollback: RefCell<HashMap<UiId, Option<ComponentStateEntry>>>,
158    tracking_frame: Cell<bool>,
159}
160
161impl ComponentStateStore {
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    pub fn begin_frame(&self) {
167        if self.tracking_frame.get() {
168            self.abort_frame();
169        }
170        self.seen.borrow_mut().clear();
171        self.rollback.borrow_mut().clear();
172        self.tracking_frame.set(true);
173    }
174
175    pub fn end_frame(&self) {
176        let seen = self.seen.borrow();
177        self.states.borrow_mut().retain(|id, _| seen.contains(id));
178        self.tracking_frame.set(false);
179        drop(seen);
180        self.seen.borrow_mut().clear();
181        self.rollback.borrow_mut().clear();
182    }
183
184    pub fn abort_frame(&self) {
185        let rollback = std::mem::take(&mut *self.rollback.borrow_mut());
186        let mut states = self.states.borrow_mut();
187        for (id, previous) in rollback {
188            match previous {
189                Some(previous) => {
190                    states.insert(id, previous);
191                }
192                None => {
193                    states.remove(&id);
194                }
195            }
196        }
197        self.tracking_frame.set(false);
198        self.seen.borrow_mut().clear();
199    }
200
201    pub fn with_mut<T, R>(&self, id: &UiId, f: impl FnOnce(&mut T) -> R) -> R
202    where
203        T: ComponentState + Clone + Default + 'static,
204    {
205        self.with_mut_inner(id, None, f)
206    }
207
208    pub fn with_mut_for_component<T, R>(
209        &self,
210        id: &UiId,
211        owner: ComponentId,
212        invalidation_id: UiId,
213        f: impl FnOnce(&mut T) -> R,
214    ) -> R
215    where
216        T: ComponentState + Clone + Default + 'static,
217    {
218        self.with_mut_inner(
219            id,
220            Some(ComponentStateBinding::Component {
221                owner,
222                invalidation_id,
223            }),
224            f,
225        )
226    }
227
228    pub(crate) fn with_mut_for_compositing_layer<T, R>(
229        &self,
230        id: &UiId,
231        target_id: UiId,
232        f: impl FnOnce(&mut T) -> R,
233    ) -> (R, CompositingLayerSpec, bool)
234    where
235        T: CompositingLayerAnimation,
236    {
237        fn project<T>(state: &dyn ComponentState) -> CompositingLayerSpec
238        where
239            T: CompositingLayerAnimation,
240        {
241            state
242                .as_any()
243                .downcast_ref::<CompositingLayerAnimationState<T>>()
244                .expect("compositing layer animation type mismatch for UiId")
245                .0
246                .compositing_layer_spec()
247        }
248
249        self.with_mut_inner(
250            id,
251            Some(ComponentStateBinding::CompositingLayer {
252                target_id,
253                project: project::<T>,
254            }),
255            |state: &mut CompositingLayerAnimationState<T>| {
256                let result = f(&mut state.0);
257                (
258                    result,
259                    state.0.compositing_layer_spec(),
260                    state.0.wants_frame(),
261                )
262            },
263        )
264    }
265
266    fn with_mut_inner<T, R>(
267        &self,
268        id: &UiId,
269        binding: Option<ComponentStateBinding>,
270        f: impl FnOnce(&mut T) -> R,
271    ) -> R
272    where
273        T: ComponentState + Clone + Default + 'static,
274    {
275        self.mark_seen(id);
276        let mut states = self.states.borrow_mut();
277        if self.tracking_frame.get() && !self.rollback.borrow().contains_key(id) {
278            let previous = states.get(id).map(|entry| ComponentStateEntry {
279                state: Box::new(
280                    entry
281                        .state
282                        .as_any()
283                        .downcast_ref::<T>()
284                        .expect("component state type mismatch for UiId")
285                        .clone(),
286                ) as Box<dyn ComponentState>,
287                binding: entry.binding.clone(),
288            });
289            self.rollback.borrow_mut().insert(id.clone(), previous);
290        }
291        let entry = states
292            .entry(id.clone())
293            .or_insert_with(|| ComponentStateEntry {
294                state: Box::<T>::default(),
295                binding: None,
296            });
297        entry.binding = binding;
298        let state = entry
299            .state
300            .as_any_mut()
301            .downcast_mut::<T>()
302            .expect("component state type mismatch for UiId");
303        f(state)
304    }
305
306    pub fn handle_action(&self, id: &UiId, action: &UiAction) -> ComponentActionOutcome {
307        self.states
308            .borrow_mut()
309            .get_mut(id)
310            .map_or_else(ComponentActionOutcome::ignored, |entry| {
311                entry.state.handle_action(action)
312            })
313    }
314
315    pub fn contains(&self, id: &UiId) -> bool {
316        self.states.borrow().contains_key(id)
317    }
318
319    pub fn take_route_invalidation(&self, id: &UiId) -> bool {
320        self.states
321            .borrow_mut()
322            .get_mut(id)
323            .is_some_and(|entry| entry.state.take_route_invalidation())
324    }
325
326    pub fn advance(&self, elapsed_ms: f32) -> Vec<UiId> {
327        self.advance_invalidations(elapsed_ms)
328            .into_iter()
329            .map(|invalidation| invalidation.state_id)
330            .collect()
331    }
332
333    pub(crate) fn requested_frame_interval_ms(&self) -> Option<u64> {
334        self.states
335            .borrow()
336            .values()
337            .filter(|entry| entry.state.wants_frame())
338            .map(|entry| entry.state.frame_interval_ms().max(1))
339            .min()
340    }
341
342    pub(crate) fn advance_invalidations(&self, elapsed_ms: f32) -> Vec<ComponentStateInvalidation> {
343        let mut dirty = Vec::new();
344        for (id, entry) in self.states.borrow_mut().iter_mut() {
345            if entry.state.advance(elapsed_ms) {
346                let (target_id, owner, retained_update) = match entry.binding.as_ref() {
347                    Some(ComponentStateBinding::Component {
348                        owner,
349                        invalidation_id,
350                    }) => (invalidation_id.clone(), Some(*owner), None),
351                    Some(ComponentStateBinding::CompositingLayer { target_id, project }) => (
352                        target_id.clone(),
353                        None,
354                        Some(RetainedNodeUpdate::CompositingLayer(project(
355                            entry.state.as_ref(),
356                        ))),
357                    ),
358                    None => (id.clone(), None, None),
359                };
360                dirty.push(ComponentStateInvalidation {
361                    state_id: id.clone(),
362                    target_id,
363                    owner,
364                    frame_interval_ms: entry.state.frame_interval_ms().max(1),
365                    retained_update,
366                });
367            }
368        }
369        dirty
370    }
371
372    #[cfg(test)]
373    pub fn with<T, R>(&self, id: &UiId, f: impl FnOnce(&T) -> R) -> Option<R>
374    where
375        T: ComponentState + 'static,
376    {
377        self.mark_seen(id);
378        let states = self.states.borrow();
379        let state = states.get(id)?.state.as_any().downcast_ref::<T>()?;
380        Some(f(state))
381    }
382
383    pub fn preserve_scope(&self, scope: &UiScope) {
384        if !self.tracking_frame.get() {
385            return;
386        }
387        let scope_id = scope.scope_id();
388        let prefix = scope_id.as_str();
389        let nested_prefix = format!("{prefix}.");
390        let states = self.states.borrow();
391        let mut seen = self.seen.borrow_mut();
392        seen.extend(
393            states
394                .keys()
395                .filter(|id| id.as_str() == prefix || id.as_str().starts_with(&nested_prefix))
396                .cloned(),
397        );
398    }
399
400    fn mark_seen(&self, id: &UiId) {
401        if self.tracking_frame.get() {
402            self.seen.borrow_mut().insert(id.clone());
403        }
404    }
405}
406
407#[cfg(test)]
408#[path = "component_state_test.rs"]
409mod tests;