blinc_layout 0.5.1

Blinc layout engine - Flexbox layout powered by Taffy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! Interactive state management for layout nodes
//!
//! This module provides:
//! - Node state storage (arbitrary typed state per node)
//! - Dirty tracking for incremental re-renders
//! - FSM integration for interaction states
//!
//! # Architecture
//!
//! The interactive system is integrated at the layout level because:
//! - State is tied to layout nodes, not abstract widgets
//! - Dirty tracking enables incremental re-rendering of the tree
//! - FSM state transitions affect rendering properties directly
//!
//! # Example
//!
//! ```ignore
//! use blinc_layout::prelude::*;
//!
//! // Create an interactive render tree
//! let mut tree = InteractiveTree::new();
//!
//! // Set state for a node
//! tree.set_state(node_id, ButtonState { scale: 1.0 });
//!
//! // Mark nodes as dirty
//! tree.mark_dirty(node_id);
//!
//! // Process only dirty nodes
//! for node_id in tree.take_dirty() {
//!     // Re-render this node
//! }
//! ```

use std::any::Any;
use std::collections::{HashMap, HashSet};

use blinc_core::events::Event;
use blinc_core::fsm::{EventId, StateMachine};

use crate::tree::LayoutNodeId;

/// Trait for node state types
///
/// Any type that can be stored as node state must implement this trait.
/// The `as_any` methods enable type-safe downcasting.
pub trait NodeState: Send + 'static {
    /// Get self as Any for downcasting
    fn as_any(&self) -> &dyn Any;

    /// Get self as mutable Any for downcasting
    fn as_any_mut(&mut self) -> &mut dyn Any;
}

/// Blanket implementation for all types
impl<T: Send + 'static> NodeState for T {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}

/// Data stored for each interactive node
#[derive(Default)]
struct NodeData {
    /// Optional FSM for interaction states
    fsm: Option<StateMachine>,
    /// Custom state (type-erased)
    state: Option<Box<dyn NodeState>>,
}

/// Dirty tracking for incremental re-renders
#[derive(Default)]
pub struct DirtyTracker {
    /// Set of dirty node IDs
    dirty: HashSet<LayoutNodeId>,
    /// Whether the entire tree needs re-layout
    needs_layout: bool,
}

impl DirtyTracker {
    /// Create a new dirty tracker
    pub fn new() -> Self {
        Self::default()
    }

    /// Mark a node as dirty (needs re-render)
    pub fn mark(&mut self, id: LayoutNodeId) {
        self.dirty.insert(id);
    }

    /// Mark the tree as needing full re-layout
    pub fn mark_layout(&mut self) {
        self.needs_layout = true;
    }

    /// Check if a node is dirty
    pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
        self.dirty.contains(&id)
    }

    /// Check if any nodes are dirty
    pub fn has_dirty(&self) -> bool {
        !self.dirty.is_empty()
    }

    /// Check if layout is needed
    pub fn needs_layout(&self) -> bool {
        self.needs_layout
    }

    /// Take all dirty node IDs (clears the set)
    pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
        self.dirty.drain().collect()
    }

    /// Clear the layout flag
    pub fn clear_layout(&mut self) {
        self.needs_layout = false;
    }

    /// Clear all dirty flags
    pub fn clear_all(&mut self) {
        self.dirty.clear();
        self.needs_layout = false;
    }
}

/// Interactive state manager for layout nodes
///
/// Manages FSMs, custom state, and dirty tracking for layout nodes.
/// This is the core infrastructure for interactive widgets.
pub struct InteractiveContext {
    /// Node data storage (keyed by LayoutNodeId's raw index for HashMap)
    nodes: HashMap<u64, NodeData>,
    /// Dirty tracker
    dirty: DirtyTracker,
}

impl Default for InteractiveContext {
    fn default() -> Self {
        Self::new()
    }
}

impl InteractiveContext {
    /// Create a new interactive context
    pub fn new() -> Self {
        Self {
            nodes: HashMap::new(),
            dirty: DirtyTracker::new(),
        }
    }

