Skip to main content

blinc_layout/
interactive.rs

1//! Interactive state management for layout nodes
2//!
3//! This module provides:
4//! - Node state storage (arbitrary typed state per node)
5//! - Dirty tracking for incremental re-renders
6//! - FSM integration for interaction states
7//!
8//! # Architecture
9//!
10//! The interactive system is integrated at the layout level because:
11//! - State is tied to layout nodes, not abstract widgets
12//! - Dirty tracking enables incremental re-rendering of the tree
13//! - FSM state transitions affect rendering properties directly
14//!
15//! # Example
16//!
17//! ```ignore
18//! use blinc_layout::prelude::*;
19//!
20//! // Create an interactive render tree
21//! let mut tree = InteractiveTree::new();
22//!
23//! // Set state for a node
24//! tree.set_state(node_id, ButtonState { scale: 1.0 });
25//!
26//! // Mark nodes as dirty
27//! tree.mark_dirty(node_id);
28//!
29//! // Process only dirty nodes
30//! for node_id in tree.take_dirty() {
31//!     // Re-render this node
32//! }
33//! ```
34
35use std::any::Any;
36use std::collections::{HashMap, HashSet};
37
38use blinc_core::events::Event;
39use blinc_core::fsm::{EventId, StateMachine};
40
41use crate::tree::LayoutNodeId;
42
43/// Trait for node state types
44///
45/// Any type that can be stored as node state must implement this trait.
46/// The `as_any` methods enable type-safe downcasting.
47pub trait NodeState: Send + 'static {
48    /// Get self as Any for downcasting
49    fn as_any(&self) -> &dyn Any;
50
51    /// Get self as mutable Any for downcasting
52    fn as_any_mut(&mut self) -> &mut dyn Any;
53}
54
55/// Blanket implementation for all types
56impl<T: Send + 'static> NodeState for T {
57    fn as_any(&self) -> &dyn Any {
58        self
59    }
60
61    fn as_any_mut(&mut self) -> &mut dyn Any {
62        self
63    }
64}
65
66/// Data stored for each interactive node
67#[derive(Default)]
68struct NodeData {
69    /// Optional FSM for interaction states
70    fsm: Option<StateMachine>,
71    /// Custom state (type-erased)
72    state: Option<Box<dyn NodeState>>,
73}
74
75/// Dirty tracking for incremental re-renders
76#[derive(Default)]
77pub struct DirtyTracker {
78    /// Set of dirty node IDs
79    dirty: HashSet<LayoutNodeId>,
80    /// Whether the entire tree needs re-layout
81    needs_layout: bool,
82}
83
84impl DirtyTracker {
85    /// Create a new dirty tracker
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Mark a node as dirty (needs re-render)
91    pub fn mark(&mut self, id: LayoutNodeId) {
92        self.dirty.insert(id);
93    }
94
95    /// Mark the tree as needing full re-layout
96    pub fn mark_layout(&mut self) {
97        self.needs_layout = true;
98    }
99
100    /// Check if a node is dirty
101    pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
102        self.dirty.contains(&id)
103    }
104
105    /// Check if any nodes are dirty
106    pub fn has_dirty(&self) -> bool {
107        !self.dirty.is_empty()
108    }
109
110    /// Check if layout is needed
111    pub fn needs_layout(&self) -> bool {
112        self.needs_layout
113    }
114
115    /// Take all dirty node IDs (clears the set)
116    pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
117        self.dirty.drain().collect()
118    }
119
120    /// Clear the layout flag
121    pub fn clear_layout(&mut self) {
122        self.needs_layout = false;
123    }
124
125    /// Clear all dirty flags
126    pub fn clear_all(&mut self) {
127        self.dirty.clear();
128        self.needs_layout = false;
129    }
130}
131
132/// Interactive state manager for layout nodes
133///
134/// Manages FSMs, custom state, and dirty tracking for layout nodes.
135/// This is the core infrastructure for interactive widgets.
136pub struct InteractiveContext {
137    /// Node data storage (keyed by LayoutNodeId's raw index for HashMap)
138    nodes: HashMap<u64, NodeData>,
139    /// Dirty tracker
140    dirty: DirtyTracker,
141}
142
143impl Default for InteractiveContext {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl InteractiveContext {
150    /// Create a new interactive context
151    pub fn new() -> Self {
152        Self {
153            nodes: HashMap::new(),
154            dirty: DirtyTracker::new(),
155        }
156    }
157
158    /// Get the raw key for a LayoutNodeId
159    fn key(id: LayoutNodeId) -> u64 {
160        // SlotMap keys can be converted to u64 via their index
161        use slotmap::Key;
162        id.data().as_ffi()
163    }
164
165    /// Register a node with optional FSM
166    pub fn register(&mut self, id: LayoutNodeId, fsm: Option<StateMachine>) {
167        self.nodes
168            .insert(Self::key(id), NodeData { fsm, state: None });
169        self.dirty.mark(id);
170    }
171
172    /// Register a node with an FSM
173    pub fn register_with_fsm(&mut self, id: LayoutNodeId, fsm: StateMachine) {
174        self.register(id, Some(fsm));
175    }
176
177    /// Unregister a node
178    pub fn unregister(&mut self, id: LayoutNodeId) {
179        self.nodes.remove(&Self::key(id));
180    }
181
182    /// Check if a node is registered
183    pub fn is_registered(&self, id: LayoutNodeId) -> bool {
184        self.nodes.contains_key(&Self::key(id))
185    }
186
187    /// Get the FSM state for a node
188    pub fn get_fsm_state(&self, id: LayoutNodeId) -> Option<u32> {
189        self.nodes
190            .get(&Self::key(id))
191            .and_then(|d| d.fsm.as_ref())
192            .map(|fsm| fsm.current_state())
193    }
194
195    /// Send an event to a node's FSM
196    ///
197    /// Returns true if the FSM transitioned to a new state.
198    pub fn send_event(&mut self, id: LayoutNodeId, event_type: EventId) -> bool {
199        let key = Self::key(id);
200        if let Some(data) = self.nodes.get_mut(&key) {
201            if let Some(ref mut fsm) = data.fsm {
202                let old_state = fsm.current_state();
203                fsm.send(event_type);
204                let new_state = fsm.current_state();
205
206                if old_state != new_state {
207                    self.dirty.mark(id);
208                    return true;
209                }
210            }
211        }
212        false
213    }
214
215    /// Dispatch an Event struct to a node's FSM
216    ///
217    /// Convenience method that extracts the event_type and calls send_event.
218    /// Returns true if the FSM transitioned to a new state.
219    pub fn dispatch_event(&mut self, id: LayoutNodeId, event: &Event) -> bool {
220        self.send_event(id, event.event_type)
221    }
222
223    /// Set custom state for a node
224    pub fn set_state<S: NodeState>(&mut self, id: LayoutNodeId, state: S) {
225        let key = Self::key(id);
226        if let Some(data) = self.nodes.get_mut(&key) {
227            data.state = Some(Box::new(state));
228            self.dirty.mark(id);
229        } else {
230            // Auto-register if not registered
231            self.nodes.insert(
232                key,
233                NodeData {
234                    fsm: None,
235                    state: Some(Box::new(state)),
236                },
237            );
238            self.dirty.mark(id);
239        }
240    }
241
242    /// Get custom state for a node (immutable)
243    pub fn get_state<S: 'static>(&self, id: LayoutNodeId) -> Option<&S> {
244        self.nodes
245            .get(&Self::key(id))
246            .and_then(|d| d.state.as_ref())
247            .and_then(|s| {
248                // Double deref to get concrete type (Box -> dyn NodeState -> concrete)
249                (**s).as_any().downcast_ref()
250            })
251    }
252
253    /// Get custom state for a node (mutable)
254    pub fn get_state_mut<S: 'static>(&mut self, id: LayoutNodeId) -> Option<&mut S> {
255        self.nodes
256            .get_mut(&Self::key(id))
257            .and_then(|d| d.state.as_mut())
258            .and_then(|s| {
259                // Double deref to get concrete type
260                (**s).as_any_mut().downcast_mut()
261            })
262    }
263
264    /// Mark a node as dirty
265    pub fn mark_dirty(&mut self, id: LayoutNodeId) {
266        self.dirty.mark(id);
267    }
268
269    /// Mark the tree as needing full re-layout
270    pub fn mark_layout(&mut self) {
271        self.dirty.mark_layout();
272    }
273
274    /// Check if a node is dirty
275    pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
276        self.dirty.is_dirty(id)
277    }
278
279    /// Check if any nodes are dirty
280    pub fn has_dirty(&self) -> bool {
281        self.dirty.has_dirty()
282    }
283
284    /// Check if layout is needed
285    pub fn needs_layout(&self) -> bool {
286        self.dirty.needs_layout()
287    }
288
289    /// Take all dirty node IDs (clears the set)
290    pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
291        self.dirty.take_dirty()
292    }
293
294    /// Clear the layout flag
295    pub fn clear_layout(&mut self) {
296        self.dirty.clear_layout();
297    }
298
299    /// Clear all dirty flags
300    pub fn clear_all(&mut self) {
301        self.dirty.clear_all();
302    }
303
304    /// Get the dirty tracker (immutable)
305    pub fn dirty_tracker(&self) -> &DirtyTracker {
306        &self.dirty
307    }
308
309    /// Get the dirty tracker (mutable)
310    pub fn dirty_tracker_mut(&mut self) -> &mut DirtyTracker {
311        &mut self.dirty
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use blinc_core::events::{event_types, EventData};
319    use blinc_core::fsm::StateMachine;
320    use slotmap::SlotMap;
321
322    // Create test node IDs
323    fn create_node_id() -> LayoutNodeId {
324        let mut sm: SlotMap<LayoutNodeId, ()> = SlotMap::with_key();
325        sm.insert(())
326    }
327
328    #[test]
329    fn test_dirty_tracker() {
330        let mut tracker = DirtyTracker::new();
331        let id = create_node_id();
332
333        assert!(!tracker.is_dirty(id));
334        assert!(!tracker.has_dirty());
335
336        tracker.mark(id);
337        assert!(tracker.is_dirty(id));
338        assert!(tracker.has_dirty());
339
340        let dirty = tracker.take_dirty();
341        assert_eq!(dirty.len(), 1);
342        assert!(!tracker.has_dirty());
343    }
344
345    #[test]
346    fn test_interactive_context_state() {
347        let mut ctx = InteractiveContext::new();
348        let id = create_node_id();
349
350        // Set state
351        ctx.set_state(id, 42u32);
352
353        // Get state
354        let state = ctx.get_state::<u32>(id);
355        assert_eq!(state, Some(&42));
356
357        // Modify state
358        if let Some(s) = ctx.get_state_mut::<u32>(id) {
359            *s = 100;
360        }
361        assert_eq!(ctx.get_state::<u32>(id), Some(&100));
362    }
363
364    #[test]
365    fn test_interactive_context_fsm() {
366        let mut ctx = InteractiveContext::new();
367        let id = create_node_id();
368
369        // Create FSM: IDLE --(POINTER_ENTER)--> HOVERED
370        let fsm = StateMachine::builder(0)
371            .on(0, event_types::POINTER_ENTER, 1)
372            .on(1, event_types::POINTER_LEAVE, 0)
373            .build();
374
375        ctx.register_with_fsm(id, fsm);
376        assert_eq!(ctx.get_fsm_state(id), Some(0));
377
378        // Clear dirty flag from registration
379        ctx.take_dirty();
380
381        // Send event directly
382        let transitioned = ctx.send_event(id, event_types::POINTER_ENTER);
383        assert!(transitioned);
384        assert_eq!(ctx.get_fsm_state(id), Some(1));
385        assert!(ctx.is_dirty(id));
386
387        // Also test dispatch_event with Event struct
388        ctx.take_dirty();
389        let event = Event {
390            event_type: event_types::POINTER_LEAVE,
391            target: 0,
392            data: EventData::Pointer {
393                x: 0.0,
394                y: 0.0,
395                button: 0,
396                pressure: 1.0,
397            },
398            timestamp: 0,
399            propagation_stopped: false,
400        };
401
402        let transitioned = ctx.dispatch_event(id, &event);
403        assert!(transitioned);
404        assert_eq!(ctx.get_fsm_state(id), Some(0));
405    }
406
407    #[test]
408    fn test_complex_state_type() {
409        #[derive(Debug, PartialEq)]
410        struct ButtonState {
411            scale: f32,
412            clicked: bool,
413        }
414
415        let mut ctx = InteractiveContext::new();
416        let id = create_node_id();
417
418        ctx.set_state(
419            id,
420            ButtonState {
421                scale: 1.0,
422                clicked: false,
423            },
424        );
425
426        if let Some(state) = ctx.get_state_mut::<ButtonState>(id) {
427            state.scale = 0.95;
428            state.clicked = true;
429        }
430
431        let state = ctx.get_state::<ButtonState>(id).unwrap();
432        assert_eq!(state.scale, 0.95);
433        assert!(state.clicked);
434    }
435}