blinc_layout/
interactive.rs1use 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
43pub trait NodeState: Send + 'static {
48 fn as_any(&self) -> &dyn Any;
50
51 fn as_any_mut(&mut self) -> &mut dyn Any;
53}
54
55impl<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#[derive(Default)]
68struct NodeData {
69 fsm: Option<StateMachine>,
71 state: Option<Box<dyn NodeState>>,
73}
74
75#[derive(Default)]
77pub struct DirtyTracker {
78 dirty: HashSet<LayoutNodeId>,
80 needs_layout: bool,
82}
83
84impl DirtyTracker {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn mark(&mut self, id: LayoutNodeId) {
92 self.dirty.insert(id);
93 }
94
95 pub fn mark_layout(&mut self) {
97 self.needs_layout = true;
98 }
99
100 pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
102 self.dirty.contains(&id)
103 }
104
105 pub fn has_dirty(&self) -> bool {
107 !self.dirty.is_empty()
108 }
109
110 pub fn needs_layout(&self) -> bool {
112 self.needs_layout
113 }
114
115 pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
117 self.dirty.drain().collect()
118 }
119
120 pub fn clear_layout(&mut self) {
122 self.needs_layout = false;
123 }
124
125 pub fn clear_all(&mut self) {
127 self.dirty.clear();
128 self.needs_layout = false;
129 }
130}
131
132pub struct InteractiveContext {
137 nodes: HashMap<u64, NodeData>,
139 dirty: DirtyTracker,
141}
142
143impl Default for InteractiveContext {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl InteractiveContext {
150 pub fn new() -> Self {
152 Self {
153 nodes: HashMap::new(),
154 dirty: DirtyTracker::new(),
155 }
156 }
157
158 fn key(id: LayoutNodeId) -> u64 {
160 use slotmap::Key;
162 id.data().as_ffi()
163 }
164
165 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 pub fn register_with_fsm(&mut self, id: LayoutNodeId, fsm: StateMachine) {
174 self.register(id, Some(fsm));
175 }
176
177 pub fn unregister(&mut self, id: LayoutNodeId) {
179 self.nodes.remove(&Self::key(id));
180 }
181
182 pub fn is_registered(&self, id: LayoutNodeId) -> bool {
184 self.nodes.contains_key(&Self::key(id))
185 }
186
187 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 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 pub fn dispatch_event(&mut self, id: LayoutNodeId, event: &Event) -> bool {
220 self.send_event(id, event.event_type)
221 }
222
223 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 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 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 (**s).as_any().downcast_ref()
250 })
251 }
252
253 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 (**s).as_any_mut().downcast_mut()
261 })
262 }
263
264 pub fn mark_dirty(&mut self, id: LayoutNodeId) {
266 self.dirty.mark(id);
267 }
268
269 pub fn mark_layout(&mut self) {
271 self.dirty.mark_layout();
272 }
273
274 pub fn is_dirty(&self, id: LayoutNodeId) -> bool {
276 self.dirty.is_dirty(id)
277 }
278
279 pub fn has_dirty(&self) -> bool {
281 self.dirty.has_dirty()
282 }
283
284 pub fn needs_layout(&self) -> bool {
286 self.dirty.needs_layout()
287 }
288
289 pub fn take_dirty(&mut self) -> Vec<LayoutNodeId> {
291 self.dirty.take_dirty()
292 }
293
294 pub fn clear_layout(&mut self) {
296 self.dirty.clear_layout();
297 }
298
299 pub fn clear_all(&mut self) {
301 self.dirty.clear_all();
302 }
303
304 pub fn dirty_tracker(&self) -> &DirtyTracker {
306 &self.dirty
307 }
308
309 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 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 ctx.set_state(id, 42u32);
352
353 let state = ctx.get_state::<u32>(id);
355 assert_eq!(state, Some(&42));
356
357 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 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 ctx.take_dirty();
380
381 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 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}