    /// Get the raw key for a LayoutNodeId
    fn key(id: LayoutNodeId) -> u64 {
        // SlotMap keys can be converted to u64 via their index
        use slotmap::Key;
        id.data().as_ffi()
    }

    /// Register a node with optional FSM
    pub fn register(&mut self, id: LayoutNodeId, fsm: Option<StateMachine>) {
        self.nodes
            .insert(Self::key(id), NodeData { fsm, state: None });
        self.dirty.mark(id);
    }

    /// Register a node with an FSM
    pub fn register_with_fsm(&mut self, id: LayoutNodeId, fsm: StateMachine) {
        self.register(id, Some(fsm));
    }

    /// Unregister a node
    pub fn unregister(&mut self, id: LayoutNodeId) {
        self.nodes.remove(&Self::key(id));
    }

    /// Check if a node is registered
    pub fn is_registered(&self, id: LayoutNodeId) -> bool {
        self.nodes.contains_key(&Self::key(id))
    }

    /// Get the FSM state for a node
    pub fn get_fsm_state(&self, id: LayoutNodeId) -> Option<u32> {
        self.nodes
            .get(&Self::key(id))
            .and_then(|d| d.fsm.as_ref())
            .map(|fsm| fsm.current_state())
    }

    /// Send an event to a node's FSM
    ///
    /// Returns true if the FSM transitioned to a new state.
    pub fn send_event(&mut self, id: LayoutNodeId, event_type: EventId) -> bool {
        let key = Self::key(id);
        if let Some(data) = self.nodes.get_mut(&key) {
            if let Some(ref mut fsm) = data.fsm {
                let old_state = fsm.current_state();
                fsm.send(event_type);
                let new_state = fsm.current_state();

                if old_state != new_state {
                    self.dirty.mark(id);
                    return true;
                }
            }
        }
        false
    }

    /// Dispatch an Event struct to a node's FSM
    ///
    /// Convenience method that extracts the event_type and calls send_event.
    /// Returns true if the FSM transitioned to a new state.
    pub fn dispatch_event(&mut self, id: LayoutNodeId, event: &Event) -> bool {
        self.send_event(id, event.event_type)
    }

    /// Set custom state for a node
    pub fn set_state<S: NodeState>(&mut self, id: LayoutNodeId, state: S) {
        let key = Self::key(id);
        if let Some(data) = self.nodes.get_mut(&key) {
            data.state = Some(Box::new(state));
            self.dirty.mark(id);
        } else {
            // Auto-register if not registered
            self.nodes.insert(
                key,
                NodeData {
                    fsm: None,
                    state: Some(Box::new(state)),
                },
            );
            self.dirty.mark(id);
        }
    }

    /// Get custom state for a node (immutable)
    pub fn get_state<S: 'static>(&self, id: LayoutNodeId) -> Option<&S> {
        self.nodes
            .get(&Self::key(id))
            .and_then(|d| d.state.as_ref())
            .and_then(|s| {
                // Double deref to get concrete type (Box -> dyn NodeState -> concrete)
                (**s).as_any().downcast_ref()
            })
    }

    /// Get custom state for a node (mutable)
    pub fn get_state_mut<S: 'static>(&mut self, id: LayoutNodeId) -> Option<&mut S> {
        self.nodes
            .get_mut(&Self::key(id))
            .and_then(|d| d.state.as_mut())
            .and_then(|s| {
                // Double deref to get concrete type
                (**s).as_any_mut().downcast_mut()
            })
    }

    /// Mark a node as dirty
    pub fn mark_dirty(&mut self, id: LayoutNodeId) {
        self.dirty.mark(id);
    }

    /// Mark the tree as needing full re-layout
    pub fn mark_layout(&mut self) {
        self.dirty.mark_layout();
    }

    /// Check if a node is dirty
    pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
        self.dirty.is_dirty(id)
    }

    /// Check if any nodes are dirty
    pub fn has_dirty(&self) -> bool {
        self.dirty.has_dirty()
    }

    /// Check if layout is needed
    pub fn needs_layout(&self) -> bool {
        self.dirty.needs_layout()
    }

    /// Take all dirty node IDs (clears the set)
    pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
        self.dirty.take_dirty()
    }

    /// Clear the layout flag
    pub fn clear_layout(&mut self) {
        self.dirty.clear_layout();
    }

    /// Clear all dirty flags
    pub fn clear_all(&mut self) {
        self.dirty.clear_all();
    }

    /// Get the dirty tracker (immutable)
    pub fn dirty_tracker(&self) -> &DirtyTracker {
        &self.dirty
    }

    /// Get the dirty tracker (mutable)
    pub fn dirty_tracker_mut(&mut self) -> &mut DirtyTracker {
        &mut self.dirty
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use blinc_core::events::{event_types, EventData};
    use blinc_core::fsm::StateMachine;
    use slotmap::SlotMap;

    // Create test node IDs
    fn create_node_id() -> LayoutNodeId {
        let mut sm: SlotMap<LayoutNodeId, ()> = SlotMap::with_key();
        sm.insert(())
    }

    #[test]
    fn test_dirty_tracker() {
        let mut tracker = DirtyTracker::new();
        let id = create_node_id();

        assert!(!tracker.is_dirty(id));
        assert!(!tracker.has_dirty());

        tracker.mark(id);
        assert!(tracker.is_dirty(id));
        assert!(tracker.has_dirty());

        let dirty = tracker.take_dirty();
        assert_eq!(dirty.len(), 1);
        assert!(!tracker.has_dirty());
    }

    #[test]
    fn test_interactive_context_state() {
        let mut ctx = InteractiveContext::new();
        let id = create_node_id();

        // Set state
        ctx.set_state(id, 42u32);

        // Get state
        let state = ctx.get_state::<u32>(id);
        assert_eq!(state, Some(&42));

        // Modify state
        if let Some(s) = ctx.get_state_mut::<u32>(id) {
            *s = 100;
        }
        assert_eq!(ctx.get_state::<u32>(id), Some(&100));
    }

    #[test]
    fn test_interactive_context_fsm() {
        let mut ctx = InteractiveContext::new();
        let id = create_node_id();

        // Create FSM: IDLE --(POINTER_ENTER)--> HOVERED
        let fsm = StateMachine::builder(0)
            .on(0, event_types::POINTER_ENTER, 1)
            .on(1, event_types::POINTER_LEAVE, 0)
            .build();

        ctx.register_with_fsm(id, fsm);
        assert_eq!(ctx.get_fsm_state(id), Some(0));

        // Clear dirty flag from registration
        ctx.take_dirty();

        // Send event directly
        let transitioned = ctx.send_event(id, event_types::POINTER_ENTER);
        assert!(transitioned);
        assert_eq!(ctx.get_fsm_state(id), Some(1));
        assert!(ctx.is_dirty(id));

        // Also test dispatch_event with Event struct
        ctx.take_dirty();
        let event = Event {
            event_type: event_types::POINTER_LEAVE,
            target: 0,
            data: EventData::Pointer {
                x: 0.0,
                y: 0.0,
                button: 0,
                pressure: 1.0,
            },
            timestamp: 0,
            propagation_stopped: false,
        };

        let transitioned = ctx.dispatch_event(id, &event);
        assert!(transitioned);
        assert_eq!(ctx.get_fsm_state(id), Some(0));
    }

    #[test]
    fn test_complex_state_type() {
        #[derive(Debug, PartialEq)]
        struct ButtonState {
            scale: f32,
            clicked: bool,
        }

        let mut ctx = InteractiveContext::new();
        let id = create_node_id();

        ctx.set_state(
            id,
            ButtonState {
                scale: 1.0,
                clicked: false,
            },
        );

        if let Some(state) = ctx.get_state_mut::<ButtonState>(id) {
            state.scale = 0.95;
            state.clicked = true;
        }

        let state = ctx.get_state::<ButtonState>(id).unwrap();
        assert_eq!(state.scale, 0.95);
        assert!(state.clicked);
    }
}