Skip to main content

cvkg_core/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     -- State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     -- Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     -- Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    -- Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     -- Read the target, its surrounding context, and its full call graph
13//                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     -- Every major pub fn, unsafe block, and non-trivial algorithm in
15//                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   -- Check every tool call / command for progress every 30 seconds.
18//                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//   CVKG Extended: Section 2 of the CVKG Design Specification
24
25//! The View trait is the fundamental building block of CVKG. Every UI element -- from a plain text label
26//! to a complex navigation controller -- is a View. The trait is intentionally minimal; complexity emerges
27//! through modifier composition.
28//!
29//! # Conformance rules:
30//! 1. `body()` must be pure and side-effect free
31//! 2. Primitive views use `Never` as `Body` and register a `PaintCommand` directly with the scene graph
32//! 3. `View` types must implement `Send` but not necessarily `Sync`, enabling safe multi-threaded layout passes
33
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::collections::HashSet;
37use std::str::FromStr;
38
39pub mod error_types;
40pub mod future_views;
41pub mod security;
42
43pub use future_views::{HologramView, ParticleEmitter, StreamingText};
44
45// P1-13: extracted modules
46pub mod asset;
47pub mod error_boundary;
48pub mod knowledge;
49pub mod renderer;
50pub mod undo;
51pub mod window;
52
53// P1-13: re-exports for backward compatibility
54pub use asset::{AssetKey, AssetState, TokenValue, YggdrasilTokens};
55pub use error_boundary::{ComponentErrorState, ErrorBoundary};
56pub use knowledge::{AnnouncementPriority, KnowledgeFragment, KnowledgeId, KnowledgeState, MemoryLayer, Realm, TemporalEdge, TemporalNode};
57pub use undo::{UndoGroup, UndoManager};
58pub use window::{Window, WindowCloseAction, WindowConfig, WindowHandle, WindowId, WindowLevel};
59
60pub trait View: Sized + Send {
61    /// The concrete type produced after applying modifiers.
62    /// For primitive views this is Self.
63    type Body: View;
64
65    fn body(self) -> Self::Body;
66
67    /// Render this view into the provided renderer at the specified bounds.
68    /// Primitive views override this to perform drawing operations.
69    fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
70
71    /// Calculate the natural (intrinsic) size of this view given proposed constraints.
72    /// This allows views like Buttons or Labels to inform the layout engine of their needs.
73    fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
74        Size::ZERO
75    }
76
77    /// Optionally provide a layout implementation for this view.
78    fn layout(&self) -> Option<&dyn layout::LayoutView> {
79        None
80    }
81
82    /// Returns the flex weight of this view for proportional distribution in stacks.
83    fn flex_weight(&self) -> f32 {
84        0.0
85    }
86
87    /// Returns the grid placement configuration for this view if it is laid out in a Grid.
88    fn get_grid_placement(&self) -> Option<GridPlacement> {
89        None
90    }
91
92    /// Provided modifier entry point
93    fn modifier<M: ViewModifier>(self, m: M) -> ModifiedView<Self, M> {
94        ModifiedView::new(self, m)
95    }
96
97    /// Apply a Bifrost (Frosted Glass) effect to the view
98    fn bifrost(
99        self,
100        blur: f32,
101        saturation: f32,
102        opacity: f32,
103    ) -> ModifiedView<Self, BifrostModifier> {
104        self.modifier(BifrostModifier {
105            blur,
106            saturation,
107            opacity,
108            fresnel_strength: 1.0,
109        })
110    }
111
112    /// Apply a Bifrost (Frosted Glass) effect with full parameter control.
113    fn bifrost_full(
114        self,
115        blur: f32,
116        saturation: f32,
117        opacity: f32,
118        fresnel_strength: f32,
119    ) -> ModifiedView<Self, BifrostModifier> {
120        self.modifier(BifrostModifier {
121            blur,
122            saturation,
123            opacity,
124            fresnel_strength,
125        })
126    }
127
128    /// Apply a Gungnir (Neon Glow) effect to the view
129    fn gungnir(
130        self,
131        color: impl Into<String>,
132        radius: f32,
133        intensity: f32,
134    ) -> ModifiedView<Self, GungnirModifier> {
135        self.modifier(GungnirModifier {
136            color: color.into(),
137            radius,
138            intensity,
139        })
140    }
141
142    /// Apply a Mjolnir Slice (Geometric cut) to the view
143    fn mjolnir_slice(self, angle: f32, offset: f32) -> ModifiedView<Self, MjolnirSliceModifier> {
144        self.modifier(MjolnirSliceModifier { angle, offset })
145    }
146
147    /// Apply a Mjolnir Shatter (Fragmented transition) to the view
148    fn mjolnir_shatter(
149        self,
150        pieces: u32,
151        force: f32,
152    ) -> ModifiedView<Self, MjolnirShatterModifier> {
153        self.modifier(MjolnirShatterModifier { pieces, force })
154    }
155
156    /// Mark this view as a Bifrost Bridge (Shared Element) for cross-view persistence
157    fn bifrost_bridge(self, id: impl Into<String>) -> ModifiedView<Self, BifrostBridgeModifier> {
158        self.modifier(BifrostBridgeModifier { id: id.into() })
159    }
160
161    /// Add a background color to this view
162    fn background(self, color: [f32; 4]) -> ModifiedView<Self, BackgroundModifier> {
163        self.modifier(BackgroundModifier { color })
164    }
165
166    /// Add padding to this view
167    fn padding(self, amount: f32) -> ModifiedView<Self, PaddingModifier> {
168        self.modifier(PaddingModifier { amount })
169    }
170
171    /// Set the opacity (alpha) of this view in the range [0.0, 1.0].
172    fn opacity(self, opacity: f32) -> ModifiedView<Self, OpacityModifier> {
173        self.modifier(OpacityModifier {
174            opacity: opacity.clamp(0.0, 1.0),
175        })
176    }
177
178    /// Override the foreground (text / icon) color of this view.
179    fn foreground_color(self, color: [f32; 4]) -> ModifiedView<Self, ForegroundColorModifier> {
180        self.modifier(ForegroundColorModifier { color })
181    }
182
183    /// Constrain this view to an explicit width and/or height.
184    /// Constrains the size of this view using fixed width/height values.
185    fn frame(self, width: Option<f32>, height: Option<f32>) -> ModifiedView<Self, FrameModifier> {
186        self.modifier(FrameModifier {
187            width,
188            height,
189            min_width: None,
190            max_width: None,
191            min_height: None,
192            max_height: None,
193            alignment: Alignment::Center,
194        })
195    }
196
197    /// Give this view a flex weight for proportional space distribution in stacks.
198    fn flex(self, weight: f32) -> ModifiedView<Self, FlexModifier> {
199        self.modifier(FlexModifier { weight })
200    }
201
202    /// Specify the grid placement configuration (column, row, column_span, row_span) for this view.
203    fn grid_placement(self, placement: GridPlacement) -> ModifiedView<Self, GridPlacementModifier> {
204        self.modifier(GridPlacementModifier { placement })
205    }
206
207    /// Overlay a view on top of this view, aligned and offset relative to it.
208    fn overlay<O: View + Clone + 'static>(
209        self,
210        overlay: O,
211        alignment: Alignment,
212        offset: [f32; 2],
213        on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
214    ) -> ModifiedView<Self, OverlayModifier> {
215        self.modifier(OverlayModifier {
216            overlay: overlay.erase(),
217            alignment,
218            offset,
219            on_dismiss,
220        })
221    }
222
223    /// Automatically add padding to avoid overlapping with platform safe areas (notches, bars).
224    fn safe_area_padding(self) -> ModifiedView<Self, SafeAreaModifier> {
225        self.modifier(SafeAreaModifier { ignores: false })
226    }
227
228    /// Explicitly ignore platform safe areas and draw into the margins.
229    fn ignores_safe_area(self) -> ModifiedView<Self, SafeAreaModifier> {
230        self.modifier(SafeAreaModifier { ignores: true })
231    }
232
233    /// Clip all child drawing to this view's bounds.
234    fn clip_to_bounds(self) -> ModifiedView<Self, ClipModifier> {
235        self.modifier(ClipModifier)
236    }
237
238    /// Draw a colored border around this view.
239    fn border(self, color: [f32; 4], width: f32) -> ModifiedView<Self, BorderModifier> {
240        self.modifier(BorderModifier { color, width })
241    }
242
243    /// Add elevation (shadow) to the view. Level determines the shadow depth.
244    fn elevation(self, level: f32) -> ModifiedView<Self, ElevationModifier> {
245        self.modifier(ElevationModifier { level })
246    }
247
248    /// Add a magnetic effect that pulls the view towards the cursor.
249    fn magnetic(self, radius: f32, intensity: f32) -> ModifiedView<Self, MagneticModifier> {
250        self.modifier(MagneticModifier { radius, intensity })
251    }
252
253    /// Add a ManiGlow (Lunar Illuminator) effect that glows near the cursor.
254    fn mani_glow(self, color: [f32; 4], radius: f32) -> ModifiedView<Self, ManiGlowModifier> {
255        self.modifier(ManiGlowModifier { color, radius })
256    }
257
258    /// Theme this view based on a specific memory layer.
259    fn memory_layer(self, layer: MemoryLayer) -> ModifiedView<Self, BifrostLayerModifier> {
260        self.modifier(BifrostLayerModifier { layer })
261    }
262
263    /// Enable Fafnir's Evolution: The component grows and glows as it is used.
264    fn fafnir_evolve(self, id: u64) -> ModifiedView<Self, FafnirModifier> {
265        self.modifier(FafnirModifier { id })
266    }
267
268    /// Enable Mimir's Intent: The component anticipates user interaction via pointer kinematics.
269    fn mimir_intent(self) -> ModifiedView<Self, MimirIntentModifier> {
270        self.modifier(MimirIntentModifier)
271    }
272
273    /// Enable Kvasir's Vibes: Subconscious telemetry representing cognitive complexity.
274    fn kvasir_vibes(self, complexity: f32) -> ModifiedView<Self, KvasirVibeModifier> {
275        self.modifier(KvasirVibeModifier { complexity })
276    }
277
278    /// Bestow Odin's Eye: Global omniscient observability layer.
279    fn odins_eye(self) -> ModifiedView<Self, OdinsEyeModifier> {
280        self.modifier(OdinsEyeModifier)
281    }
282
283    /// Trigger an action when the view appears
284    fn on_appear<F: Fn() + Send + Sync + 'static>(
285        self,
286        action: F,
287    ) -> ModifiedView<Self, LifecycleModifier> {
288        self.modifier(LifecycleModifier {
289            on_appear: Some(Arc::new(action)),
290            on_disappear: None,
291        })
292    }
293
294    /// Trigger an action when the view disappears
295    fn on_disappear<F: Fn() + Send + Sync + 'static>(
296        self,
297        action: F,
298    ) -> ModifiedView<Self, LifecycleModifier> {
299        self.modifier(LifecycleModifier {
300            on_appear: None,
301            on_disappear: Some(Arc::new(action)),
302        })
303    }
304
305    /// Trigger an action when the view is clicked
306    fn on_click<F: Fn() + Send + Sync + 'static>(
307        self,
308        action: F,
309    ) -> ModifiedView<Self, OnClickModifier> {
310        self.modifier(OnClickModifier {
311            action: Arc::new(action),
312        })
313    }
314
315    /// Trigger an action when the pointer enters the view bounds
316    fn on_pointer_enter<F: Fn() + Send + Sync + 'static>(
317        self,
318        action: F,
319    ) -> ModifiedView<Self, OnPointerEnterModifier> {
320        self.modifier(OnPointerEnterModifier {
321            action: Arc::new(action),
322        })
323    }
324
325    /// Trigger an action when the pointer leaves the view bounds
326    fn on_pointer_leave<F: Fn() + Send + Sync + 'static>(
327        self,
328        action: F,
329    ) -> ModifiedView<Self, OnPointerLeaveModifier> {
330        self.modifier(OnPointerLeaveModifier {
331            action: Arc::new(action),
332        })
333    }
334
335    /// Trigger an action when the pointer moves inside the view bounds
336    fn on_pointer_move<F: Fn(f32, f32) + Send + Sync + 'static>(
337        self,
338        action: F,
339    ) -> ModifiedView<Self, OnPointerMoveModifier> {
340        self.modifier(OnPointerMoveModifier {
341            action: Arc::new(action),
342        })
343    }
344
345    /// Trigger an action when the pointer is pressed down
346    fn on_pointer_down<F: Fn() + Send + Sync + 'static>(
347        self,
348        action: F,
349    ) -> ModifiedView<Self, OnPointerDownModifier> {
350        self.modifier(OnPointerDownModifier {
351            action: Arc::new(action),
352        })
353    }
354
355    /// Trigger an action when the pointer is released
356    fn on_pointer_up<F: Fn() + Send + Sync + 'static>(
357        self,
358        action: F,
359    ) -> ModifiedView<Self, OnPointerUpModifier> {
360        self.modifier(OnPointerUpModifier {
361            action: Arc::new(action),
362        })
363    }
364
365    /// Type-erase this view into AnyView
366    fn erase(self) -> AnyView
367    where
368        Self: Clone + 'static,
369    {
370        AnyView::new(self)
371    }
372
373    // =============================================================================
374    // ACCESSIBILITY
375    // =============================================================================
376
377    /// Return accessibility properties for this view.
378    /// Override to expose semantic role, label, state to assistive technology.
379    /// Default returns `None` (view is not explicitly accessible).
380    fn aria_properties(&self) -> Option<AriaProperties> {
381        None
382    }
383
384    /// Handle a keyboard navigation event.
385    /// Return true if consumed, false to bubble.
386    fn on_key_event(&self, _key: &str, _modifiers: KeyModifiers) -> bool {
387        false
388    }
389
390    /// Return keyboard shortcuts this view responds to.
391    fn key_shortcuts(&self) -> Vec<KeyShortcut> {
392        vec![]
393    }
394
395    /// Return true when this view's render output has changed since last build.
396    /// Default true for backward compatibility. Override for incremental skip.
397    /// When returns false, VDom::build skips this subtree entirely.
398    fn changed(&self) -> bool { true }
399
400    /// Stable identity for diff keying. Return None for anonymous views.
401    /// When Some(id), the VDOM layer uses this to match nodes across rebuilds,
402    /// enabling handler survival and incremental patch generation.
403    fn view_id(&self) -> Option<u64> { None }
404}
405
406// =============================================================================
407// ARIA PROPERTIES
408// =============================================================================
409
410/// Semantic role for assistive technology (WCAG 2.1 §4.1.2).
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
412pub enum AriaRole {
413    Alert,
414    Alertdialog,
415    Article,
416    Banner,
417    Button,
418    Checkbox,
419    Columnheader,
420    Combobox,
421    Complementary,
422    Contentinfo,
423    Dialog,
424    Form,
425    Grid,
426    Gridcell,
427    Heading,
428    Img,
429    Link,
430    List,
431    Listbox,
432    Listitem,
433    Main,
434    Menu,
435    Menubar,
436    Menuitem,
437    Menuitemcheckbox,
438    Menuitemradio,
439    Navigation,
440    None,
441    Note,
442    Option,
443    Presentation,
444    Progressbar,
445    Radio,
446    Radiogroup,
447    Region,
448    Row,
449    Rowgroup,
450    Rowheader,
451    Search,
452    Separator,
453    Slider,
454    Spinbutton,
455    Status,
456    Switch,
457    Tab,
458    Table,
459    Tablist,
460    Tabpanel,
461    Textbox,
462    Toolbar,
463    Tooltip,
464    Tree,
465    Treeitem,
466}
467
468/// Accessible properties for a view, describing its semantic role and state.
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct AriaProperties {
471    pub role: AriaRole,
472    pub label: String,
473    pub description: Option<String>,
474    pub value: Option<String>,
475    pub pressed: Option<bool>,
476    pub checked: Option<bool>,
477    pub expanded: Option<bool>,
478    pub disabled: bool,
479    pub hidden: bool,
480    pub level: Option<u8>,
481    pub shortcut: Option<String>,
482    pub focused: bool,
483    pub live: Option<String>,
484    pub atomic: bool,
485}
486
487impl AriaProperties {
488    pub fn new(role: AriaRole, label: impl Into<String>) -> Self {
489        Self {
490            role,
491            label: label.into(),
492            description: None,
493            value: None,
494            pressed: None,
495            checked: None,
496            expanded: None,
497            disabled: false,
498            hidden: false,
499            level: None,
500            shortcut: None,
501            focused: false,
502            live: None,
503            atomic: false,
504        }
505    }
506
507    pub fn description(mut self, d: impl Into<String>) -> Self {
508        self.description = Some(d.into());
509        self
510    }
511    pub fn value(mut self, v: impl Into<String>) -> Self {
512        self.value = Some(v.into());
513        self
514    }
515    pub fn checked(mut self, c: bool) -> Self {
516        self.checked = Some(c);
517        self
518    }
519    pub fn disabled(mut self, d: bool) -> Self {
520        self.disabled = d;
521        self
522    }
523    pub fn expanded(mut self, e: bool) -> Self {
524        self.expanded = Some(e);
525        self
526    }
527    pub fn level(mut self, l: u8) -> Self {
528        self.level = Some(l.clamp(1, 6));
529        self
530    }
531    pub fn shortcut(mut self, s: impl Into<String>) -> Self {
532        self.shortcut = Some(s.into());
533        self
534    }
535    pub fn focused(mut self, f: bool) -> Self {
536        self.focused = f;
537        self
538    }
539}
540
541// =============================================================================
542// KEYBOARD NAVIGATION
543// =============================================================================
544
545/// Modifier keys for keyboard events.
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
547pub struct KeyModifiers {
548    pub shift: bool,
549    pub ctrl: bool,
550    pub alt: bool,
551    pub meta: bool,
552}
553
554/// A keyboard shortcut binding.
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct KeyShortcut {
557    pub key: String,
558    pub modifiers: KeyModifiers,
559    pub description: String,
560}
561
562impl KeyShortcut {
563    pub fn new(key: impl Into<String>, desc: impl Into<String>) -> Self {
564        Self {
565            key: key.into(),
566            modifiers: KeyModifiers::default(),
567            description: desc.into(),
568        }
569    }
570    pub fn with_ctrl(mut self) -> Self {
571        self.modifiers.ctrl = true;
572        self
573    }
574    pub fn with_shift(mut self) -> Self {
575        self.modifiers.shift = true;
576        self
577    }
578    pub fn with_alt(mut self) -> Self {
579        self.modifiers.alt = true;
580        self
581    }
582    pub fn with_meta(mut self) -> Self {
583        self.modifiers.meta = true;
584        self
585    }
586}
587
588// =============================================================================
589// FOCUS MANAGEMENT
590// =============================================================================
591
592/// Unique ID for a focusable element.
593#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
594pub struct FocusableId(String);
595
596impl FocusableId {
597    /// Returns the inner string representation of the focusable ID.
598    pub fn as_str(&self) -> &str {
599        &self.0
600    }
601}
602
603impl From<&str> for FocusableId {
604    fn from(s: &str) -> Self {
605        Self(s.to_string())
606    }
607}
608impl From<String> for FocusableId {
609    fn from(s: String) -> Self {
610        Self(s)
611    }
612}
613
614/// Focus trap for confining Tab navigation (e.g., modals).
615#[derive(Debug, Clone)]
616pub struct FocusTrap {
617    pub id: FocusableId,
618    pub order: Vec<FocusableId>,
619    pub wrap: bool,
620}
621
622impl FocusTrap {
623    pub fn new(id: impl Into<FocusableId>, order: Vec<FocusableId>) -> Self {
624        Self {
625            id: id.into(),
626            order,
627            wrap: true,
628        }
629    }
630}
631
632/// Manages focus order, Tab/Shift+Tab navigation, and focus traps.
633#[derive(Debug, Default)]
634pub struct FocusManager {
635    order: Vec<FocusableId>,
636    order_set: HashSet<FocusableId>,
637    focused: Option<FocusableId>,
638    traps: Vec<FocusTrap>,
639}
640
641impl FocusManager {
642    pub fn new() -> Self {
643        Self::default()
644    }
645
646    pub fn register(&mut self, id: impl Into<FocusableId>) {
647        let id = id.into();
648        if self.order_set.insert(id.clone()) {
649            self.order.push(id);
650        }
651    }
652
653    pub fn unregister(&mut self, id: &FocusableId) {
654        if self.order_set.remove(id) {
655            self.order.retain(|x| x != id);
656        }
657        if self.focused.as_ref() == Some(id) {
658            self.focused = None;
659        }
660    }
661
662    pub fn focused(&self) -> Option<&FocusableId> {
663        self.focused.as_ref()
664    }
665
666    pub fn focus(&mut self, id: impl Into<FocusableId>) -> bool {
667        let id = id.into();
668        if self.order.contains(&id) || self.traps.iter().any(|t| t.order.contains(&id)) {
669            self.focused = Some(id);
670            true
671        } else {
672            false
673        }
674    }
675
676    pub fn focus_next(&mut self) -> Option<&FocusableId> {
677        let order = self.effective_order();
678        if order.is_empty() {
679            return None;
680        }
681        let idx = self
682            .focused
683            .as_ref()
684            .and_then(|f| order.iter().position(|x| x == f));
685        let next = match idx {
686            Some(i) if i + 1 < order.len() => &order[i + 1],
687            _ => &order[0],
688        };
689        self.focused = Some(next.clone());
690        self.focused.as_ref()
691    }
692
693    pub fn focus_prev(&mut self) -> Option<&FocusableId> {
694        let order = self.effective_order();
695        if order.is_empty() {
696            return None;
697        }
698        let idx = self
699            .focused
700            .as_ref()
701            .and_then(|f| order.iter().position(|x| x == f));
702        let prev = match idx {
703            Some(i) if i > 0 => &order[i - 1],
704            _ => &order[order.len() - 1],
705        };
706        self.focused = Some(prev.clone());
707        self.focused.as_ref()
708    }
709
710    pub fn push_trap(&mut self, trap: FocusTrap) -> FocusableId {
711        let id = trap.id.clone();
712        self.traps.push(trap);
713        id
714    }
715
716    pub fn pop_trap(&mut self) {
717        self.traps.pop();
718    }
719    pub fn trap_count(&self) -> usize {
720        self.traps.len()
721    }
722
723    fn effective_order(&self) -> &[FocusableId] {
724        self.traps
725            .last()
726            .map(|t| t.order.as_slice())
727            .unwrap_or(&self.order)
728    }
729}
730
731// =============================================================================
732// REDUCED MOTION
733// =============================================================================
734
735/// Detects OS-level reduced motion preference via [`AccessibilityPreferences`].
736///
737/// This delegates to `AccessibilityPreferences::detect_from_system()` which
738/// queries the correct OS API on macOS, Linux, and Windows.
739pub fn is_reduced_motion() -> bool {
740    AccessibilityPreferences::detect_from_system().reduce_motion
741}
742
743/// Returns effective animation duration (0.0 if reduced motion is active).
744pub fn effective_duration(secs: f32) -> f32 {
745    if is_reduced_motion() { 0.0 } else { secs }
746}
747
748/// An object-safe version of the View trait for type erasure.
749pub trait ErasedView: Send {
750    fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect);
751    fn name(&self) -> &'static str;
752    fn flex_weight_erased(&self) -> f32;
753    fn layout_erased(&self) -> Option<&dyn layout::LayoutView>;
754    fn grid_placement_erased(&self) -> Option<GridPlacement>;
755    fn clone_box(&self) -> Box<dyn ErasedView>;
756}
757
758impl<V: View + Clone + 'static> ErasedView for V {
759    fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect) {
760        self.render(renderer, rect);
761    }
762
763    fn name(&self) -> &'static str {
764        std::any::type_name::<V>()
765    }
766
767    fn flex_weight_erased(&self) -> f32 {
768        self.flex_weight()
769    }
770
771    fn layout_erased(&self) -> Option<&dyn layout::LayoutView> {
772        self.layout()
773    }
774
775    fn grid_placement_erased(&self) -> Option<GridPlacement> {
776        self.get_grid_placement()
777    }
778
779    fn clone_box(&self) -> Box<dyn ErasedView> {
780        Box::new(self.clone())
781    }
782}
783
784/// A view that memoizes its rendering based on a stable ID and data hash.
785/// The renderer can use this to skip re-rendering the sub-tree if the data hasn't changed.
786pub struct MemoView<V, F> {
787    id: u64,
788    data_hash: u64,
789    builder: F,
790    _v: std::marker::PhantomData<V>,
791}
792
793impl<V: View, F: Fn() -> V + Send + Sync> MemoView<V, F> {
794    /// Create a new MemoView with a stable ID and a data hash.
795    pub fn new(id: u64, data_hash: u64, builder: F) -> Self {
796        Self {
797            id,
798            data_hash,
799            builder,
800            _v: std::marker::PhantomData,
801        }
802    }
803}
804
805impl<V: View + 'static, F: Fn() -> V + Send + Sync + 'static> View for MemoView<V, F> {
806    type Body = Never;
807    fn body(self) -> Self::Body {
808        // SAFETY: `Never` is uninhabitable (zero variants). MemoView renders via
809        // `render()` using the memoized builder closure and never exposes a body.
810        unreachable!("MemoView does not have a body")
811    }
812
813    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
814        renderer.memoize(self.id, self.data_hash, &|r| {
815            let view = (self.builder)();
816            view.render(r, rect);
817        });
818    }
819}
820
821/// A type-erased View wrapper.
822pub struct AnyView {
823    inner: Box<dyn ErasedView>,
824}
825
826impl Clone for AnyView {
827    fn clone(&self) -> Self {
828        Self {
829            inner: self.inner.clone_box(),
830        }
831    }
832}
833
834impl AnyView {
835    pub fn new<V: View + Clone + 'static>(view: V) -> Self {
836        Self {
837            inner: Box::new(view),
838        }
839    }
840}
841
842impl View for AnyView {
843    type Body = Never;
844    fn body(self) -> Self::Body {
845        // SAFETY: `Never` is uninhabitable. AnyView is a type-erased wrapper that
846        // renders via `render_erased()` and never exposes a composable body.
847        unreachable!()
848    }
849
850    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
851        renderer.push_vnode(rect, self.inner.name());
852        self.inner.render_erased(renderer, rect);
853        renderer.pop_vnode();
854    }
855
856    fn flex_weight(&self) -> f32 {
857        self.inner.flex_weight_erased()
858    }
859
860    fn layout(&self) -> Option<&dyn layout::LayoutView> {
861        self.inner.layout_erased()
862    }
863
864    fn get_grid_placement(&self) -> Option<GridPlacement> {
865        self.inner.grid_placement_erased()
866    }
867}
868
869/// BifrostBridgeModifier enables shared-element transitions.
870/// When two views share the same Bifrost Bridge ID, the Sleipnir solver will
871/// interpolate their geometry and effects (blur, glow) during the transition.
872#[derive(Debug, Clone, PartialEq)]
873pub struct BifrostBridgeModifier {
874    pub id: String,
875}
876
877impl ViewModifier for BifrostBridgeModifier {
878    fn modify<V: View>(self, content: V) -> impl View {
879        ModifiedView::new(content, self)
880    }
881
882    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
883        // Register this element with the renderer for shared-element transition logic
884        renderer.register_shared_element(&self.id, rect);
885    }
886}
887
888/// MjolnirSliceModifier implements the "Geometric Slice" aesthetic.
889/// It uses a signed distance field (SDF) to clip the view along a sharp angled line.
890#[derive(Debug, Clone, Copy, PartialEq)]
891pub struct MjolnirSliceModifier {
892    pub angle: f32,
893    pub offset: f32,
894}
895
896impl ViewModifier for MjolnirSliceModifier {
897    fn modify<V: View>(self, content: V) -> impl View {
898        ModifiedView::new(content, self)
899    }
900
901    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
902        renderer.push_mjolnir_slice(self.angle, self.offset);
903    }
904
905    fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
906        renderer.pop_mjolnir_slice();
907    }
908}
909
910/// MjolnirShatterModifier implements the "Shattering" effect.
911/// It breaks the view into discrete geometric fragments that can be animated.
912#[derive(Debug, Clone, Copy, PartialEq)]
913pub struct MjolnirShatterModifier {
914    pub pieces: u32,
915    pub force: f32,
916}
917
918impl ViewModifier for MjolnirShatterModifier {
919    fn modify<V: View>(self, content: V) -> impl View {
920        ModifiedView::new(content, self)
921    }
922
923    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
924        // RADIAL SHATTER: Fragment the view into wedges
925        let pieces = self.pieces.max(1);
926        for i in 0..pieces {
927            let progress = i as f32 / pieces as f32;
928            let next_progress = (i + 1) as f32 / pieces as f32;
929
930            let angle_start = progress * 360.0;
931            let angle_end = next_progress * 360.0;
932
933            // Wedge slice: intersection of two half-planes
934            renderer.push_mjolnir_slice(angle_start, 0.0);
935            renderer.push_mjolnir_slice(angle_end + 180.0, 0.0);
936
937            // Apply radial force offset
938            let mid_angle = (angle_start + angle_end) / 2.0;
939            let rad = mid_angle.to_radians();
940            let dx = rad.cos() * self.force;
941            let dy = rad.sin() * self.force;
942
943            let shard_rect = Rect {
944                x: rect.x + dx,
945                y: rect.y + dy,
946                ..rect
947            };
948
949            view.render(renderer, shard_rect);
950
951            renderer.pop_mjolnir_slice();
952            renderer.pop_mjolnir_slice();
953        }
954    }
955}
956
957/// BifrostModifier implements the Cyberpunk "Frosted Glass" aesthetic.
958/// It triggers backdrop blurring and light scattering in the render pipeline.
959#[derive(Debug, Clone, Copy, PartialEq)]
960pub struct BifrostModifier {
961    pub blur: f32,
962    pub saturation: f32,
963    pub opacity: f32,
964    /// Fresnel strength multiplier. 0.0 = no fresnel, 1.0 = full.
965    pub fresnel_strength: f32,
966}
967
968impl ViewModifier for BifrostModifier {
969    fn modify<V: View>(self, content: V) -> impl View {
970        ModifiedView::new(content, self)
971    }
972
973    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
974        if renderer.is_over_budget() {
975            // Degrade: Use lower quality (half blur) if over budget
976            renderer.bifrost(rect, self.blur * 0.5, self.saturation, self.opacity);
977        } else {
978            renderer.bifrost(rect, self.blur, self.saturation, self.opacity);
979        }
980    }
981}
982
983/// A modifier that adds a background color to a view.
984#[derive(Debug, Clone, Copy, PartialEq)]
985pub struct BackgroundModifier {
986    pub color: [f32; 4],
987}
988
989impl ViewModifier for BackgroundModifier {
990    fn modify<V: View>(self, content: V) -> impl View {
991        ModifiedView::new(content, self)
992    }
993
994    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
995        renderer.fill_rect(rect, self.color);
996    }
997}
998
999/// A modifier that adds padding to a view.
1000#[derive(Debug, Clone, Copy, PartialEq)]
1001pub struct PaddingModifier {
1002    pub amount: f32,
1003}
1004
1005impl ViewModifier for PaddingModifier {
1006    fn modify<V: View>(self, content: V) -> impl View {
1007        ModifiedView::new(content, self)
1008    }
1009
1010    fn transform_rect(&self, rect: Rect) -> Rect {
1011        Rect {
1012            x: rect.x + self.amount,
1013            y: rect.y + self.amount,
1014            width: (rect.width - 2.0 * self.amount).max(0.0),
1015            height: (rect.height - 2.0 * self.amount).max(0.0),
1016        }
1017    }
1018
1019    fn transform_proposal(&self, mut proposal: SizeProposal) -> SizeProposal {
1020        if let Some(w) = proposal.width {
1021            proposal.width = Some((w - 2.0 * self.amount).max(0.0));
1022        }
1023        if let Some(h) = proposal.height {
1024            proposal.height = Some((h - 2.0 * self.amount).max(0.0));
1025        }
1026        proposal
1027    }
1028
1029    fn transform_size(&self, mut size: Size) -> Size {
1030        size.width += 2.0 * self.amount;
1031        size.height += 2.0 * self.amount;
1032        size
1033    }
1034}
1035
1036/// GungnirModifier implements the "Neon Glow" aesthetic.
1037/// It uses additive blending and multi-pass blurring to simulate glowing light.
1038#[derive(Debug, Clone, PartialEq)]
1039pub struct GungnirModifier {
1040    pub color: String,
1041    pub radius: f32,
1042    pub intensity: f32,
1043}
1044
1045impl ViewModifier for GungnirModifier {
1046    fn modify<V: View>(self, content: V) -> impl View {
1047        ModifiedView::new(content, self)
1048    }
1049
1050    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1051        // Neon Glow using Mode 1 in the Surtr pipeline
1052        renderer.stroke_rect(rect, [0.0, 1.0, 1.0, self.intensity], self.radius / 10.0);
1053    }
1054}
1055
1056/// GungnirPulseModifier implements a "breathing" neon effect.
1057#[derive(Debug, Clone, Copy, PartialEq)]
1058pub struct GungnirPulseModifier {
1059    pub color: [f32; 4],
1060    pub radius: f32,
1061    pub speed: f32,
1062}
1063
1064impl ViewModifier for GungnirPulseModifier {
1065    fn modify<V: View>(self, content: V) -> impl View {
1066        ModifiedView::new(content, self)
1067    }
1068
1069    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1070        let time = std::time::SystemTime::now()
1071            .duration_since(std::time::UNIX_EPOCH)
1072            .unwrap_or_default()
1073            .as_secs_f32();
1074
1075        // Mode 19: Dashed Border
1076        // Mode 20: 9-Slice / Patch Scaling
1077        let intensity = (time * self.speed).sin() * 0.5 + 0.5;
1078        let mut color = self.color;
1079        color[3] *= intensity;
1080
1081        // Mode 1 neon glow with dynamic intensity
1082        renderer.stroke_rect(rect, color, self.radius);
1083    }
1084}
1085
1086/// MagneticModifier makes a view "magnetic", subtly leaning towards or pulling the cursor.
1087#[derive(Debug, Clone, Copy, PartialEq)]
1088pub struct MagneticModifier {
1089    pub radius: f32,
1090    pub intensity: f32,
1091}
1092
1093impl ViewModifier for MagneticModifier {
1094    fn modify<V: View>(self, content: V) -> impl View {
1095        ModifiedView::new(content, self)
1096    }
1097
1098    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1099        let [px, py] = renderer.get_pointer_position();
1100        let center_x = rect.x + rect.width / 2.0;
1101        let center_y = rect.y + rect.height / 2.0;
1102
1103        let dx = px - center_x;
1104        let dy = py - center_y;
1105        let dist = (dx * dx + dy * dy).sqrt();
1106
1107        let mut offset_x = 0.0;
1108        let mut offset_y = 0.0;
1109
1110        if dist < self.radius && dist > 0.0 {
1111            let force = (1.0 - dist / self.radius) * self.intensity;
1112            offset_x = dx * force;
1113            offset_y = dy * force;
1114        }
1115
1116        let magnetic_rect = Rect {
1117            x: rect.x + offset_x,
1118            y: rect.y + offset_y,
1119            ..rect
1120        };
1121
1122        view.render(renderer, magnetic_rect);
1123    }
1124}
1125
1126/// ManiGlowModifier adds a soft, lunar-like cursor glow to a view.
1127/// Named after Máni, the personification of the Moon.
1128#[derive(Debug, Clone, Copy, PartialEq)]
1129pub struct ManiGlowModifier {
1130    pub color: [f32; 4],
1131    pub radius: f32,
1132}
1133
1134impl ViewModifier for ManiGlowModifier {
1135    fn modify<V: View>(self, content: V) -> impl View {
1136        ModifiedView::new(content, self)
1137    }
1138
1139    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1140        if crate::load_system_state().realm == Realm::Asgard {
1141            renderer.mani_glow(rect, self.color, self.radius);
1142        }
1143        view.render(renderer, rect);
1144    }
1145}
1146
1147/// BifrostLayerModifier themes a view based on its cognitive memory layer.
1148/// Episodic: Shifting aurora clouds.
1149/// Semantic: Crystalline gold.
1150/// Procedural: Heavy obsidian stone.
1151#[derive(Debug, Clone, Copy, PartialEq)]
1152pub struct BifrostLayerModifier {
1153    pub layer: MemoryLayer,
1154}
1155
1156impl ViewModifier for BifrostLayerModifier {
1157    fn modify<V: View>(self, content: V) -> impl View {
1158        ModifiedView::new(content, self)
1159    }
1160
1161    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1162        let realm = crate::load_system_state().realm;
1163        match self.layer {
1164            MemoryLayer::Episodic => {
1165                if realm == Realm::Asgard {
1166                    renderer.bifrost(rect, 40.0, 1.2, 0.7);
1167                } else {
1168                    renderer.fill_rect(rect, [0.1, 0.12, 0.15, 0.8]);
1169                }
1170            }
1171            MemoryLayer::Semantic => {
1172                if realm == Realm::Asgard {
1173                    renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0, 0.6);
1174                } else {
1175                    renderer.stroke_rect(rect, [0.4, 0.4, 0.4, 1.0], 1.5);
1176                }
1177            }
1178            MemoryLayer::Procedural => {
1179                renderer.fill_rect(rect, [0.05, 0.05, 0.07, 0.95]);
1180                let stroke_color = if realm == Realm::Asgard {
1181                    [0.3, 0.3, 0.3, 1.0]
1182                } else {
1183                    [0.2, 0.2, 0.2, 1.0]
1184                };
1185                renderer.stroke_rect(rect, stroke_color, 2.0);
1186            }
1187        }
1188        view.render(renderer, rect);
1189    }
1190}
1191
1192/// FafnirModifier enables self-evolving UI capabilities.
1193/// Named after Fafnir, the dragon who grows in power based on the gold he hoards.
1194/// In CVKG, 'Gold' is user attention/interaction.
1195#[derive(Debug, Clone, Copy, PartialEq)]
1196pub struct FafnirModifier {
1197    /// Unique ID for tracking this component's vitality across frames.
1198    pub id: u64,
1199}
1200
1201impl ViewModifier for FafnirModifier {
1202    fn modify<V: View>(self, content: V) -> impl View {
1203        ModifiedView::new(content, self)
1204    }
1205
1206    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1207        let state = crate::load_system_state();
1208        let vitality = state
1209            .get_component_state::<f32>(self.id)
1210            .map(|v| *v.read().unwrap())
1211            .unwrap_or(1.0);
1212
1213        // Calculate evolutionary growth factors
1214        // Max growth at vitality 5.0 (50% scale increase, strong glow)
1215        let growth = (vitality - 1.0).clamp(0.0, 4.0);
1216        let scale = 1.0 + growth * 0.12;
1217        let glow_intensity = growth * 0.25;
1218
1219        // Feed Fafnir: Register interaction to boost vitality
1220        let id = self.id;
1221        renderer.register_handler(
1222            "pointermove",
1223            std::sync::Arc::new(move |_| {
1224                crate::update_system_state(|s| {
1225                    let mut s = s.clone();
1226                    let v = s
1227                        .get_component_state::<f32>(id)
1228                        .map(|v| *v.read().unwrap())
1229                        .unwrap_or(1.0);
1230                    s.set_component_state(id, (v + 0.05).min(5.0)); // Cap at 5.0
1231                    s
1232                });
1233            }),
1234        );
1235
1236        if scale > 1.01 {
1237            renderer.push_transform([0.0, 0.0], [scale, scale], 0.0);
1238        }
1239
1240        if glow_intensity > 0.1 && state.realm == Realm::Asgard {
1241            renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0 * vitality, glow_intensity);
1242        }
1243
1244        view.render(renderer, rect);
1245
1246        if scale > 1.01 {
1247            renderer.pop_transform();
1248        }
1249    }
1250}
1251
1252/// MimirIntentModifier anticipates user movement and manifests holographic ghosts.
1253#[derive(Debug, Clone, Copy, PartialEq)]
1254pub struct MimirIntentModifier;
1255
1256impl ViewModifier for MimirIntentModifier {
1257    fn modify<V: View>(self, content: V) -> impl View {
1258        ModifiedView::new(content, self)
1259    }
1260
1261    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1262        let state = crate::load_system_state();
1263        let pos = state.last_pointer_pos;
1264        let vel = state.pointer_velocity;
1265
1266        // Calculate if the cursor is moving towards this rect
1267        let center = [rect.x + rect.width / 2.0, rect.y + rect.height / 2.0];
1268        let dx = center[0] - pos[0];
1269        let dy = center[1] - pos[1];
1270
1271        // Dot product of velocity and direction to center
1272        let dot = vel[0] * dx + vel[1] * dy;
1273        let speed_sq = vel[0] * vel[0] + vel[1] * vel[1];
1274        let dist_sq = dx * dx + dy * dy;
1275
1276        if dot > 0.0 && dist_sq < 250.0 * 250.0 && speed_sq > 0.5 && state.realm == Realm::Asgard {
1277            // Intent detected: render a subtle "ghost" reveal
1278            let intent_strength = (dot / (speed_sq.sqrt() * dist_sq.sqrt())).clamp(0.0, 1.0);
1279            renderer.stroke_rect(rect, [0.0, 0.9, 1.0, 0.3 * intent_strength], 1.5);
1280        }
1281
1282        view.render(renderer, rect);
1283    }
1284}
1285
1286/// KvasirVibeModifier renders a cognitive telemetry cloud representing agent complexity.
1287#[derive(Debug, Clone, Copy, PartialEq)]
1288pub struct KvasirVibeModifier {
1289    pub complexity: f32,
1290}
1291
1292impl ViewModifier for KvasirVibeModifier {
1293    fn modify<V: View>(self, content: V) -> impl View {
1294        ModifiedView::new(content, self)
1295    }
1296
1297    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1298        if crate::load_system_state().realm == Realm::Asgard {
1299            let t = renderer.elapsed_time();
1300            let c = self.complexity.clamp(0.0, 1.0);
1301
1302            // 1. Core Cognitive Cloud (Bifrost)
1303            // Turbulence increases with complexity
1304            let blur = 20.0 + c * 40.0;
1305            let turbulence_x = (t * (1.0 + c * 2.0)).sin() * 8.0 * c;
1306            let turbulence_y = (t * (0.8 + c * 1.5)).cos() * 5.0 * c;
1307            renderer.bifrost(
1308                rect.offset(turbulence_x, turbulence_y),
1309                blur,
1310                0.8 + c * 0.4,
1311                0.25,
1312            );
1313
1314            // 2. Synaptic Discharge (Gungnir pulses)
1315            if c > 0.2 {
1316                let pulse = (t * (3.0 + c * 5.0)).sin().abs() * c;
1317                let color = [0.0, 0.9, 1.0, 0.4 * pulse]; // Cyan synaptic pulse
1318                renderer.gungnir(rect, color, 12.0 + c * 24.0, 0.6 * pulse);
1319            }
1320
1321            // 3. Unstable Resonance (Magenta/Red shift for high complexity)
1322            if c > 0.7 {
1323                let instability = (t * 15.0).cos().abs() * (c - 0.7) * 3.3;
1324                let warning_color = [1.0, 0.0, 0.4, 0.12 * instability];
1325                renderer.fill_rect(rect, warning_color);
1326                renderer.stroke_rect(rect, [1.0, 0.0, 0.2, 0.45 * instability], 1.8);
1327            }
1328        }
1329        view.render(renderer, rect);
1330    }
1331}
1332
1333/// OdinsEyeModifier bestows omniscient observability over the entire scene graph.
1334#[derive(Debug, Clone, Copy, PartialEq)]
1335pub struct OdinsEyeModifier;
1336
1337impl ViewModifier for OdinsEyeModifier {
1338    fn modify<V: View>(self, content: V) -> impl View {
1339        ModifiedView::new(content, self)
1340    }
1341
1342    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1343        let state = crate::load_system_state();
1344        let t = renderer.elapsed_time();
1345
1346        // 1. Render Background content
1347        view.render(renderer, rect);
1348
1349        if state.realm == Realm::Asgard {
1350            // 2. Bestow Odin's Eye (Atmospheric Overlay)
1351            // Soft, large circular pulse representing the 'Eye'
1352            let eye_pulse = (t * 0.5).sin().abs() * 0.05;
1353            renderer.draw_radial_gradient(
1354                rect,
1355                [0.0, 0.6, 0.8, 0.08 + eye_pulse], // Inner Cyan
1356                [0.0, 0.0, 0.0, 0.0],              // Outer Black
1357            );
1358
1359            // 3. Hugin (Thought) Telemetry - Left Side
1360            let hugin_rect = Rect {
1361                x: rect.x + 20.0,
1362                y: rect.y + 40.0,
1363                width: 200.0,
1364                height: rect.height - 80.0,
1365            };
1366            renderer.draw_text(
1367                "HUGIN: THOUGHT",
1368                hugin_rect.x,
1369                hugin_rect.y,
1370                10.0,
1371                [0.0, 1.0, 1.0, 0.6],
1372            );
1373            for (i, thought) in state.thoughts.iter().rev().take(10).enumerate() {
1374                renderer.draw_text(
1375                    thought,
1376                    hugin_rect.x,
1377                    hugin_rect.y + 20.0 + i as f32 * 14.0,
1378                    9.0,
1379                    [1.0, 1.0, 1.0, 0.4],
1380                );
1381            }
1382
1383            // 4. Munin (Memory) Telemetry - Right Side
1384            let munin_rect = Rect {
1385                x: rect.x + rect.width - 220.0,
1386                y: rect.y + 40.0,
1387                width: 200.0,
1388                height: rect.height - 80.0,
1389            };
1390            renderer.draw_text(
1391                "MUNIN: MEMORY",
1392                munin_rect.x,
1393                munin_rect.y,
1394                10.0,
1395                [1.0, 0.84, 0.0, 0.6],
1396            );
1397            for (i, node) in state.nodes.iter().take(10).enumerate() {
1398                let opacity = (node.weight.min(1.0)) * 0.5;
1399                renderer.draw_text(
1400                    &node.id,
1401                    munin_rect.x,
1402                    munin_rect.y + 20.0 + i as f32 * 14.0,
1403                    9.0,
1404                    [1.0, 1.0, 1.0, opacity],
1405                );
1406            }
1407
1408            // 5. Omniscient Focus Beams (Gungnir Beams)
1409            if let Some(focus_id) = &state.odin_focus {
1410                // Visualize causal links to the focus node
1411                renderer.draw_text(
1412                    &format!("EYE FOCUS: {}", focus_id),
1413                    rect.x + rect.width / 2.0 - 50.0,
1414                    rect.y + 20.0,
1415                    12.0,
1416                    [0.0, 1.0, 1.0, 0.8],
1417                );
1418
1419                // In a real implementation, we would find the rect of the focus_id component.
1420                // For the 'Eye', we manifest a central beam of wisdom.
1421                renderer.gungnir(
1422                    Rect {
1423                        x: rect.x + rect.width / 2.0 - 1.0,
1424                        y: rect.y,
1425                        width: 2.0,
1426                        height: rect.height,
1427                    },
1428                    [0.0, 1.0, 1.0, 1.0],
1429                    20.0,
1430                    0.4,
1431                );
1432            }
1433        }
1434    }
1435}
1436
1437/// Sleipnir spring parameters for the physics solver
1438#[derive(Debug, Clone, Copy, PartialEq)]
1439pub struct SleipnirParams {
1440    pub stiffness: f32,
1441    pub damping: f32,
1442    pub mass: f32,
1443}
1444
1445impl SleipnirParams {
1446    pub fn snappy() -> Self {
1447        Self {
1448            stiffness: 230.0,
1449            damping: 22.0,
1450            mass: 1.0,
1451        }
1452    }
1453    pub fn fluid() -> Self {
1454        Self {
1455            stiffness: 170.0,
1456            damping: 26.0,
1457            mass: 1.0,
1458        }
1459    }
1460    pub fn heavy() -> Self {
1461        Self {
1462            stiffness: 90.0,
1463            damping: 20.0,
1464            mass: 1.0,
1465        }
1466    }
1467    pub fn bouncy() -> Self {
1468        Self {
1469            stiffness: 190.0,
1470            damping: 14.0,
1471            mass: 1.0,
1472        }
1473    }
1474}
1475
1476impl Default for SleipnirParams {
1477    fn default() -> Self {
1478        Self::fluid()
1479    }
1480}
1481
1482#[derive(Debug, Clone, Copy, PartialEq)]
1483struct SolverState {
1484    x: f32,
1485    v: f32,
1486}
1487
1488/// SleipnirSolver implements a 4th-order Runge-Kutta (RK4) integration for springs.
1489/// This provides superior stability for high-fidelity interactive motion.
1490#[derive(Debug, Clone, Copy, PartialEq)]
1491pub struct SleipnirSolver {
1492    params: SleipnirParams,
1493    target: f32,
1494    state: SolverState,
1495}
1496
1497impl SleipnirSolver {
1498    /// Create a new solver with a target value and starting state.
1499    pub fn new(params: SleipnirParams, target: f32, current: f32) -> Self {
1500        Self {
1501            params,
1502            target,
1503            state: SolverState { x: current, v: 0.0 },
1504        }
1505    }
1506
1507    /// Advance the simulation by dt seconds using RK4 integration.
1508    pub fn tick(&mut self, dt: f32) -> f32 {
1509        if dt <= 0.0 {
1510            return self.state.x;
1511        }
1512
1513        // Use a fixed time step for stability if dt is too large
1514        let mut remaining = dt;
1515        let step = 1.0 / 120.0;
1516
1517        while remaining > 0.0 {
1518            let d = remaining.min(step);
1519            self.step(d);
1520            remaining -= d;
1521        }
1522
1523        self.state.x
1524    }
1525
1526    fn step(&mut self, dt: f32) {
1527        let a = self.evaluate(self.state, 0.0, SolverState { x: 0.0, v: 0.0 });
1528        let b = self.evaluate(self.state, dt * 0.5, a);
1529        let c = self.evaluate(self.state, dt * 0.5, b);
1530        let d = self.evaluate(self.state, dt, c);
1531
1532        let dxdt = 1.0 / 6.0 * (a.x + 2.0 * (b.x + c.x) + d.x);
1533        let dvdt = 1.0 / 6.0 * (a.v + 2.0 * (b.v + c.v) + d.v);
1534
1535        self.state.x += dxdt * dt;
1536        self.state.v += dvdt * dt;
1537    }
1538
1539    fn evaluate(&self, initial: SolverState, dt: f32, d: SolverState) -> SolverState {
1540        let state = SolverState {
1541            x: initial.x + d.x * dt,
1542            v: initial.v + d.v * dt,
1543        };
1544        let force =
1545            -self.params.stiffness * (state.x - self.target) - self.params.damping * state.v;
1546        let mass = self.params.mass.max(0.001);
1547        SolverState {
1548            x: state.v,
1549            v: force / mass,
1550        }
1551    }
1552
1553    pub fn is_settled(&self) -> bool {
1554        (self.state.x - self.target).abs() < 0.001 && self.state.v.abs() < 0.001
1555    }
1556
1557    pub fn set_target(&mut self, target: f32) {
1558        self.target = target;
1559    }
1560
1561    pub fn current_value(&self) -> f32 {
1562        self.state.x
1563    }
1564}
1565
1566/// SleipnirModifier handles physics-based animations via the Sleipnir RK4 solver.
1567#[derive(Debug, Clone, PartialEq)]
1568pub struct SleipnirModifier {
1569    pub id: u64,
1570    pub target: f32,
1571    pub params: SleipnirParams,
1572}
1573
1574impl ViewModifier for SleipnirModifier {
1575    fn modify<V: View>(self, content: V) -> impl View {
1576        ModifiedView::new(content, self)
1577    }
1578
1579    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1580        let state = load_system_state();
1581
1582        // Try to fetch the solver from persistent state.
1583        let solver_lock_opt = state.get_component_state::<SleipnirSolver>(self.id);
1584
1585        let current_val;
1586
1587        if let Some(lock) = solver_lock_opt {
1588            // Found a solver. Tick it.
1589            let mut solver = lock.write().unwrap();
1590            solver.set_target(self.target);
1591            current_val = solver.tick(renderer.delta_time());
1592
1593            // If the solver hasn't settled yet, request another frame.
1594            if !solver.is_settled() {
1595                renderer.request_redraw();
1596            }
1597        } else {
1598            // First time seeing this ID. Initialize solver state.
1599            let solver = SleipnirSolver::new(
1600                self.params,
1601                self.target,
1602                self.target, // Initialize at target to avoid jump on first frame
1603            );
1604
1605            // Insert into registry for next frame.
1606            get_system_state().rcu(|old| {
1607                let mut new_state = (**old).clone();
1608                new_state.set_component_state(self.id, solver);
1609                new_state
1610            });
1611
1612            current_val = self.target;
1613        }
1614
1615        // Apply the solved value as a vertical translation.
1616        renderer.push_transform([0.0, current_val], [1.0, 1.0], 0.0);
1617        view.render(renderer, rect);
1618        renderer.pop_transform();
1619    }
1620}
1621
1622/// TransformModifier applies a 2D transform (translation, scale, rotation) to its child.
1623/// This modifier is "layout-neutral" and can be animated without re-running the layout engine.
1624#[derive(Debug, Clone, Copy, PartialEq)]
1625pub struct TransformModifier {
1626    pub translation: [f32; 2],
1627    pub scale: [f32; 2],
1628    pub rotation: f32,
1629}
1630
1631impl Default for TransformModifier {
1632    fn default() -> Self {
1633        Self::new()
1634    }
1635}
1636
1637impl TransformModifier {
1638    pub fn new() -> Self {
1639        Self {
1640            translation: [0.0, 0.0],
1641            scale: [1.0, 1.0],
1642            rotation: 0.0,
1643        }
1644    }
1645
1646    pub fn translate(mut self, x: f32, y: f32) -> Self {
1647        self.translation = [x, y];
1648        self
1649    }
1650
1651    pub fn scale(mut self, x: f32, y: f32) -> Self {
1652        self.scale = [x, y];
1653        self
1654    }
1655
1656    pub fn rotate(mut self, radians: f32) -> Self {
1657        self.rotation = radians;
1658        self
1659    }
1660}
1661
1662impl ViewModifier for TransformModifier {
1663    fn modify<V: View>(self, content: V) -> impl View {
1664        ModifiedView::new(content, self)
1665    }
1666
1667    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1668        renderer.push_transform(self.translation, self.scale, self.rotation);
1669        view.render(renderer, rect);
1670        renderer.pop_transform();
1671    }
1672}
1673
1674/// LifecycleModifier handles on_appear and on_disappear hooks.
1675
1676#[derive(Clone)]
1677pub struct LifecycleModifier {
1678    pub on_appear: Option<Arc<dyn Fn() + Send + Sync>>,
1679    pub on_disappear: Option<Arc<dyn Fn() + Send + Sync>>,
1680}
1681
1682impl ViewModifier for LifecycleModifier {
1683    fn modify<V: View>(self, content: V) -> impl View {
1684        ModifiedView::new(content, self)
1685    }
1686}
1687
1688/// OpacityModifier fades this view and all its descendants to the given alpha.
1689/// The renderer is expected to honour `push_opacity`/`pop_opacity` on the Renderer trait.
1690#[derive(Debug, Clone, Copy, PartialEq)]
1691pub struct OpacityModifier {
1692    pub opacity: f32,
1693}
1694
1695impl ViewModifier for OpacityModifier {
1696    fn modify<V: View>(self, content: V) -> impl View {
1697        ModifiedView::new(content, self)
1698    }
1699
1700    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1701        renderer.push_opacity(self.opacity);
1702    }
1703
1704    fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1705        renderer.pop_opacity();
1706    }
1707}
1708
1709/// OnClickModifier registers a click handler for this view.
1710#[derive(Clone)]
1711pub struct OnClickModifier {
1712    pub action: Arc<dyn Fn() + Send + Sync>,
1713}
1714
1715impl ViewModifier for OnClickModifier {
1716    fn modify<V: View>(self, content: V) -> impl View {
1717        ModifiedView::new(content, self)
1718    }
1719
1720    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1721        let action = self.action.clone();
1722        renderer.register_handler(
1723            "pointerclick",
1724            std::sync::Arc::new(move |event| {
1725                if let Event::PointerClick { .. } = event {
1726                    (action)();
1727                }
1728            }),
1729        );
1730    }
1731}
1732
1733/// OnPointerEnterModifier registers a pointer enter handler.
1734#[derive(Clone)]
1735pub struct OnPointerEnterModifier {
1736    pub action: Arc<dyn Fn() + Send + Sync>,
1737}
1738
1739impl ViewModifier for OnPointerEnterModifier {
1740    fn modify<V: View>(self, content: V) -> impl View {
1741        ModifiedView::new(content, self)
1742    }
1743
1744    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1745        let action = self.action.clone();
1746        renderer.register_handler(
1747            "pointerenter",
1748            std::sync::Arc::new(move |event| {
1749                if let Event::PointerEnter = event {
1750                    (action)();
1751                }
1752            }),
1753        );
1754    }
1755}
1756
1757/// OnPointerLeaveModifier registers a pointer leave handler.
1758#[derive(Clone)]
1759pub struct OnPointerLeaveModifier {
1760    pub action: Arc<dyn Fn() + Send + Sync>,
1761}
1762
1763impl ViewModifier for OnPointerLeaveModifier {
1764    fn modify<V: View>(self, content: V) -> impl View {
1765        ModifiedView::new(content, self)
1766    }
1767
1768    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1769        let action = self.action.clone();
1770        renderer.register_handler(
1771            "pointerleave",
1772            std::sync::Arc::new(move |event| {
1773                if let Event::PointerLeave = event {
1774                    (action)();
1775                }
1776            }),
1777        );
1778    }
1779}
1780
1781/// OnPointerMoveModifier registers a pointer move handler.
1782#[derive(Clone)]
1783pub struct OnPointerMoveModifier {
1784    pub action: Arc<dyn Fn(f32, f32) + Send + Sync>,
1785}
1786
1787impl ViewModifier for OnPointerMoveModifier {
1788    fn modify<V: View>(self, content: V) -> impl View {
1789        ModifiedView::new(content, self)
1790    }
1791
1792    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1793        let action = self.action.clone();
1794        renderer.register_handler(
1795            "pointermove",
1796            std::sync::Arc::new(move |event| {
1797                if let Event::PointerMove { x, y, .. } = event {
1798                    (action)(x, y);
1799                }
1800            }),
1801        );
1802    }
1803}
1804
1805/// OnPointerDownModifier registers a pointer down handler.
1806#[derive(Clone)]
1807pub struct OnPointerDownModifier {
1808    pub action: Arc<dyn Fn() + Send + Sync>,
1809}
1810
1811impl ViewModifier for OnPointerDownModifier {
1812    fn modify<V: View>(self, content: V) -> impl View {
1813        ModifiedView::new(content, self)
1814    }
1815
1816    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1817        let action = self.action.clone();
1818        renderer.register_handler(
1819            "pointerdown",
1820            std::sync::Arc::new(move |event| {
1821                if let Event::PointerDown { .. } = event {
1822                    (action)();
1823                }
1824            }),
1825        );
1826    }
1827}
1828
1829/// OnPointerUpModifier registers a pointer up handler.
1830#[derive(Clone)]
1831pub struct OnPointerUpModifier {
1832    pub action: Arc<dyn Fn() + Send + Sync>,
1833}
1834
1835impl ViewModifier for OnPointerUpModifier {
1836    fn modify<V: View>(self, content: V) -> impl View {
1837        ModifiedView::new(content, self)
1838    }
1839
1840    fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1841        let action = self.action.clone();
1842        renderer.register_handler(
1843            "pointerup",
1844            std::sync::Arc::new(move |event| {
1845                if let Event::PointerUp { .. } = event {
1846                    (action)();
1847                }
1848            }),
1849        );
1850    }
1851}
1852
1853/// ForegroundColorModifier overrides the foreground (text / icon) color inherited
1854/// by all descendants until another ForegroundColorModifier is encountered.
1855#[derive(Debug, Clone, Copy, PartialEq)]
1856pub struct ForegroundColorModifier {
1857    pub color: [f32; 4],
1858}
1859
1860impl ViewModifier for ForegroundColorModifier {
1861    fn modify<V: View>(self, content: V) -> impl View {
1862        ModifiedView::new(content, self)
1863    }
1864}
1865
1866/// ClipModifier restricts all child drawing to the view's layout rectangle.
1867/// The renderer must support `push_clip_rect`/`pop_clip_rect`.
1868#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1869pub struct ClipModifier;
1870
1871impl ViewModifier for ClipModifier {
1872    fn modify<V: View>(self, content: V) -> impl View {
1873        ModifiedView::new(content, self)
1874    }
1875
1876    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1877        renderer.push_clip_rect(rect);
1878    }
1879
1880    fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1881        renderer.pop_clip_rect();
1882    }
1883}
1884
1885/// BorderModifier draws a solid-color border around the view bounds.
1886#[derive(Debug, Clone, Copy, PartialEq)]
1887pub struct BorderModifier {
1888    pub color: [f32; 4],
1889    pub width: f32,
1890}
1891
1892impl ViewModifier for BorderModifier {
1893    fn modify<V: View>(self, content: V) -> impl View {
1894        ModifiedView::new(content, self)
1895    }
1896
1897    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1898        renderer.stroke_rect(rect, self.color, self.width);
1899    }
1900}
1901
1902// Primitive (leaf) views implement Never as body
1903#[doc(hidden)]
1904pub enum Never {}
1905
1906impl View for Never {
1907    type Body = Never;
1908    fn body(self) -> Never {
1909        // SAFETY: `Never` is an uninhabitable enum (zero variants), so this function
1910        // can never be called with a valid value and can never return.
1911        unreachable!()
1912    }
1913}
1914
1915/// EmptyView - A view that renders nothing and takes up no space.
1916#[derive(Debug, Clone, Copy, Default)]
1917pub struct EmptyView;
1918
1919impl View for EmptyView {
1920    type Body = Never;
1921    fn body(self) -> Self::Body {
1922        // SAFETY: `Never` is uninhabitable. EmptyView renders nothing and has no
1923        // composable body -- it registers zero paint commands.
1924        unreachable!()
1925    }
1926    fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1927    fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
1928        Size {
1929            width: 0.0,
1930            height: 0.0,
1931        }
1932    }
1933}
1934
1935/// A view that has been transformed by a modifier.
1936/// Section 4.3: "Each modifier implements ViewModifier and produces a ModifiedView<Inner, Self>."
1937#[derive(Clone)]
1938pub struct ModifiedView<V, M> {
1939    view: V,
1940    modifier: M,
1941}
1942
1943impl<V: View, M: ViewModifier> ModifiedView<V, M> {
1944    #[doc(hidden)]
1945    pub fn new(view: V, modifier: M) -> Self {
1946        Self { view, modifier }
1947    }
1948}
1949
1950impl<V: View, M: ViewModifier> View for ModifiedView<V, M> {
1951    type Body = ModifiedView<V::Body, M>;
1952
1953    fn body(self) -> Self::Body {
1954        ModifiedView {
1955            view: self.view.body(),
1956            modifier: self.modifier.clone(),
1957        }
1958    }
1959
1960    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1961        self.modifier.render_view(&self.view, renderer, rect);
1962    }
1963
1964    fn intrinsic_size(&self, renderer: &mut dyn Renderer, proposal: SizeProposal) -> Size {
1965        self.modifier.measure_view(&self.view, renderer, proposal)
1966    }
1967
1968    fn flex_weight(&self) -> f32 {
1969        self.modifier.child_flex_weight(&self.view)
1970    }
1971
1972    fn layout(&self) -> Option<&dyn layout::LayoutView> {
1973        self.modifier.layout().or_else(|| self.view.layout())
1974    }
1975
1976    fn get_grid_placement(&self) -> Option<GridPlacement> {
1977        self.modifier
1978            .get_grid_placement()
1979            .or_else(|| self.view.get_grid_placement())
1980    }
1981}
1982
1983pub trait ViewModifier: Send + Clone {
1984    fn modify<V: View>(self, content: V) -> impl View;
1985
1986    /// Returns the grid placement configuration if this modifier defines one.
1987    fn get_grid_placement(&self) -> Option<GridPlacement> {
1988        None
1989    }
1990
1991    /// Core rendering hook called before child views.
1992    fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1993
1994    /// Cleanup hook called after child views.
1995    fn post_render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1996
1997    /// Allows a modifier to completely override or wrap the rendering of its child.
1998    /// Default implementation performs a standard push -> transform -> render child -> pop sequence.
1999    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
2000        self.render(renderer, rect);
2001        let child_rect = self.transform_rect(rect);
2002        view.render(renderer, child_rect);
2003        self.post_render(renderer, rect);
2004    }
2005
2006    fn transform_rect(&self, rect: Rect) -> Rect {
2007        rect
2008    }
2009
2010    /// Allows a modifier to transform the layout proposal before it reaches the child.
2011    fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
2012        proposal
2013    }
2014
2015    /// Allows a modifier to transform the resulting size from the child.
2016    fn transform_size(&self, size: Size) -> Size {
2017        size
2018    }
2019
2020    /// Measure hook that coordinates size propagation.
2021    fn measure_view<V: View>(
2022        &self,
2023        view: &V,
2024        renderer: &mut dyn Renderer,
2025        proposal: SizeProposal,
2026    ) -> Size {
2027        let child_proposal = self.transform_proposal(proposal);
2028        let child_size = view.intrinsic_size(renderer, child_proposal);
2029        self.transform_size(child_size)
2030    }
2031
2032    /// Allows a modifier to override or pass through the child's flex weight.
2033    fn child_flex_weight<V: View>(&self, view: &V) -> f32 {
2034        view.flex_weight()
2035    }
2036
2037    fn layout(&self) -> Option<&dyn layout::LayoutView> {
2038        None
2039    }
2040}
2041
2042/// Captures the depth of every stack-pushing operation on the `Renderer`.
2043///
2044/// Created via `Renderer::snapshot_render_state()` and consumed by
2045/// `Renderer::restore_render_state()`. The renderer uses this to recover
2046/// from mid-render panics -- any items pushed beyond the snapshot point
2047/// are popped so sibling views drawn afterward don't inherit leaked
2048/// clip / opacity / transform / shadow / vnode / mjolnir-slice state.
2049///
2050/// Frame-scoped: the renderer resets all stacks in `begin_frame()` so a
2051/// snapshot taken in one frame is meaningless in another.
2052#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2053pub struct RenderStateSnapshot {
2054    pub clip_depth: u32,
2055    pub opacity_depth: u32,
2056    pub slice_depth: u32,
2057    pub shadow_depth: u32,
2058    pub transform_depth: u32,
2059    pub vnode_depth: u32,
2060}
2061
2062/// TelemetryData tracks real-time performance metrics for the GPU renderer.
2063#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
2064pub struct TelemetryData {
2065    pub frame_time_ms: f32,
2066    /// Total frame budget in milliseconds for the active policy.
2067    pub frame_budget_ms: f32,
2068    /// Remaining frame budget after the frame completed; negative means over budget.
2069    pub frame_budget_remaining_ms: f32,
2070    /// Remaining layout budget after layout completed; negative means the layout slice was exceeded.
2071    pub layout_budget_remaining_ms: f32,
2072    /// Whether the frame exceeded the total budget.
2073    pub frame_over_budget: bool,
2074    /// Whether the layout phase exceeded its budget slice.
2075    pub layout_over_budget: bool,
2076    /// 99th percentile frame time over the last window, used to detect tail latency.
2077    pub p99_frame_time_ms: f32,
2078    /// Statistical jitter (variance in frame timing).
2079    pub frame_jitter_ms: f32,
2080    /// Indicates if a hardware stall (DRAM refresh, thermal spike) was detected.
2081    pub hardware_stall_detected: bool,
2082
2083    // Pass timing
2084    pub input_time_ms: f32,
2085    pub state_flush_time_ms: f32,
2086    pub layout_time_ms: f32,
2087    pub draw_time_ms: f32,
2088    pub gpu_submit_time_ms: f32,
2089
2090    pub draw_calls: u32,
2091    pub vertices: u32,
2092
2093    /// Global Berserker Pipeline Intensity (0.0 - 1.0+)
2094    pub berserker_rage: f32,
2095
2096    // Memory breakdown
2097    pub vram_usage_mb: f32,
2098    pub vram_textures_mb: f32,
2099    pub vram_buffers_mb: f32,
2100    pub vram_pipelines_mb: f32,
2101    /// Indicates if the Mega-Atlas or VRAM pools are at capacity.
2102    pub vram_exhausted: bool,
2103}
2104
2105/// Configuration for render-loop frame timing and degradation strategies.
2106#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2107pub struct FrameBudget {
2108    /// Target frame time in milliseconds (default: 16.0 for 60FPS)
2109    pub target_ms: f32,
2110    /// If true, the renderer is allowed to dynamically skip non-critical effects
2111    /// (like heavy blurs or complex shadows) when the budget is exceeded.
2112    pub allow_degradation: bool,
2113}
2114
2115impl Default for FrameBudget {
2116    fn default() -> Self {
2117        Self {
2118            target_ms: 16.0,
2119            allow_degradation: true,
2120        }
2121    }
2122}
2123
2124/// The Renderer trait defines the atomic drawing operations for all CVKG backends.
2125/// This trait is object-safe and used by the View::render system.
2126/// # Implementation Requirements
2127/// 1. Coordinate system is origin-top-left (0,0) with Y increasing downwards.
2128/// 2. Colors are [R, G, B, A] in the [0.0, 1.0] range.
2129/// 3. All operations must be batchable by the underlying backend.
2130///    Trait providing timing information for the render loop.
2131pub trait ElapsedTime {
2132    /// Returns the cumulative time since the renderer started in seconds.
2133    fn elapsed_time(&self) -> f32;
2134
2135    /// Returns the time elapsed since the last frame in seconds.
2136    fn delta_time(&self) -> f32;
2137}
2138
2139/// The Renderer trait defines the atomic drawing operations for all CVKG backends.
2140/// This trait is object-safe and used by the View::render system.
2141/// # Implementation Requirements
2142/// 1. Coordinate system is origin-top-left (0,0) with Y increasing downwards.
2143/// 2. Colors are [R, G, B, A] in the [0.0, 1.0] range.
2144/// 3. All operations must be batchable by the underlying backend.
2145///
2146/// Sub-traits in `renderer/mod.rs` (RendererCore, RendererShapes, etc.) provide
2147/// logical groupings for consumer code. Backends implement this monolithic trait;
2148/// the sub-traits are aspirational documentation and NOT enforced as supertraits
2149/// to avoid method ambiguity (sub-traits re-declare the same methods).
2150pub trait Renderer: ElapsedTime + Send {
2151    /// Requests that the renderer redraws as soon as possible.
2152    /// Used for continuous animations.
2153    fn request_redraw(&mut self) {}
2154
2155    /// Returns true if the current frame is over the time budget.
2156    /// This can be used to skip expensive visual effects.
2157    fn is_over_budget(&self) -> bool {
2158        false
2159    }
2160
2161    // ── Filled shapes ────────────────────────────────────────────────────
2162    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]);
2163    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]);
2164    /// Fill an ellipse/circle that fits inside `rect`.
2165    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]);
2166
2167    /// Draw a background image that fills the entire rect.
2168    /// This is a convenience wrapper around `draw_image` for the common case
2169    /// of a full-rect background. The image must have been pre-warmed via
2170    /// `prewarm_vram` before the first frame.
2171    fn draw_background_image(&mut self, image_name: &str, rect: Rect) {
2172        Renderer::draw_image(self, image_name, rect);
2173    }
2174
2175    /// Fill a rounded rect with glass material for frosted backdrop effect.
2176    /// This is the proper way to render glass cards for macOS Tahoe-style blur.
2177    /// The blur_radius controls the intensity of the backdrop blur.
2178    fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
2179        // Default no-op implementation; GPU backend overrides
2180        let _ = (rect, radius, blur_radius);
2181    }
2182    /// Fill a rounded rect with glass material with explicit intensity control.
2183    /// `glass_intensity` ranges from 0.0 (solid) to 1.0 (full glass). Default: 1.0.
2184    fn fill_glass_rect_with_intensity(&mut self, rect: Rect, radius: f32, blur_radius: f32, glass_intensity: f32) {
2185        let _ = (rect, radius, blur_radius, glass_intensity);
2186    }
2187    /// Fill a rounded rect with glass material with explicit tint color and intensity.
2188    /// `tint_color` is the glass fill color (RGBA). `glass_intensity` ranges from 0.0 (solid) to 1.0 (full glass).
2189    fn fill_glass_rect_with_tint(&mut self, rect: Rect, radius: f32, blur_radius: f32, tint_color: [f32; 4], glass_intensity: f32) {
2190        // Default: delegate to intensity-only version using tint color as a simple fill
2191        let _ = (rect, radius, blur_radius, tint_color, glass_intensity);
2192    }
2193    /// Fill a rounded rect with glass material, modulated by touch pressure.
2194    /// `pressure` ranges from 0.0 (no touch) to 1.0 (full pressure).
2195    /// When pressure > 0, refraction distortion is scaled by pressure amount.
2196    /// Desktop stub: pressure is always 1.0 for mouse clicks, 0.0 otherwise.
2197    fn fill_glass_rect_with_pressure(&mut self, rect: Rect, radius: f32, blur_radius: f32, pressure: f32) {
2198        // Default: delegate to standard glass with intensity = pressure
2199        Renderer::fill_glass_rect_with_intensity(self, rect, radius, blur_radius, pressure);
2200    }
2201
2202    /// Fill a squircle (superellipse) for Apple-style icon silhouettes.
2203    /// `n` controls the squareness: 2.0 = rounded rect, 4.0 = classic squircle, higher = more square.
2204    fn fill_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4]) {
2205        // Default fallback to rounded rect
2206        Renderer::fill_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color);
2207    }
2208
2209    /// Stroke a squircle (superellipse) outline.
2210    fn stroke_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4], stroke_width: f32) {
2211        Renderer::stroke_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color, stroke_width);
2212    }
2213
2214    /// Draw a focus ring around a rect (for keyboard navigation accessibility).
2215    /// `offset` is the gap between the rect and the ring, `width` is the ring thickness.
2216    fn draw_focus_ring(&mut self, rect: Rect, radius: f32, offset: f32, width: f32, color: [f32; 4]) {
2217        // Default fallback to a stroked rounded rect
2218        let ring_rect = Rect {
2219            x: rect.x - offset,
2220            y: rect.y - offset,
2221            width: rect.width + 2.0 * offset,
2222            height: rect.height + 2.0 * offset,
2223        };
2224        Renderer::stroke_rounded_rect(self, ring_rect, radius + offset, color, width);
2225    }
2226
2227    /// Draw a high-fidelity 3D cube inside the given rectangle using specialized shader logic.
2228    /// `rotation` is [pitch, yaw, roll] in radians.
2229    fn draw_3d_cube(&mut self, _rect: Rect, _color: [f32; 4], _rotation: [f32; 3]) {}
2230
2231    // ── Stroked shapes ───────────────────────────────────────────────────
2232    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2233    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32);
2234    /// Stroke an ellipse/circle that fits inside `rect`.
2235    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2236    /// Draw a straight line from (x1,y1) to (x2,y2).
2237    fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32);
2238    /// Fill a polygon defined by a set of vertices.
2239    fn fill_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4]) {}
2240    /// Stroke a polygon defined by a set of vertices.
2241    fn stroke_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4], _stroke_width: f32) {}
2242
2243    // ── Text ─────────────────────────────────────────────────────────────
2244    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
2245        let span = cvkg_runic_text::TextSpan::new(
2246            text,
2247            cvkg_runic_text::TextStyle {
2248                family: "Inter".to_string(),
2249                font_size: size,
2250                color: [(color[0]*255.0) as u8, (color[1]*255.0) as u8, (color[2]*255.0) as u8, (color[3]*255.0) as u8],
2251                fallback_families: vec![
2252                    "SF Pro".to_string(),
2253                    "SF Pro Text".to_string(),
2254                    "Helvetica Neue".to_string(),
2255                    "Helvetica".to_string(),
2256                    "Arial".to_string(),
2257                    "sans-serif".to_string(),
2258                ],
2259                ..Default::default()
2260            },
2261        );
2262        if let Some(shaped) = Renderer::shape_rich_text(
2263            self,
2264            &[span],
2265            None,
2266            cvkg_runic_text::TextAlign::Start,
2267            cvkg_runic_text::TextOverflow::Visible,
2268        ) {
2269            Renderer::draw_shaped_text(self, &shaped, x, y);
2270        }
2271    }
2272
2273    /// Measure the width and height of the specified text.
2274    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2275        let span = cvkg_runic_text::TextSpan::new(
2276            text,
2277            cvkg_runic_text::TextStyle {
2278                family: "Inter".to_string(),
2279                font_size: size,
2280                fallback_families: vec![
2281                    "SF Pro".to_string(),
2282                    "SF Pro Text".to_string(),
2283                    "Helvetica Neue".to_string(),
2284                    "Helvetica".to_string(),
2285                    "Arial".to_string(),
2286                    "sans-serif".to_string(),
2287                ],
2288                ..Default::default()
2289            },
2290        );
2291        if let Some(shaped) = Renderer::shape_rich_text(
2292            self,
2293            &[span],
2294            None,
2295            cvkg_runic_text::TextAlign::Start,
2296            cvkg_runic_text::TextOverflow::Visible,
2297        ) {
2298            let scale = self.text_scale_factor().max(1.0);
2299            (shaped.width / scale, shaped.height / scale)
2300        } else {
2301            (0.0, 0.0)
2302        }
2303    }
2304
2305    /// Return the baseline offset (ascent) for the given text and size.
2306    /// This is the distance from the text origin (y in draw_text) to the baseline.
2307    /// Default returns 0.0; override in renderers that support text shaping.
2308    fn measure_text_baseline(&mut self, text: &str, size: f32) -> f32 {
2309        let span = cvkg_runic_text::TextSpan::new(
2310            text,
2311            cvkg_runic_text::TextStyle {
2312                family: "Inter".to_string(),
2313                font_size: size,
2314                fallback_families: vec![
2315                    "SF Pro".to_string(),
2316                    "SF Pro Text".to_string(),
2317                    "Helvetica Neue".to_string(),
2318                    "Helvetica".to_string(),
2319                    "Arial".to_string(),
2320                    "sans-serif".to_string(),
2321                ],
2322                ..Default::default()
2323            },
2324        );
2325        if let Some(shaped) = Renderer::shape_rich_text(
2326            self,
2327            &[span],
2328            None,
2329            cvkg_runic_text::TextAlign::Start,
2330            cvkg_runic_text::TextOverflow::Visible,
2331        ) {
2332            shaped.ascent / self.text_scale_factor().max(1.0)
2333        } else {
2334            0.0
2335        }
2336    }
2337
2338    /// Scale factor used by text measurement helpers.
2339    ///
2340    /// Renderers that shape text in device pixels should return their current
2341    /// device scale so `measure_text` and `measure_text_baseline` stay in logical pixels.
2342    fn text_scale_factor(&self) -> f32 {
2343        1.0
2344    }
2345
2346    fn shape_rich_text(
2347        &mut self,
2348        _spans: &[cvkg_runic_text::TextSpan],
2349        _max_width: Option<f32>,
2350        _align: cvkg_runic_text::TextAlign,
2351        _overflow: cvkg_runic_text::TextOverflow,
2352    ) -> Option<cvkg_runic_text::ShapedText> {
2353        None
2354    }
2355
2356    fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {}
2357
2358    // ── Images & textures ────────────────────────────────────────────────
2359    /// Draw a texture (GPU-side) at the specified rect.
2360    fn draw_texture(&mut self, _texture_id: u32, _rect: Rect) {}
2361    /// Draw an image asset by name or path.
2362    fn draw_image(&mut self, _image_name: &str, _rect: Rect) {}
2363    /// Load an image asset from memory.
2364    fn load_image(&mut self, _name: &str, _data: &[u8]) {}
2365    /// Pre-warm the renderer with assets. Implementations can use this
2366    /// to populate texture atlases or warm up shader caches.
2367    fn prewarm_vram(&mut self, _assets: Vec<(String, Vec<u8>)>) {}
2368
2369    /// Get the current pointer (mouse/touch) position.
2370    fn get_pointer_position(&self) -> [f32; 2] {
2371        [0.0, 0.0]
2372    }
2373
2374    // ── Data Visualization ───────────────────────────────────────────────
2375    /// Upload raw float data as a GPU texture for heatmap rendering.
2376    fn upload_data_texture(&mut self, _id: &str, _data: &[f32], _width: u32, _height: u32) {}
2377    /// Draw a heatmap using a previously uploaded data texture.
2378    fn draw_heatmap(&mut self, _texture_id: &str, _rect: Rect, _palette: &str) {}
2379
2380    // ── 3D Objects ───────────────────────────────────────────────────────
2381    /// Draw a 3D mesh.
2382    fn draw_mesh(&mut self, _mesh: &Mesh, _color: [f32; 4], _transform: glam::Mat4) {}
2383
2384    /// Draw a 3D mesh with full material and transform support.
2385    fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
2386
2387    /// Set the 3D camera for perspective/orthographic projection.
2388    /// If not called, rendering defaults to the 2D orthographic projection.
2389    fn set_camera_3d(&mut self, _camera: &Camera3D) {}
2390
2391    /// Push a 3D transform onto the transform stack.
2392    /// All subsequent drawing is affected until `pop_transform_3d`.
2393    fn push_transform_3d(&mut self, _transform: &Transform3D) {}
2394
2395    /// Pop the most recently pushed 3D transform.
2396    fn pop_transform_3d(&mut self) {}
2397
2398    /// Render a 3D scene graph node. Reads position_3d, rotation_3d, scale_3d
2399    /// from the node and emits the appropriate draw call.
2400    /// Default implementation is a no-op; 3D renderers override this.
2401    ///
2402    /// `position`: [x, y, z] world-space position
2403    /// `rotation`: [x, y, z, w] quaternion rotation
2404    /// `scale`: [x, y, z] scale factors
2405    /// `color`: [r, g, b, a] base color for unlit rendering
2406    fn render_scene_node_3d(
2407        &mut self,
2408        _position: [f32; 3],
2409        _rotation: [f32; 4],
2410        _scale: [f32; 3],
2411        _color: [f32; 4],
2412        _meshes: &[Mesh],
2413    ) {
2414        // Default no-op: 2D renderers ignore 3D scene nodes
2415    }
2416
2417    /// Draw a linear gradient between two colors at the specified angle.
2418    fn draw_linear_gradient(
2419        &mut self,
2420        _rect: Rect,
2421        _start_color: [f32; 4],
2422        _end_color: [f32; 4],
2423        _angle: f32,
2424    ) {
2425    }
2426    /// Draw a radial gradient between two colors.
2427    fn draw_radial_gradient(
2428        &mut self,
2429        _rect: Rect,
2430        _inner_color: [f32; 4],
2431        _outer_color: [f32; 4],
2432    ) {
2433    }
2434    /// Draw a high-fidelity drop shadow for a rounded rectangle.
2435    fn draw_drop_shadow(
2436        &mut self,
2437        _rect: Rect,
2438        _radius: f32,
2439        _color: [f32; 4],
2440        _blur: f32,
2441        _spread: f32,
2442    ) {
2443    }
2444    /// Draw a dashed border for a rounded rectangle.
2445    fn stroke_dashed_rounded_rect(
2446        &mut self,
2447        _rect: Rect,
2448        _radius: f32,
2449        _color: [f32; 4],
2450        _width: f32,
2451        _dash: f32,
2452        _gap: f32,
2453    ) {
2454    }
2455    /// Draw a 9-slice / patch scaled image.
2456    fn draw_9slice(
2457        &mut self,
2458        _image_name: &str,
2459        _rect: Rect,
2460        _left: f32,
2461        _top: f32,
2462        _right: f32,
2463        _bottom: f32,
2464    ) {
2465    }
2466
2467    // ── Clipping ─────────────────────────────────────────────────────────
2468    /// Push a clip rectangle.  All subsequent drawing is clipped to `rect`.
2469    /// Implementations that do not support clipping may ignore this call.
2470    fn push_clip_rect(&mut self, _rect: Rect) {}
2471    /// Pop the most recently pushed clip rectangle.
2472    fn pop_clip_rect(&mut self) {}
2473    /// Get the current clip rectangle in screen coordinates.
2474    /// Returns a rect covering the entire screen if no clip is active.
2475    fn current_clip_rect(&self) -> Rect {
2476        Rect::new(-10000.0, -10000.0, 20000.0, 20000.0)
2477    }
2478
2479    // ── Global opacity ───────────────────────────────────────────────────
2480    /// Set a global opacity multiplier applied to all subsequent draw calls
2481    /// until `pop_opacity` is called.  `opacity` is in [0.0, 1.0].
2482    fn push_opacity(&mut self, _opacity: f32) {}
2483    /// Restore the previous opacity level.
2484    fn pop_opacity(&mut self) {}
2485
2486    // ── Berserker Pipeline State ─────────────────────────────────────────
2487    fn set_theme(&mut self, _theme: ColorTheme) {}
2488    fn set_rage(&mut self, _rage: f32) {}
2489    fn set_berserker_mode(&mut self, _state: BerserkerMode) {}
2490    fn trigger_shatter_event(&mut self, _origin: [f32; 2], _force: f32) {}
2491    /// Set the fireball position for dynamic glass specular highlights.
2492    fn set_fireball_pos(&mut self, _pos: [f32; 2]) {}
2493    /// Set the desktop scene preset (Aurora, Void, Nebula, Glitch, Yggdrasil).
2494    fn set_scene(&mut self, _scene: &str) {}
2495    /// Set the desktop scene by name. Case-insensitive.
2496    /// Supports: "aurora", "void", "nebula", "glitch", "yggdrasil".
2497    /// Aliases: "empty", "none", "blank" → Void.
2498    fn set_scene_by_name(&mut self, name: &str) {
2499        if let Some(preset) = resolve_scene_by_name(name) {
2500            Renderer::set_scene_preset(self, preset);
2501        }
2502    }
2503
2504    // ── Export & Print ───────────────────────────────────────────────────
2505    /// Capture the current frame as a PNG byte buffer.
2506    fn capture_png(&mut self) -> Vec<u8> {
2507        Vec::new()
2508    }
2509    /// Trigger a native print dialog or spooling operation.
2510    fn print(&mut self) {}
2511
2512    fn set_scene_preset(&mut self, _preset: u32) {}
2513
2514    // ── Cyberpunk Effects ────────────────────────────────────────────────
2515    /// Apply a Bifrost (Frosted Glass) effect to the specified rect.
2516    fn bifrost(&mut self, _rect: Rect, _blur: f32, _saturation: f32, _opacity: f32) {}
2517    /// Apply a Gungnir (Neon Glow) effect to the specified rect.
2518    fn gungnir(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2519    /// Soft glow variant -- half the intensity of gungnir(). Use for hover highlights.
2520    fn gungnir_soft(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2521    /// Set the default background color for the canvas (RGBA).
2522    /// Used when the app does not draw its own background.
2523    fn set_default_background_color(&mut self, _color: [f32; 4]) {}
2524    /// Apply a ManiGlow (Lunar Illuminator) effect.
2525    fn mani_glow(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32) {}
2526    /// Push a Mjolnir Slice (geometric clipping).
2527    fn push_mjolnir_slice(&mut self, _angle: f32, _offset: f32) {}
2528    fn pop_mjolnir_slice(&mut self) {}
2529    /// Execute a render function with memoization.
2530    /// If the renderer supports caching and the `id` + `data_hash` match a previous run,
2531    /// it may replay cached commands instead of executing the function.
2532    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer));
2533    /// Capture current renderer stack depths for later panic recovery.
2534    /// The default implementation returns `RenderStateSnapshot::default()`,
2535    /// which is safe but does nothing useful -- backends with stack state
2536    /// must override this to record their actual depths.
2537    fn snapshot_render_state(&self) -> RenderStateSnapshot {
2538        RenderStateSnapshot::default()
2539    }
2540    /// Restore renderer stack state by popping items pushed beyond the
2541    /// snapshot point. Used by `ErrorBoundary` to recover from mid-render
2542    /// panics so sibling views don't inherit leaked clip/opacity/transform
2543    /// state. Idempotent: a no-op if stacks are already at or below the
2544    /// snapshot depths. Default implementation is a no-op for backends
2545    /// that have no stack state.
2546    fn restore_render_state(&mut self, _snap: RenderStateSnapshot) {}
2547    /// Apply a Mjolnir Shatter effect (fragmentation) to the specified rect.
2548    fn mjolnir_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2549    fn mjolnir_fluid_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2550    fn draw_mjolnir_bolt(&mut self, _from: [f32; 2], _to: [f32; 2], _color: [f32; 4]) {}
2551
2552    // ── Futuristic UI Compute & Volumetric ───────────────────────────────
2553    /// Dispatches a burst of GPU particles (e.g. fireworks, data streams).
2554    fn dispatch_particles(
2555        &mut self,
2556        _origin: [f32; 2],
2557        _count: u32,
2558        _effect_type: &str,
2559        _color: [f32; 4],
2560    ) {
2561    }
2562
2563    /// Draws a volumetric hologram into the specified bounding rectangle.
2564    fn draw_hologram(&mut self, _rect: Rect, _hologram_id: &str, _time: f32) {}
2565
2566    // ── Accessibility (ShieldWall) ───────────────────────────────────────
2567    fn set_aria_role(&mut self, _role: &str) {}
2568    fn set_aria_label(&mut self, _label: &str) {}
2569    fn set_aria_valuemin(&mut self, _min: f32) {}
2570    fn set_aria_valuemax(&mut self, _max: f32) {}
2571    fn set_aria_valuenow(&mut self, _now: f32) {}
2572
2573    /// Register a shared element for Bifrost Bridge transitions.
2574    fn register_shared_element(&mut self, _id: &str, _rect: Rect) {}
2575
2576    /// Set a unique key for the current VDOM node to ensure stable identity during diffing.
2577    fn set_key(&mut self, _key: &str) {}
2578
2579    // ── Telemetry ────────────────────────────────────────────────────────
2580    /// Get real-time performance telemetry.
2581    fn get_telemetry(&self) -> TelemetryData {
2582        TelemetryData::default()
2583    }
2584
2585    // ── GPU State Management ─────────────────────────────────────────────
2586    /// Push a shadow state to the stack. All following draw calls will have this shadow.
2587    fn push_shadow(&mut self, _radius: f32, _color: [f32; 4], _offset: [f32; 2]) {}
2588    /// Pop the last shadow state from the stack.
2589    fn pop_shadow(&mut self) {}
2590
2591    // ── VDOM & Scene Graph ───────────────────────────────────────────────
2592    /// Push a Virtual DOM node onto the stack for hierarchy tracking.
2593    fn push_vnode(&mut self, _rect: Rect, _name: &'static str) {}
2594    /// Pop the current Virtual DOM node from the stack.
2595    fn pop_vnode(&mut self) {}
2596    /// Register an event handler for the current VDOM node.
2597    fn register_handler(
2598        &mut self,
2599        _event_type: &str,
2600        _handler: std::sync::Arc<dyn Fn(Event) + Send + Sync>,
2601    ) {
2602    }
2603
2604    // ── Z-Index & Depth ──────────────────────────────────────────────────
2605    /// Set the current Z-index for depth sorting.
2606    /// Higher values appear closer to the viewer.
2607    fn set_z_index(&mut self, _z: f32) {}
2608    /// Get the current Z-index.
2609    fn get_z_index(&self) -> f32 {
2610        0.0
2611    }
2612
2613    // ── Vector Graphics ──────────────────────────────────────────────────
2614    /// Load an SVG model from raw bytes.
2615    fn load_svg(&mut self, _name: &str, _svg_data: &[u8]) {}
2616    /// Draw a pre-loaded SVG model.
2617    fn draw_svg(&mut self, _name: &str, _rect: Rect) {}
2618    /// Draw a pre-loaded SVG model with a per-instance animation time offset.
2619    /// The offset shifts the animation phase, allowing multiple draws of the same
2620    /// SVG to animate independently. Default delegates to draw_svg (no offset).
2621    fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, _animation_time_offset: f32) {
2622        Renderer::draw_svg(self, name, rect);
2623    }
2624    /// Draw a pre-loaded SVG model with explicit draw_order for z-sorting.
2625    /// draw_order=200 renders above UI chrome (draw_order=0).
2626    fn draw_svg_with_order(&mut self, name: &str, rect: Rect, _draw_order: i32) {
2627        Renderer::draw_svg(self, name, rect);
2628    }
2629    /// Serialize a pre-loaded SVG model back to SVG XML markup.
2630    /// Returns the serialized SVG string, or an error if the model is not loaded
2631    /// or serialization is not supported by this renderer.
2632    fn serialize_svg(&mut self, _name: &str) -> Result<String, String> {
2633        Err("SVG serialization not supported by this renderer".into())
2634    }
2635    /// Apply an SVG filter to a pre-loaded SVG model by filter element ID.
2636    /// The filter is evaluated and the result composited back into the SVG.
2637    /// Returns the filtered SVG as XML, or an error if not supported.
2638    fn apply_svg_filter(
2639        &mut self,
2640        _name: &str,
2641        _filter_id: &str,
2642        _region: Rect,
2643    ) -> Result<String, String> {
2644        Err("SVG filter not supported by this renderer".into())
2645    }
2646
2647    // ── GPU Transformations ──────────────────────────────────────────────
2648    /// Push a 2D transform (translation, scale, rotation) onto the stack.
2649    /// This transform should be applied to all subsequent draw calls until popped.
2650    /// Transform-only animations use this to avoid re-triggering the layout engine.
2651    fn push_transform(&mut self, _translation: [f32; 2], _scale: [f32; 2], _rotation: f32) {}
2652    /// Push a raw 2D affine transform matrix [a, b, c, d, e, f] corresponding to
2653    /// [m11, m12, m21, m22, tx, ty].
2654    fn push_affine(&mut self, _transform: [f32; 6]) {}
2655    /// Pop the last 2D transform from the stack.
2656    fn pop_transform(&mut self) {}
2657    /// Return the resolved layout bounds for a specific node ID if it exists.
2658    fn query_layout(&self, _node_id: scene_graph::NodeId) -> Option<Rect> {
2659        None
2660    }
2661    /// Enable or disable the layout debug overlay (bounds, padding, margin).
2662    fn set_debug_layout(&mut self, _enabled: bool) {}
2663    /// Check if the layout debug overlay is currently enabled.
2664    fn get_debug_layout(&self) -> bool {
2665        false
2666    }
2667
2668    // ── Material Routing ─────────────────────────────────────────────────
2669    /// Set the active material for subsequent draw calls.
2670    /// Controls which pass a draw call is routed to in the multi-pass pipeline.
2671    fn set_material(&mut self, _material: crate::material::DrawMaterial) {}
2672    /// Return the currently active material (defaults to Opaque).
2673    fn current_material(&self) -> crate::material::DrawMaterial {
2674        crate::material::DrawMaterial::Opaque
2675    }
2676
2677    // ── Vili Interaction Paradigm ──────────────────────────────────────────
2678    /// Compute the user's velocity/intent vector.
2679    fn mimir_intent(&self) -> [f32; 2] {
2680        [0.0, 0.0]
2681    }
2682    /// Calculate magnetic coordinate warp towards an anchor.
2683    fn magnetic_warp(&self, pointer: [f32; 2], anchor_rect: Rect, strength: f32) -> [f32; 2] {
2684        if strength <= 0.0 {
2685            return pointer;
2686        }
2687        let cx = anchor_rect.x + anchor_rect.width / 2.0;
2688        let cy = anchor_rect.y + anchor_rect.height / 2.0;
2689        let dx = pointer[0] - cx;
2690        let dy = pointer[1] - cy;
2691        let dist = (dx * dx + dy * dy).sqrt();
2692        let radius = 120.0;
2693        if dist < radius && dist > 0.0 {
2694            let force = (1.0 - dist / radius) * strength;
2695            [pointer[0] - dx * force, pointer[1] - dy * force]
2696        } else {
2697            pointer
2698        }
2699    }
2700    /// Calculate kinematic glow intensity based on proximity.
2701    fn mani_glow_intensity(&self, pointer: [f32; 2], bounds: Rect, radius: f32) -> f32 {
2702        let cx = bounds.x + bounds.width / 2.0;
2703        let cy = bounds.y + bounds.height / 2.0;
2704        let dist = ((pointer[0] - cx).powi(2) + (pointer[1] - cy).powi(2)).sqrt();
2705        if dist < radius {
2706            (1.0 - dist / radius).clamp(0.0, 1.0)
2707        } else {
2708            0.0
2709        }
2710    }
2711    /// Calculate dynamic element attention (scaling/morphing) statelessly per frame.
2712    fn fafnir_evolve(&self, pointer: [f32; 2], bounds: Rect, max_scale: f32) -> f32 {
2713        let prox = self.mani_glow_intensity(pointer, bounds, 120.0);
2714        1.0 + (max_scale - 1.0) * prox
2715    }
2716    /// Sets the precise Vili SDF Shape boundary for hit-testing.
2717    fn set_sdf_shape(&mut self, _shape: crate::layout::SdfShape) {}
2718
2719    // -- Portal / PhaseGate rendering -----------------------------------------
2720
2721    /// Begin rendering into the portal root layer instead of the inline tree.
2722    /// All draw calls between `enter_portal` and `exit_portal` are collected
2723    /// into a separate buffer that is composited AFTER the main tree.
2724    ///
2725    /// WHY separate buffer: The main tree may have clipping, transforms, or
2726    /// opacity that should NOT affect overlays. The portal layer renders on top
2727    /// of everything, ignoring the local coordinate system.
2728    fn enter_portal(&mut self, _z_index: i32) {}
2729
2730    /// Exit the portal layer and return to inline rendering.
2731    /// The portal content collected since `enter_portal` is now sealed --
2732    /// no more draw calls will be appended to it.
2733    fn exit_portal(&mut self) {}
2734
2735    /// Get the current viewport size in logical pixels.
2736    /// Used by portal content to size itself to the full screen.
2737    fn viewport_size(&self) -> Rect {
2738        Rect::new(0.0, 0.0, 1920.0, 1080.0)
2739    }
2740
2741    // -- Accessibility announcements -----------------------------------------
2742
2743    /// Announce a message to screen readers via the platform accessibility API.
2744    /// This call is non-blocking. The message is queued and the screen reader
2745    /// will speak it at its own pace.
2746    fn announce(&mut self, _message: &str, _priority: AnnouncementPriority) {}
2747}
2748
2749/// Utility for accessibility compliance (WCAG 2.1).
2750pub mod accessibility {
2751    /// Calculate the relative luminance of an sRGB color.
2752    pub fn relative_luminance(color: [f32; 4]) -> f32 {
2753        let f = |c: f32| {
2754            if c <= 0.03928 {
2755                c / 12.92
2756            } else {
2757                ((c + 0.055) / 1.055).powf(2.4)
2758            }
2759        };
2760        0.2126 * f(color[0]) + 0.7152 * f(color[1]) + 0.0722 * f(color[2])
2761    }
2762
2763    /// Calculate the contrast ratio between two colors.
2764    pub fn contrast_ratio(c1: [f32; 4], c2: [f32; 4]) -> f32 {
2765        let l1 = relative_luminance(c1);
2766        let l2 = relative_luminance(c2);
2767        let (light, dark) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
2768        (light + 0.05) / (dark + 0.05)
2769    }
2770}
2771/// Defines the hardware acceleration tier and feature set available to the renderer.
2772#[derive(
2773    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
2774)]
2775pub enum RenderTier {
2776    /// High-performance GPU path (WebGPU / Vulkan / Metal / DX12) with full shader support.
2777    Tier1GPU = 0,
2778    /// Mid-tier GPU path (WebGL2 / OpenGL 3.3) with standard shader support.
2779    Tier2GPU = 1,
2780    /// Fallback software or basic hardware path (Canvas 2D / GDI+) with limited effects.
2781    Tier3Fallback = 2,
2782}
2783// =============================================================================
2784// BERSERKER UNIFORMS
2785// =============================================================================
2786use bytemuck::{Pod, Zeroable};
2787/// Fully themeable color palette for the Berserker pipeline.
2788#[repr(C)]
2789#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2790pub struct ColorTheme {
2791    pub primary_neon: [f32; 4], // (R, G, B, intensity)
2792    pub shatter_neon: [f32; 4],
2793    pub glass_base: [f32; 4],
2794    pub glass_edge: [f32; 4],
2795    pub rune_glow: [f32; 4],
2796    pub ember_core: [f32; 4],
2797    pub background_deep: [f32; 4],
2798    pub mani_glow: [f32; 4], // (R, G, B, radius)
2799    pub glass_blur_strength: f32,
2800    pub shatter_edge_width: f32,
2801    pub neon_bloom_radius: f32,
2802    pub rune_opacity: f32,
2803    /// Weight of adaptive tint from backdrop [0.0, 1.0].
2804    /// 0.0 = static theme tint, 1.0 = fully adaptive.
2805    pub glass_tint_adapt: f32,
2806    /// Per-frame glass IOR override. 0.0 = use shader default (1.45).
2807    pub glass_ior: f32,
2808    /// Color space for framebuffer output. 0 = sRGB (default), 1 = Display P3, 2 = Adobe RGB.
2809    pub color_space: u32,
2810    // Padding to match WGSL uniform buffer 16-byte struct alignment (total = 176 bytes).
2811    pub _pad0: f32,
2812    pub _pad1: f32,
2813    pub _pad2: f32,
2814    pub _pad3: f32,
2815    pub _pad4: f32,
2816}
2817// P2-9: Compile-time layout verification between Rust ColorTheme and WGSL.
2818// WGSL std140 struct size = 176 bytes (164 raw + 12 alignment padding).
2819// Rust repr(C) struct must match exactly.
2820const _: () = assert!(
2821    std::mem::size_of::<ColorTheme>() == 176,
2822    "ColorTheme Rust/WGSL layout mismatch: expected 176 bytes"
2823);
2824impl ColorTheme {
2825    /// Asgard Mode: The high-fidelity "Cyberpunk Viking" aesthetic.
2826    pub fn asgard() -> Self {
2827        Self {
2828            primary_neon: [0.0, 1.0, 0.95, 1.2],
2829            shatter_neon: [1.0, 0.0, 0.75, 1.5],
2830            glass_base: [0.04, 0.04, 0.06, 0.82],
2831            glass_edge: [0.0, 0.45, 0.55, 0.6],
2832            rune_glow: [0.75, 0.98, 1.0, 0.9],
2833            ember_core: [0.95, 0.12, 0.12, 1.0],
2834            background_deep: [0.01, 0.01, 0.03, 1.0],
2835            mani_glow: [0.7, 0.9, 1.0, 0.05],
2836            glass_blur_strength: 0.6,
2837            shatter_edge_width: 1.8,
2838            neon_bloom_radius: 0.022,
2839            rune_opacity: 0.55,
2840            glass_tint_adapt: 0.35,
2841            glass_ior: 1.45,
2842            color_space: 0,
2843            _pad0: 0.0,
2844            _pad1: 0.0,
2845            _pad2: 0.0,
2846            _pad3: 0.0,
2847            _pad4: 0.0,
2848        }
2849    }
2850
2851    /// Midgard Mode: A clean, functional tactical HUD for standard operations.
2852    pub fn midgard() -> Self {
2853        Self {
2854            primary_neon: [0.2, 0.4, 0.6, 1.0], // Muted blue
2855            shatter_neon: [0.5, 0.5, 0.5, 1.0], // Neutral gray
2856            glass_base: [0.1, 0.12, 0.15, 1.0], // Solid slate
2857            glass_edge: [0.3, 0.35, 0.4, 1.0],  // Subtle border
2858            rune_glow: [0.8, 0.8, 0.8, 0.0],    // Runes disabled
2859            ember_core: [0.5, 0.5, 0.5, 1.0],
2860            background_deep: [0.05, 0.05, 0.07, 1.0],
2861            mani_glow: [0.0, 0.0, 0.0, 0.0], // No cursor glow
2862            glass_blur_strength: 0.0,        // No blur
2863            shatter_edge_width: 1.0,
2864            neon_bloom_radius: 0.0,
2865            rune_opacity: 0.0,
2866            glass_tint_adapt: 0.0,
2867            glass_ior: 1.0,
2868            color_space: 0,
2869            _pad0: 0.0,
2870            _pad1: 0.0,
2871            _pad2: 0.0,
2872            _pad3: 0.0,
2873            _pad4: 0.0,
2874        }
2875    }
2876
2877    pub fn cyberpunk_viking() -> Self {
2878        Self::asgard()
2879    }
2880    pub fn vibrant_glass() -> Self {
2881        Self {
2882            primary_neon: [0.0, 1.0, 0.95, 1.2],
2883            shatter_neon: [1.0, 0.0, 0.75, 1.5],
2884            glass_base: [0.55, 0.6, 0.7, 0.08], // Luminous cool tint
2885            glass_edge: [0.7, 0.85, 1.0, 0.45], // Subtle blue-white rim
2886            rune_glow: [0.75, 0.98, 1.0, 0.9],
2887            ember_core: [1.0, 0.4, 0.1, 1.0],
2888            background_deep: [0.05, 0.05, 0.1, 1.0],
2889            mani_glow: [0.7, 0.9, 1.0, 0.05],
2890            glass_blur_strength: 0.9,
2891            shatter_edge_width: 1.8,
2892            neon_bloom_radius: 0.022,
2893            rune_opacity: 0.55,
2894            glass_tint_adapt: 0.65,
2895            glass_ior: 1.45,
2896            color_space: 0,
2897            _pad0: 0.0,
2898            _pad1: 0.0,
2899            _pad2: 0.0,
2900            _pad3: 0.0,
2901            _pad4: 0.0,
2902        }
2903    }
2904
2905    /// Berserker Mode: Blood-iron neon, aggressive contrast, forge-heated glass.
2906    pub fn berserker() -> Self {
2907        Self {
2908            primary_neon: [1.0, 0.08, 0.12, 1.8],
2909            shatter_neon: [0.95, 0.92, 0.88, 1.6],
2910            glass_base: [0.03, 0.02, 0.02, 0.88],
2911            glass_edge: [0.8, 0.35, 0.08, 0.7],
2912            rune_glow: [0.9, 0.72, 0.3, 1.0],
2913            ember_core: [0.98, 0.25, 0.05, 1.0],
2914            background_deep: [0.01, 0.005, 0.005, 1.0],
2915            mani_glow: [0.8, 0.2, 0.05, 0.08],
2916            glass_blur_strength: 0.85,
2917            shatter_edge_width: 2.8,
2918            neon_bloom_radius: 0.035,
2919            rune_opacity: 0.85,
2920            glass_tint_adapt: 0.15,
2921            glass_ior: 1.85,
2922            color_space: 0,
2923            _pad0: 0.0,
2924            _pad1: 0.0,
2925            _pad2: 0.0,
2926            _pad3: 0.0,
2927            _pad4: 0.0,
2928        }
2929    }
2930}
2931impl Default for ColorTheme {
2932    fn default() -> Self {
2933        Self::vibrant_glass()
2934    }
2935}
2936/// Per-frame scene state for the Berserker pipeline.
2937#[repr(C)]
2938#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2939pub struct SceneUniforms {
2940    pub view: glam::Mat4,
2941    pub proj: glam::Mat4,
2942    pub time: f32,
2943    pub delta_time: f32,
2944    pub resolution: [f32; 2],
2945    pub mouse: [f32; 2],
2946    pub mouse_velocity: [f32; 2],
2947    pub shatter_origin: [f32; 2],
2948    pub shatter_time: f32,
2949    pub shatter_force: f32,
2950    pub berzerker_rage: f32,
2951    pub berzerker_mode: u32,
2952    pub scroll_offset: f32,
2953    pub scale_factor: f32,
2954    pub scene_type: u32,
2955    pub _pad_vec2_align: [u32; 1], // 4-byte pad: WGSL vec2<f32> requires 8-byte alignment
2956    pub fireball_pos: [f32; 2],
2957    pub _pad: [f32; 4], // Align to 224 bytes (struct align 16 from Mat4)
2958}
2959
2960pub const SCENE_AURORA: u32 = 0;
2961pub const SCENE_VOID: u32 = 1;
2962pub const SCENE_NEBULA: u32 = 2;
2963pub const SCENE_GLITCH: u32 = 3;
2964pub const SCENE_YGGDRASIL: u32 = 4;
2965
2966/// Resolve a scene name string to a scene preset constant.
2967/// Case-insensitive. Supports: "aurora", "void", "nebula", "glitch", "yggdrasil".
2968/// Also supports common aliases: "empty", "none" → VOID.
2969/// Returns None if the name is not recognized.
2970pub fn resolve_scene_by_name(name: &str) -> Option<u32> {
2971    let normalized = name.to_lowercase().replace(['-', '_', ' ', '.'], "");
2972    match normalized.as_str() {
2973        "aurora" => Some(SCENE_AURORA),
2974        "void" | "empty" | "none" | "blank" => Some(SCENE_VOID),
2975        "nebula" => Some(SCENE_NEBULA),
2976        "glitch" => Some(SCENE_GLITCH),
2977        "yggdrasil" | "worldtree" | "tree" => Some(SCENE_YGGDRASIL),
2978        _ => None,
2979    }
2980}
2981
2982impl SceneUniforms {
2983    pub fn new(width: f32, height: f32) -> Self {
2984        Self {
2985            view: glam::Mat4::IDENTITY,
2986            proj: glam::Mat4::orthographic_lh(0.0, width, height, 0.0, -100.0, 100.0),
2987            time: 0.0,
2988            delta_time: 0.016,
2989            resolution: [width, height],
2990            mouse: [0.5, 0.5],
2991            mouse_velocity: [0.0, 0.0],
2992            shatter_origin: [0.5, 0.5],
2993            shatter_time: -100.0,
2994            shatter_force: 0.0,
2995            berzerker_rage: 0.0,
2996            berzerker_mode: 0,
2997            scroll_offset: 0.0,
2998            scale_factor: 1.0,
2999            scene_type: SCENE_AURORA,
3000            _pad_vec2_align: [0],
3001            fireball_pos: [0.0, 0.0],
3002            _pad: [0.0; 4],
3003        }
3004    }
3005}
3006/// A 3D mesh containing vertex and index data.
3007#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3008pub struct Mesh {
3009    pub vertices: Vec<[f32; 3]>,
3010    pub normals: Vec<[f32; 3]>,
3011    pub indices: Vec<u32>,
3012}
3013impl Mesh {
3014    pub fn from_obj(data: &[u8]) -> anyhow::Result<Vec<Self>> {
3015        let mut cursor = std::io::Cursor::new(data);
3016        let (models, _) = tobj::load_obj_buf(&mut cursor, &tobj::LoadOptions::default(), |_| {
3017            Ok((Vec::new(), Default::default()))
3018        })?;
3019        let mut meshes = Vec::new();
3020        for m in models {
3021            let mesh = m.mesh;
3022            let vertices: Vec<[f32; 3]> = mesh
3023                .positions
3024                .chunks(3)
3025                .map(|c| [c[0], c[1], c[2]])
3026                .collect();
3027            let normals = if mesh.normals.is_empty() {
3028                vec![[0.0, 0.0, 1.0]; vertices.len()]
3029            } else {
3030                mesh.normals.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
3031            };
3032            meshes.push(Mesh {
3033                vertices,
3034                normals,
3035                indices: mesh.indices,
3036            });
3037        }
3038        Ok(meshes)
3039    }
3040    pub fn from_stl(data: &[u8]) -> anyhow::Result<Self> {
3041        let mut cursor = std::io::Cursor::new(data);
3042        let stl = stl_io::read_stl(&mut cursor)?;
3043        let vertices: Vec<[f32; 3]> = stl.vertices.iter().map(|v| [v[0], v[1], v[2]]).collect();
3044        let mut indices = Vec::new();
3045        for face in stl.faces {
3046            indices.push(face.vertices[0] as u32);
3047            indices.push(face.vertices[1] as u32);
3048            indices.push(face.vertices[2] as u32);
3049        }
3050        let normals = vec![[0.0, 0.0, 1.0]; vertices.len()];
3051        Ok(Mesh {
3052            vertices,
3053            normals,
3054            indices,
3055        })
3056    }
3057}
3058
3059// ══════════════════════════════════════════════════════════════════════════
3060// 3D TYPES -- Phase 1: Camera, Transform, and 2.5D layer support
3061// ══════════════════════════════════════════════════════════════════════════
3062
3063/// A 3D transform: position, rotation (quaternion), and scale.
3064#[derive(Debug, Clone, Copy, PartialEq)]
3065pub struct Transform3D {
3066    pub position: glam::Vec3,
3067    pub rotation: glam::Quat,
3068    pub scale: glam::Vec3,
3069}
3070
3071impl Default for Transform3D {
3072    fn default() -> Self {
3073        Self {
3074            position: glam::Vec3::ZERO,
3075            rotation: glam::Quat::IDENTITY,
3076            scale: glam::Vec3::ONE,
3077        }
3078    }
3079}
3080
3081impl Transform3D {
3082    /// Convert this transform to a 4x4 model matrix.
3083    pub fn to_matrix(&self) -> glam::Mat4 {
3084        glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
3085    }
3086
3087    /// Create a 2D-compatible transform (z=0, no rotation on z axis).
3088    pub fn from_2d(x: f32, y: f32, rotation: f32) -> Self {
3089        Self {
3090            position: glam::Vec3::new(x, y, 0.0),
3091            rotation: glam::Quat::from_rotation_z(rotation),
3092            scale: glam::Vec3::ONE,
3093        }
3094    }
3095}
3096
3097/// Camera definition for 3D rendering.
3098#[derive(Debug, Clone, Copy)]
3099pub struct Camera3D {
3100    /// World-space camera position.
3101    pub position: glam::Vec3,
3102    /// World-space point the camera looks at.
3103    pub target: glam::Vec3,
3104    /// World-space up vector.
3105    pub up: glam::Vec3,
3106    /// Field of view in radians (perspective) or half-height (orthographic).
3107    pub fov_y: f32,
3108    /// Near clipping plane distance.
3109    pub near: f32,
3110    /// Far clipping plane distance.
3111    pub far: f32,
3112    /// If true, use perspective projection. If false, use orthographic.
3113    pub perspective: bool,
3114    /// Aspect ratio (width / height). Used for perspective projection.
3115    pub aspect: f32,
3116}
3117
3118/// Material properties for 3D rendering.
3119#[derive(Debug, Clone, Copy, PartialEq)]
3120pub struct Material3D {
3121    /// Base color (RGBA).
3122    pub base_color: [f32; 4],
3123    /// Metallic factor (0 = dielectric, 1 = metallic).
3124    pub metallic: f32,
3125    /// Roughness factor (0 = mirror, 1 = fully diffuse).
3126    pub roughness: f32,
3127    /// Emissive color (RGB) for self-illumination.
3128    pub emissive: [f32; 3],
3129    /// Opacity (0 = transparent, 1 = opaque).
3130    pub opacity: f32,
3131}
3132
3133impl Default for Material3D {
3134    fn default() -> Self {
3135        Self {
3136            base_color: [1.0, 1.0, 1.0, 1.0],
3137            metallic: 0.0,
3138            roughness: 0.5,
3139            emissive: [0.0, 0.0, 0.0],
3140            opacity: 1.0,
3141        }
3142    }
3143}
3144
3145impl Material3D {
3146    /// Create a simple unlit material with just a color.
3147    pub fn unlit(color: [f32; 4]) -> Self {
3148        Self {
3149            base_color: color,
3150            metallic: 0.0,
3151            roughness: 1.0,
3152            emissive: [0.0, 0.0, 0.0],
3153            opacity: color[3],
3154        }
3155    }
3156
3157    /// Create a metallic material.
3158    pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
3159        Self {
3160            base_color: color,
3161            metallic: 1.0,
3162            roughness: roughness.clamp(0.0, 1.0),
3163            emissive: [0.0, 0.0, 0.0],
3164            opacity: color[3],
3165        }
3166    }
3167}
3168
3169impl Default for Camera3D {
3170    fn default() -> Self {
3171        Self {
3172            position: glam::Vec3::new(0.0, 0.0, 10.0),
3173            target: glam::Vec3::ZERO,
3174            up: glam::Vec3::Y,
3175            fov_y: 45.0f32.to_radians(),
3176            near: 0.1,
3177            far: 1000.0,
3178            perspective: true,
3179            aspect: 16.0 / 9.0,
3180        }
3181    }
3182}
3183
3184impl Camera3D {
3185    /// Compute the view matrix (world → camera space).
3186    pub fn view_matrix(&self) -> glam::Mat4 {
3187        glam::Mat4::look_at_lh(self.position, self.target, self.up)
3188    }
3189
3190    /// Compute the projection matrix.
3191    pub fn projection_matrix(&self) -> glam::Mat4 {
3192        if self.perspective {
3193            glam::Mat4::perspective_lh(self.fov_y, self.aspect, self.near, self.far)
3194        } else {
3195            // Orthographic with fov_y as half-height
3196            let top = self.fov_y;
3197            let right = top * self.aspect;
3198            glam::Mat4::orthographic_lh(-right, right, -top, top, self.near, self.far)
3199        }
3200    }
3201
3202    /// Compute the combined view-projection matrix.
3203    pub fn view_projection(&self) -> glam::Mat4 {
3204        self.projection_matrix() * self.view_matrix()
3205    }
3206}
3207
3208/// FrameRenderer extends Renderer with frame lifecycle management.
3209/// It is typically implemented by the host windowing/rendering environment.
3210pub trait FrameRenderer<E = ()>: Renderer {
3211    fn begin_frame(&mut self) -> E;
3212    fn render_frame(&mut self) {
3213        // Default implementation does nothing - override for custom frame rendering
3214    }
3215    fn end_frame(&mut self, encoder: E);
3216}
3217use std::sync::Arc;
3218type SubscriberList<T> = Arc<std::sync::Mutex<Vec<Box<dyn Fn(&T) + Send + Sync>>>>;
3219
3220/// P1-15 fix: invoke all subscribers in a list, isolating panics so that a
3221/// single faulty callback does not poison the Mutex and break all future
3222/// state updates forever. Returns the number of subscribers invoked
3223/// successfully. Each callback is wrapped in `catch_unwind`; panics are
3224/// logged but do not propagate.
3225fn invoke_subscribers_safely<T>(subs: &SubscriberList<T>, val: &T) -> usize
3226where
3227    // No UnwindSafe bound on T: subscriber callbacks receive &T and the
3228    // user is responsible for the panic-safety contract. We use
3229    // AssertUnwindSafe internally to opt out of the check.
3230{
3231    // Acquire the lock with poison recovery: if a previous panic poisoned
3232    // the mutex, recover and continue (the previous subscriber may have
3233    // left the list in an inconsistent state, but the best we can do is
3234    // log and try again). On recovery, the existing subscriber list is
3235    // preserved so we do not silently drop user subscriptions.
3236    let guard = match subs.lock() {
3237        Ok(g) => g,
3238        Err(poisoned) => {
3239            log::warn!(
3240                "[State] subscriber list mutex was poisoned; recovering"
3241            );
3242            poisoned.into_inner()
3243        }
3244    };
3245    let mut invoked = 0usize;
3246    for cb in guard.iter() {
3247        // Wrap each callback in catch_unwind so a panicking subscriber
3248        // does not poison the mutex and break subsequent state updates.
3249        // The catch_unwind returns Err if the closure panicked.
3250        let cb_ref: &(dyn Fn(&T) + Send + Sync) = &**cb;
3251        // Use AssertUnwindSafe because subscriber callbacks are Fn (not
3252        // UnwindSafe by default due to &T parameter), but the actual
3253        // panic-safety contract is the subscriber author's responsibility.
3254        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3255            cb_ref(val);
3256        }));
3257        if let Err(payload) = result {
3258            let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
3259                (*s).to_string()
3260            } else if let Some(s) = payload.downcast_ref::<String>() {
3261                s.clone()
3262            } else {
3263                "unknown panic payload".to_string()
3264            };
3265            log::error!("[State] subscriber callback panicked: {msg}");
3266            // Do NOT re-raise; continue invoking remaining subscribers.
3267        } else {
3268            invoked += 1;
3269        }
3270    }
3271    invoked
3272}
3273/// State wrapper that owns a value and notifies subscribers when changed.
3274///
3275/// P1-14: this struct carries 4 storage mechanisms:
3276/// 1. `arc_swap::ArcSwap<T>` for lock-free reads (the hot path)
3277/// 2. `arc_swap::ArcSwap<Option<MutationMetadata>>` for metadata reads
3278/// 3. `stm::TVar<T>` for atomic compound transactions (only on non-WASM)
3279/// 4. `stm::TVar<Option<MutationMetadata>>` for transactional metadata
3280///
3281/// The audit flagged this as 4 atomic/sync primitives per State<T>
3282/// instance, which is heavy for small states. The 4 mechanisms
3283/// are kept because they serve different purposes: arc_swap is
3284/// for the read-heavy hot path, TVar is for atomic compound
3285/// transactions. A future refactor could consolidate to a single
3286/// storage backend (e.g., always use TVar) but that would have a
3287/// performance cost on reads.
3288///
3289/// The `set()` method provides a way to bypass TVar for simple
3290/// single-value updates, avoiding the storage cost when compound
3291/// transactions aren't needed.
3292#[derive(Clone)]
3293pub struct State<T: Clone + Send + Sync + 'static> {
3294    swap: Arc<arc_swap::ArcSwap<T>>,
3295    metadata_swap: Arc<arc_swap::ArcSwap<Option<agents::MutationMetadata>>>,
3296    #[cfg(not(target_arch = "wasm32"))]
3297    tvar: Arc<stm::TVar<T>>,
3298    #[cfg(not(target_arch = "wasm32"))]
3299    metadata_tvar: Arc<stm::TVar<Option<agents::MutationMetadata>>>,
3300    subscribers: SubscriberList<T>,
3301    version: Arc<std::sync::atomic::AtomicU64>,
3302    resolution: agents::ConflictResolution,
3303}
3304impl<T: Clone + Send + Sync + 'static> State<T> {
3305    /// Create a new State with initial value
3306    pub fn new(value: T) -> Self {
3307        #[cfg(not(target_arch = "wasm32"))]
3308        let tvar = Arc::new(stm::TVar::new(value.clone()));
3309        #[cfg(not(target_arch = "wasm32"))]
3310        let metadata_tvar = Arc::new(stm::TVar::new(None));
3311        Self {
3312            swap: Arc::new(arc_swap::ArcSwap::from_pointee(value)),
3313            metadata_swap: Arc::new(arc_swap::ArcSwap::new(Arc::new(None))),
3314            #[cfg(not(target_arch = "wasm32"))]
3315            tvar,
3316            #[cfg(not(target_arch = "wasm32"))]
3317            metadata_tvar,
3318            subscribers: Arc::new(std::sync::Mutex::new(Vec::new())),
3319            version: Arc::new(std::sync::atomic::AtomicU64::new(0)),
3320            resolution: agents::ConflictResolution::default(),
3321        }
3322    }
3323    /// Set the conflict resolution strategy for this state.
3324    pub fn with_resolution(mut self, resolution: agents::ConflictResolution) -> Self {
3325        self.resolution = resolution;
3326        self
3327    }
3328    /// Get the current value
3329    pub fn get(&self) -> T {
3330        (**self.swap.load()).clone()
3331    }
3332    /// Set a new value, notifying all subscribers. Applies conflict resolution if agents are present.
3333    pub fn set(&self, value: T) {
3334        #[cfg(not(target_arch = "wasm32"))]
3335        let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3336            let new_meta = agents::get_current_mutation_metadata();
3337            let existing_meta = self.metadata_tvar.read(tx)?;
3338            let mut skip = false;
3339            if self.resolution == agents::ConflictResolution::PriorityWins
3340                && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3341                && new_m.priority < old_m.priority
3342            {
3343                skip = true;
3344            }
3345            if !skip {
3346                self.tvar.write(tx, value.clone())?;
3347                self.metadata_tvar.write(tx, new_meta)?;
3348                Ok((false, value.clone(), new_meta))
3349            } else {
3350                Ok((true, self.tvar.read(tx)?, existing_meta))
3351            }
3352        });
3353        #[cfg(target_arch = "wasm32")]
3354        let (was_skipped, final_val, final_meta) =
3355            (false, value, agents::get_current_mutation_metadata());
3356        if was_skipped {
3357            if let (Some(new_m), Some(old_m)) =
3358                (agents::get_current_mutation_metadata(), final_meta)
3359            {
3360                agents::notify_conflict(agents::ConflictEvent {
3361                    agent_id: new_m.agent_id,
3362                    priority: new_m.priority,
3363                    existing_agent_id: old_m.agent_id,
3364                    existing_priority: old_m.priority,
3365                    timestamp_ms: new_m.timestamp_ms,
3366                });
3367            }
3368            return;
3369        }
3370        self.swap.store(Arc::new(final_val.clone()));
3371        self.metadata_swap.store(Arc::new(final_meta));
3372        self.version
3373            .fetch_add(1, std::sync::atomic::Ordering::Release);
3374        let subs = Arc::clone(&self.subscribers);
3375        if crate::is_batching() {
3376            crate::enqueue_batch_task(Box::new(move || {
3377                let _ = invoke_subscribers_safely(&subs, &final_val);
3378            }));
3379        } else {
3380            let _ = invoke_subscribers_safely(&subs, &final_val);
3381        }
3382    }
3383
3384    /// P1-14: direct set that bypasses TVar for callers who don't
3385    /// need atomic compound transactions. Avoids the redundant
3386    /// storage cost when only the value and metadata are updated
3387    /// (not coordinated with other State<T> instances).
3388    ///
3389    /// Use this instead of `set()` when:
3390    ///  - You don't use conflict resolution (e.g., simple
3391    ///    single-threaded UI state).
3392    ///  - You don't need to coordinate with other State<T>
3393    ///    instances in a single transaction.
3394    ///
3395    /// Both the swap and TVar are updated atomically so that
3396    /// subsequent reads via either path see a consistent value.
3397    pub fn set_direct(&self, value: T) {
3398        self.swap.store(Arc::new(value.clone()));
3399        let new_meta = agents::get_current_mutation_metadata();
3400        self.metadata_swap.store(Arc::new(new_meta));
3401        self.version
3402            .fetch_add(1, std::sync::atomic::Ordering::Release);
3403        #[cfg(not(target_arch = "wasm32"))]
3404        {
3405            let _ = stm::atomically(|tx| {
3406                self.tvar.write(tx, value.clone())?;
3407                let meta = agents::get_current_mutation_metadata();
3408                self.metadata_tvar.write(tx, meta)?;
3409                Ok(())
3410            });
3411        }
3412        let subs = Arc::clone(&self.subscribers);
3413        if crate::is_batching() {
3414            crate::enqueue_batch_task(Box::new(move || {
3415                let _ = invoke_subscribers_safely(&subs, &value);
3416            }));
3417        } else {
3418            let _ = invoke_subscribers_safely(&subs, &value);
3419        }
3420    }
3421    pub fn mutate<F: Fn(&T) -> T>(&self, f: F) {
3422        #[cfg(not(target_arch = "wasm32"))]
3423        {
3424            let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3425                let new_meta = agents::get_current_mutation_metadata();
3426                let existing_meta = self.metadata_tvar.read(tx)?;
3427                let mut skip = false;
3428                if self.resolution == agents::ConflictResolution::PriorityWins
3429                    && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3430                    && new_m.priority < old_m.priority
3431                {
3432                    skip = true;
3433                }
3434                if !skip {
3435                    let current = self.tvar.read(tx)?;
3436                    let next = f(&current);
3437                    self.tvar.write(tx, next.clone())?;
3438                    self.metadata_tvar.write(tx, new_meta)?;
3439                    Ok((false, next, new_meta))
3440                } else {
3441                    Ok((true, self.tvar.read(tx)?, existing_meta))
3442                }
3443            });
3444            if was_skipped {
3445                if let (Some(new_m), Some(old_m)) =
3446                    (agents::get_current_mutation_metadata(), final_meta)
3447                {
3448                    agents::notify_conflict(agents::ConflictEvent {
3449                        agent_id: new_m.agent_id,
3450                        priority: new_m.priority,
3451                        existing_agent_id: old_m.agent_id,
3452                        existing_priority: old_m.priority,
3453                        timestamp_ms: new_m.timestamp_ms,
3454                    });
3455                }
3456                return;
3457            }
3458            self.swap.store(Arc::new(final_val.clone()));
3459            self.metadata_swap.store(Arc::new(final_meta));
3460            self.version
3461                .fetch_add(1, std::sync::atomic::Ordering::Release);
3462            let subs = Arc::clone(&self.subscribers);
3463            if crate::is_batching() {
3464                crate::enqueue_batch_task(Box::new(move || {
3465                    let _ = invoke_subscribers_safely(&subs, &final_val);
3466                }));
3467            } else {
3468                let _ = invoke_subscribers_safely(&subs, &final_val);
3469            }
3470        }
3471        #[cfg(target_arch = "wasm32")]
3472        {
3473            self.set(f(&self.get()));
3474        }
3475    }
3476    /// Get current version
3477    pub fn version(&self) -> u64 {
3478        self.version.load(std::sync::atomic::Ordering::Acquire)
3479    }
3480    /// Subscribe to state changes
3481    pub fn subscribe<F: Fn(&T) + Send + Sync + 'static>(&self, callback: F) {
3482        self.subscribers.lock().unwrap().push(Box::new(callback));
3483    }
3484}
3485use crate::runtime::NodeStateSnapshot;
3486use std::sync::OnceLock;
3487use std::sync::atomic::{AtomicBool, Ordering};
3488
3489/// P1-17 fix: shared fallback tokio runtime for `Suspense::new_async`.
3490///
3491/// When `new_async` is called without an ambient tokio runtime, the
3492/// previous implementation spawned a new OS thread + tokio runtime
3493/// for EACH call. For an app with many async data loads (e.g. a data
3494/// lake visualizer), this could spawn hundreds of OS threads.
3495///
3496/// The fix is a process-wide shared multi-threaded runtime, lazily
3497/// initialized on first use. The runtime uses a bounded worker count
3498/// (default: `max(1, num_cpus - 1)`) so we never spawn more than
3499/// `WORKER_THREADS` OS threads, regardless of how many Suspense
3500/// instances are created.
3501///
3502/// When the process exits the runtime is dropped, which joins all
3503/// worker threads.
3504#[cfg(not(target_arch = "wasm32"))]
3505static FALLBACK_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
3506
3507/// Number of worker threads for the fallback runtime. Computed lazily
3508/// from the available CPU count, then cached.
3509#[cfg(not(target_arch = "wasm32"))]
3510static FALLBACK_WORKER_COUNT: OnceLock<usize> = OnceLock::new();
3511
3512#[cfg(not(target_arch = "wasm32"))]
3513fn fallback_runtime() -> &'static tokio::runtime::Runtime {
3514    FALLBACK_RUNTIME.get_or_init(|| {
3515        // Bounded worker count: leave at least one core for the
3516        // application, but cap at 8 to avoid runaway thread creation
3517        // on hosts with very high CPU counts.
3518        let worker_count = *FALLBACK_WORKER_COUNT.get_or_init(|| {
3519            let available = std::thread::available_parallelism()
3520                .map(|n| n.get())
3521                .unwrap_or(2);
3522            available.saturating_sub(1).clamp(1, 8)
3523        });
3524        tokio::runtime::Builder::new_current_thread()
3525            .worker_threads(worker_count)
3526            .thread_name("cvkg-fallback-rt")
3527            .enable_all()
3528            .build()
3529            .expect("failed to build fallback tokio runtime")
3530    })
3531}
3532/// Global application state registry.
3533pub static SYSTEM_STATE: OnceLock<Arc<arc_swap::ArcSwap<KnowledgeState>>> = OnceLock::new();
3534#[cfg(not(target_arch = "wasm32"))]
3535static KNOWLEDGE_TVAR: OnceLock<stm::TVar<KnowledgeState>> = OnceLock::new();
3536static IS_BATCHING: AtomicBool = AtomicBool::new(false);
3537pub static IS_RENDERING: AtomicBool = AtomicBool::new(false);
3538pub static LAYOUT_DIRTY: AtomicBool = AtomicBool::new(false);
3539type BatchQueue = OnceLock<std::sync::Mutex<Vec<Box<dyn FnOnce() + Send + Sync>>>>;
3540static BATCH_QUEUE: BatchQueue = OnceLock::new();
3541/// Global write lock to serialize updates to SYSTEM_STATE and KNOWLEDGE_TVAR,
3542/// preventing parallel race conditions between STM transactions and the lock-free reader state.
3543static STATE_WRITE_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3544/// Returns true if state updates are currently being batched.
3545pub fn is_batching() -> bool {
3546    IS_BATCHING.load(Ordering::Acquire)
3547}
3548/// Returns true if the system is currently in the render phase.
3549pub fn is_rendering() -> bool {
3550    IS_RENDERING.load(Ordering::Acquire)
3551}
3552/// Signals the start of the render phase. Mutations during this phase trigger warnings.
3553pub fn begin_render_phase() {
3554    IS_RENDERING.store(true, Ordering::Release);
3555}
3556/// Signals the end of the render phase.
3557pub fn end_render_phase() {
3558    IS_RENDERING.store(false, Ordering::Release);
3559}
3560/// Enqueues a notification task to be run when the current batch flushes.
3561pub fn enqueue_batch_task(task: Box<dyn FnOnce() + Send + Sync>) {
3562    let mut queue = BATCH_QUEUE
3563        .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3564        .lock()
3565        .unwrap();
3566    queue.push(task);
3567}
3568/// Executes multiple state updates in a single batch, deferring all subscriber
3569/// notifications until the closure completes. This prevents layout thrashing
3570/// and redundant render cycles when modifying multiple independent states.
3571pub fn batch<F: FnOnce()>(f: F) {
3572    if IS_BATCHING.swap(true, Ordering::AcqRel) {
3573        // Already inside a batch, just execute
3574        f();
3575        return;
3576    }
3577    f();
3578    IS_BATCHING.store(false, Ordering::Release);
3579    let mut queue = BATCH_QUEUE
3580        .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3581        .lock()
3582        .unwrap();
3583    let tasks: Vec<_> = queue.drain(..).collect();
3584    drop(queue);
3585    for task in tasks {
3586        task();
3587    }
3588}
3589/// Get a reference to the global system state.
3590pub fn get_system_state() -> Arc<arc_swap::ArcSwap<KnowledgeState>> {
3591    SYSTEM_STATE
3592        .get_or_init(|| Arc::new(arc_swap::ArcSwap::from_pointee(KnowledgeState::default())))
3593        .clone()
3594}
3595pub fn load_system_state() -> arc_swap::Guard<Arc<KnowledgeState>> {
3596    get_system_state().load()
3597}
3598pub fn update_system_state<F>(f: F)
3599where
3600    F: FnOnce(&KnowledgeState) -> KnowledgeState,
3601{
3602    let _lock = STATE_WRITE_MUTEX.lock().unwrap();
3603    if is_rendering() {
3604        log::warn!(
3605            "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3606        );
3607    }
3608    LAYOUT_DIRTY.store(true, Ordering::SeqCst);
3609    let swap = get_system_state();
3610    let current = swap.load();
3611    let new_state = Arc::new(f(&current));
3612    swap.store(Arc::clone(&new_state));
3613    #[cfg(not(target_arch = "wasm32"))]
3614    {
3615        let tvar = KNOWLEDGE_TVAR.get_or_init(|| stm::TVar::new((*new_state).clone()));
3616        stm::atomically(|tx| tvar.write(tx, (*new_state).clone()));
3617    }
3618}
3619pub fn transact_system_state<F>(f: F)
3620where
3621    F: Fn(&KnowledgeState) -> KnowledgeState,
3622{
3623    let _lock = STATE_WRITE_MUTEX.lock().unwrap();
3624    #[cfg(not(target_arch = "wasm32"))]
3625    {
3626        if is_rendering() {
3627            log::warn!(
3628                "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3629            );
3630        }
3631        let tvar = KNOWLEDGE_TVAR
3632            .get_or_init(|| stm::TVar::new((**get_system_state().load()).clone()))
3633            .clone();
3634        let new_state = stm::atomically(move |tx| {
3635            let current = tvar.read(tx)?;
3636            let next = f(&current);
3637            tvar.write(tx, next.clone())?;
3638            Ok(next)
3639        });
3640        get_system_state().store(Arc::new(new_state));
3641    }
3642    #[cfg(target_arch = "wasm32")]
3643    {
3644        if is_rendering() {
3645            log::warn!(
3646                "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3647            );
3648        }
3649        update_system_state(f);
3650    }
3651}
3652impl KnowledgeState {
3653    /// Create a new empty KnowledgeState.
3654    pub fn new() -> Self {
3655        Self::default()
3656    }
3657    /// Set a component's internal state.
3658    pub fn set_component_state<T: 'static + Send + Sync>(&mut self, id: u64, state: T) {
3659        self.component_states
3660            .insert(id, Arc::new(std::sync::RwLock::new(state)));
3661    }
3662    /// Get a reference to a component's internal state.
3663    pub fn get_component_state<T: 'static + Send + Sync>(
3664        &self,
3665        id: u64,
3666    ) -> Option<Arc<std::sync::RwLock<T>>> {
3667        let stored = self.component_states.get(&id)?;
3668        // X-01 fix: safe downcast via Any:: instead of unsafe transmute.
3669        // The stored value is Arc<RwLock<dyn Any>>. We obtain a read lock
3670        // to verify that the inner type is indeed T.
3671        let any_ref = stored.read().ok()?;
3672        // downcast_ref checks the vtable at runtime -- no unsafe needed.
3673        let _verified: &T = any_ref.downcast_ref::<T>()?;
3674        drop(any_ref);
3675        // Recover the original Arc. The thin pointer cast is sound because we
3676        // have verified the concrete type via Any::downcast_ref above.
3677        let raw = Arc::into_raw(stored.clone());
3678        Some(unsafe { Arc::from_raw(raw as *const std::sync::RwLock<T>) })
3679    }
3680    /// Add a new fragment to memory.
3681    pub fn remember(&mut self, fragment: KnowledgeFragment) {
3682        self.fragments.insert(fragment.id.clone(), fragment);
3683    }
3684    /// Process a search query against the local knowledge base.
3685    pub fn process_query(&mut self, query: &str) {
3686        let query_lower = query.to_lowercase();
3687        let mut results: Vec<(f32, String)> = self
3688            .fragments
3689            .iter()
3690            .map(|(id, frag)| {
3691                let mut score = 0.0;
3692                if frag.summary.to_lowercase().contains(&query_lower) {
3693                    score += 1.0;
3694                }
3695                if frag.source.to_lowercase().contains(&query_lower) {
3696                    score += 0.5;
3697                }
3698                (score, id.clone())
3699            })
3700            .filter(|(score, _)| *score > 0.0)
3701            .collect();
3702        // Sort by relevance score
3703        results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
3704        self.last_query_results = results.into_iter().map(|(_, id)| id).take(5).collect();
3705    }
3706    /// Captures a snapshot of the current state for debugging and hot-reloading.
3707    pub fn snapshot(&self) -> Vec<NodeStateSnapshot> {
3708        let mut snapshots = Vec::new();
3709        // Snapshots of agentic fragments
3710        for frag in self.fragments.values() {
3711            if let Ok(val) = serde_json::to_value(frag) {
3712                snapshots.push(NodeStateSnapshot { id: 0, state: val });
3713            }
3714        }
3715        snapshots
3716    }
3717}
3718/// A read/write projection into a `State<T>` owned elsewhere.
3719#[derive(Clone)]
3720pub struct Binding<T: Clone + Send + Sync + 'static> {
3721    swap: Arc<arc_swap::ArcSwap<T>>,
3722    #[cfg(not(target_arch = "wasm32"))]
3723    tvar: Arc<stm::TVar<T>>,
3724    version: Arc<std::sync::atomic::AtomicU64>,
3725}
3726impl<T: Clone + Send + Sync + 'static> Binding<T> {
3727    /// Create a binding from a State
3728    pub fn from_state(state: &State<T>) -> Self {
3729        Self {
3730            swap: Arc::clone(&state.swap),
3731            #[cfg(not(target_arch = "wasm32"))]
3732            tvar: Arc::clone(&state.tvar),
3733            version: Arc::clone(&state.version),
3734        }
3735    }
3736    /// Get the current value
3737    pub fn get(&self) -> T {
3738        (**self.swap.load()).clone()
3739    }
3740    /// Set a new value
3741    pub fn set(&self, value: T) {
3742        self.swap.store(Arc::new(value.clone()));
3743        #[cfg(not(target_arch = "wasm32"))]
3744        {
3745            let tvar = Arc::clone(&self.tvar);
3746            let v = value.clone();
3747            stm::atomically(move |tx| tvar.write(tx, v.clone()));
3748        }
3749        self.version
3750            .fetch_add(1, std::sync::atomic::Ordering::Release);
3751    }
3752    /// Get current version
3753    pub fn version(&self) -> u64 {
3754        self.version.load(std::sync::atomic::Ordering::Acquire)
3755    }
3756}
3757#[cfg(not(target_arch = "wasm32"))]
3758pub fn transact_pair<A, B, F>(state_a: &State<A>, state_b: &State<B>, f: F)
3759where
3760    A: Clone + Send + Sync + 'static,
3761    B: Clone + Send + Sync + 'static,
3762    F: Fn(&A, &B) -> (A, B),
3763{
3764    let tvar_a = Arc::clone(&state_a.tvar);
3765    let tvar_b = Arc::clone(&state_b.tvar);
3766    let (new_a, new_b) = stm::atomically(move |tx| {
3767        let a = tvar_a.read(tx)?;
3768        let b = tvar_b.read(tx)?;
3769        let (na, nb) = f(&a, &b);
3770        tvar_a.write(tx, na.clone())?;
3771        tvar_b.write(tx, nb.clone())?;
3772        Ok((na, nb))
3773    });
3774    state_a.swap.store(Arc::new(new_a.clone()));
3775    state_b.swap.store(Arc::new(new_b.clone()));
3776    state_a
3777        .version
3778        .fetch_add(1, std::sync::atomic::Ordering::Release);
3779    state_b
3780        .version
3781        .fetch_add(1, std::sync::atomic::Ordering::Release);
3782    let subs_a = Arc::clone(&state_a.subscribers);
3783    let subs_b = Arc::clone(&state_b.subscribers);
3784    if crate::is_batching() {
3785        crate::enqueue_batch_task(Box::new(move || {
3786            {
3787                let s = subs_a.lock().unwrap();
3788                for cb in s.iter() {
3789                    cb(&new_a);
3790                }
3791            }
3792            {
3793                let s = subs_b.lock().unwrap();
3794                for cb in s.iter() {
3795                    cb(&new_b);
3796                }
3797            }
3798        }));
3799    } else {
3800        {
3801            let s = subs_a.lock().unwrap();
3802            for cb in s.iter() {
3803                cb(&new_a);
3804            }
3805        }
3806        {
3807            let s = subs_b.lock().unwrap();
3808            for cb in s.iter() {
3809                cb(&new_b);
3810            }
3811        }
3812    }
3813}
3814use std::any::TypeId;
3815use std::sync::Mutex;
3816/// Global environment storage using TypeId as keys.
3817pub(crate) static ENVIRONMENT: OnceLock<
3818    Mutex<HashMap<TypeId, Box<dyn std::any::Any + Send + Sync>>>,
3819> = OnceLock::new();
3820/// Environment key type for accessing ambient values
3821/// Implement this trait to define a new environment key.
3822pub trait EnvKey: 'static + Send + Sync {
3823    /// The type of value stored in the environment
3824    type Value: Clone + Send + Sync + 'static;
3825    /// Get a default value for this key
3826    fn default_value() -> Self::Value;
3827}
3828/// Key for accessing the Yggdrasil design token tree
3829pub struct YggdrasilKey;
3830impl EnvKey for YggdrasilKey {
3831    type Value = YggdrasilTokens;
3832    fn default_value() -> Self::Value {
3833        default_tokens()
3834    }
3835}
3836// Duplicate AssetKey removed - original definition at line 63
3837/// System appearance (Light/Dark mode)
3838#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3839pub enum Appearance {
3840    Light,
3841    Dark,
3842}
3843/// Orientation for layouts
3844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3845pub enum Orientation {
3846    Horizontal,
3847    Vertical,
3848}
3849/// Placement configuration for placing a view within a Grid layout.
3850#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3851pub struct GridPlacement {
3852    /// 0-based column index. Negative values count from the end of columns.
3853    pub column: i32,
3854    /// Number of columns the view spans (default is 1).
3855    pub column_span: u32,
3856    /// 0-based row index. Negative values count from the end of rows.
3857    pub row: i32,
3858    /// Number of rows the view spans (default is 1).
3859    pub row_span: u32,
3860}
3861/// Cross-axis alignment for layout containers.
3862#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3863pub enum Alignment {
3864    #[default]
3865    Center,
3866    Leading,
3867    Trailing,
3868    Top,
3869    Bottom,
3870}
3871/// Main-axis distribution for linear layout containers.
3872#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3873pub enum Distribution {
3874    #[default]
3875    Fill,
3876    Center,
3877    Leading,
3878    Trailing,
3879    SpaceBetween,
3880    SpaceAround,
3881    SpaceEvenly,
3882}
3883/// A color represented by RGBA components in the [0.0, 1.0] range.
3884#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3885pub struct Color {
3886    pub r: f32,
3887    pub g: f32,
3888    pub b: f32,
3889    pub a: f32,
3890}
3891impl Color {
3892    pub const BLACK: Color = Color {
3893        r: 0.0,
3894        g: 0.0,
3895        b: 0.0,
3896        a: 1.0,
3897    };
3898    pub const WHITE: Color = Color {
3899        r: 1.0,
3900        g: 1.0,
3901        b: 1.0,
3902        a: 1.0,
3903    };
3904    pub const TRANSPARENT: Color = Color {
3905        r: 0.0,
3906        g: 0.0,
3907        b: 0.0,
3908        a: 0.0,
3909    };
3910    pub const RED: Color = Color {
3911        r: 1.0,
3912        g: 0.0,
3913        b: 0.0,
3914        a: 1.0,
3915    };
3916    pub const GREEN: Color = Color {
3917        r: 0.0,
3918        g: 1.0,
3919        b: 0.0,
3920        a: 1.0,
3921    };
3922    pub const BLUE: Color = Color {
3923        r: 0.0,
3924        g: 0.0,
3925        b: 1.0,
3926        a: 1.0,
3927    };
3928    pub const VIKING_GOLD: Color = Color {
3929        r: 1.0,
3930        g: 0.84,
3931        b: 0.0,
3932        a: 1.0,
3933    };
3934    pub const MAGENTA_LIQUID: Color = Color {
3935        r: 1.0,
3936        g: 0.0,
3937        b: 1.0,
3938        a: 1.0,
3939    };
3940    pub const TACTICAL_OBSIDIAN: Color = Color {
3941        r: 0.05,
3942        g: 0.05,
3943        b: 0.07,
3944        a: 1.0,
3945    };
3946    /// Calculate the relative luminance of the color as defined by WCAG 2.x
3947    pub fn relative_luminance(&self) -> f32 {
3948        fn res(c: f32) -> f32 {
3949            if c <= 0.03928 {
3950                c / 12.92
3951            } else {
3952                ((c + 0.055) / 1.055).powf(2.4)
3953            }
3954        }
3955        0.2126 * res(self.r) + 0.7152 * res(self.g) + 0.0722 * res(self.b)
3956    }
3957    /// Calculate the contrast ratio between this color and another color
3958    pub fn contrast_ratio(&self, other: &Color) -> f32 {
3959        let l1 = self.relative_luminance();
3960        let l2 = other.relative_luminance();
3961        if l1 > l2 {
3962            (l1 + 0.05) / (l2 + 0.05)
3963        } else {
3964            (l2 + 0.05) / (l1 + 0.05)
3965        }
3966    }
3967    pub const CYAN: Color = Color {
3968        r: 0.0,
3969        g: 1.0,
3970        b: 1.0,
3971        a: 1.0,
3972    };
3973    pub const YELLOW: Color = Color {
3974        r: 1.0,
3975        g: 1.0,
3976        b: 0.0,
3977        a: 1.0,
3978    };
3979    pub const MAGENTA: Color = Color {
3980        r: 1.0,
3981        g: 0.0,
3982        b: 1.0,
3983        a: 1.0,
3984    };
3985    pub const GRAY: Color = Color {
3986        r: 0.5,
3987        g: 0.5,
3988        b: 0.5,
3989        a: 1.0,
3990    };
3991    /// Create a new color from RGBA components.
3992    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
3993        Self { r, g, b, a }
3994    }
3995    /// Convert the color to a [r, g, b, a] array.
3996    pub fn as_array(&self) -> [f32; 4] {
3997        [self.r, self.g, self.b, self.a]
3998    }
3999
4000    /// Return a new color with lightness increased by `amount`.
4001    ///
4002    /// Adds `amount` to each RGB channel and clamps to [0.0, 1.0].
4003    /// This is a simple sRGB lightness adjustment, not perceptually uniform.
4004    /// For perceptually uniform adjustments, use OKLCH via cvkg-themes.
4005    pub fn lighten(&self, amount: f32) -> Self {
4006        Self {
4007            r: (self.r + amount).clamp(0.0, 1.0),
4008            g: (self.g + amount).clamp(0.0, 1.0),
4009            b: (self.b + amount).clamp(0.0, 1.0),
4010            a: self.a,
4011        }
4012    }
4013
4014    /// Return a new color with lightness decreased by `amount`.
4015    pub fn darken(&self, amount: f32) -> Self {
4016        Self {
4017            r: (self.r - amount).clamp(0.0, 1.0),
4018            g: (self.g - amount).clamp(0.0, 1.0),
4019            b: (self.b - amount).clamp(0.0, 1.0),
4020            a: self.a,
4021        }
4022    }
4023}
4024impl View for Color {
4025    type Body = Never;
4026    fn body(self) -> Self::Body {
4027        // SAFETY: `Never` is uninhabitable. Color is a primitive view that fills a
4028        // rectangle directly in `render()` and never exposes a composable body.
4029        unreachable!()
4030    }
4031    fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
4032        renderer.fill_rect(rect, self.as_array());
4033    }
4034}
4035/// Key for accessing the current system appearance
4036pub struct AppearanceKey;
4037impl EnvKey for AppearanceKey {
4038    type Value = Appearance;
4039    fn default_value() -> Self::Value {
4040        Appearance::Dark // Default to Dark (Ginnungagap) for Berserker aesthetic
4041    }
4042}
4043
4044/// Key for accessing the current text direction
4045pub struct DirectionKey;
4046impl EnvKey for DirectionKey {
4047    type Value = Direction;
4048    fn default_value() -> Self::Value {
4049        Direction::LTR
4050    }
4051}
4052
4053/// StyleResolver provides high-level access to themed values from the environment.
4054pub struct StyleResolver;
4055impl StyleResolver {
4056    /// Resolve a color from the current environment
4057    pub fn color(key: &str) -> String {
4058        let tokens = Environment::<YggdrasilKey>::new().get();
4059        let appearance = Environment::<AppearanceKey>::new().get();
4060        let is_dark = appearance == Appearance::Dark;
4061        tokens
4062            .get_color(key, is_dark)
4063            .unwrap_or_else(|| "#FF00FF".to_string()) // Default to MuspelMagenta on failure
4064    }
4065    /// Resolve a generic token value
4066    pub fn get<T: FromStr>(category: &str, key: &str) -> Option<T> {
4067        let tokens = Environment::<YggdrasilKey>::new().get();
4068        let appearance = Environment::<AppearanceKey>::new().get();
4069        let is_dark = appearance == Appearance::Dark;
4070        tokens.get(category, key, is_dark)
4071    }
4072    /// Resolve a color from the current environment as a [f32; 4] RGBA array.
4073    /// Returns the color value for the current appearance (light/dark).
4074    /// Falls back to magenta (#FF00FF) if the key is not found.
4075    pub fn color_array(key: &str) -> [f32; 4] {
4076        let hex = Self::color(key);
4077        parse_hex_color(&hex)
4078    }
4079}
4080
4081/// Parse a hex color string (#RRGGBB or #RRGGBBAA) into [f32; 4] RGBA.
4082fn parse_hex_color(hex: &str) -> [f32; 4] {
4083    let hex = hex.trim_start_matches('#');
4084    if hex.len() >= 6 {
4085        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255) as f32 / 255.0;
4086        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
4087        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255) as f32 / 255.0;
4088        let a = if hex.len() >= 8 {
4089            u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
4090        } else {
4091            1.0
4092        };
4093        [r, g, b, a]
4094    } else {
4095        [1.0, 0.0, 1.0, 1.0] // Magenta fallback
4096    }
4097}
4098
4099/// The authoritative Cyberpunk Viking default tokens
4100pub fn default_tokens() -> YggdrasilTokens {
4101    let mut tokens = YggdrasilTokens::new();
4102    // Core Norse Colorways
4103    tokens.color.insert(
4104        "background".to_string(),
4105        TokenValue::Adaptive {
4106            light: "#FFFFFF".to_string(), // Light mode: white background
4107            dark: "#000000".to_string(),  // Dark mode: Ginnungagap (The Void)
4108        },
4109    );
4110    tokens.color.insert(
4111        "primary".to_string(),
4112        TokenValue::Adaptive {
4113            light: "#007B8A".to_string(), // Light mode: muted cyan
4114            dark: "#00FFFF".to_string(),  // Dark mode: NiflCyan (Aesir Primary)
4115        },
4116    );
4117    tokens.color.insert(
4118        "secondary".to_string(),
4119        TokenValue::Adaptive {
4120            light: "#8A008A".to_string(), // Light mode: muted magenta
4121            dark: "#FF00FF".to_string(),  // Dark mode: MuspelMagenta (Berserker Secondary)
4122        },
4123    );
4124    tokens.color.insert(
4125        "surface".to_string(),
4126        TokenValue::Adaptive {
4127            light: "#FFFFFF".to_string(),
4128            dark: "#121212".to_string(),
4129        },
4130    );
4131    tokens.color.insert(
4132        "text".to_string(),
4133        TokenValue::Adaptive {
4134            light: "#000000".to_string(),
4135            dark: "#FFFFFF".to_string(),
4136        },
4137    );
4138    // Semantic component tokens
4139    tokens.color.insert(
4140        "surface_elevated".to_string(),
4141        TokenValue::Adaptive {
4142            light: "#FFFFFF".to_string(),
4143            dark: "#1A1A24".to_string(),
4144        },
4145    );
4146    tokens.color.insert(
4147        "surface_overlay".to_string(),
4148        TokenValue::Adaptive {
4149            light: "#FFFFFF".to_string(),
4150            dark: "#1E1E2E".to_string(),
4151        },
4152    );
4153    tokens.color.insert(
4154        "border".to_string(),
4155        TokenValue::Adaptive {
4156            light: "#D0D0D8".to_string(),
4157            dark: "#2A2A3A".to_string(),
4158        },
4159    );
4160    tokens.color.insert(
4161        "border_strong".to_string(),
4162        TokenValue::Adaptive {
4163            light: "#A0A0B0".to_string(),
4164            dark: "#3A3A50".to_string(),
4165        },
4166    );
4167    tokens.color.insert(
4168        "text_muted".to_string(),
4169        TokenValue::Adaptive {
4170            light: "#606070".to_string(),
4171            dark: "#8080A0".to_string(),
4172        },
4173    );
4174    tokens.color.insert(
4175        "text_dim".to_string(),
4176        TokenValue::Adaptive {
4177            light: "#9090A0".to_string(),
4178            dark: "#505070".to_string(),
4179        },
4180    );
4181    tokens.color.insert(
4182        "accent".to_string(),
4183        TokenValue::Adaptive {
4184            light: "#007B8A".to_string(), // Light mode: muted cyan
4185            dark: "#00FFFF".to_string(),  // Dark mode: NiflCyan
4186        },
4187    );
4188    tokens.color.insert(
4189        "accent_hover".to_string(),
4190        TokenValue::Adaptive {
4191            light: "#00A0B0".to_string(), // Light mode: lighter muted cyan
4192            dark: "#33FFFF".to_string(),  // Dark mode: brighter cyan
4193        },
4194    );
4195    tokens.color.insert(
4196        "success".to_string(),
4197        TokenValue::Single {
4198            value: "#00E676".to_string(),
4199        },
4200    );
4201    tokens.color.insert(
4202        "warning".to_string(),
4203        TokenValue::Single {
4204            value: "#FFB300".to_string(),
4205        },
4206    );
4207    tokens.color.insert(
4208        "error".to_string(),
4209        TokenValue::Single {
4210            value: "#FF5252".to_string(),
4211        },
4212    );
4213    tokens.color.insert(
4214        "info".to_string(),
4215        TokenValue::Single {
4216            value: "#448AFF".to_string(),
4217        },
4218    );
4219    tokens.color.insert(
4220        "hover".to_string(),
4221        TokenValue::Adaptive {
4222            light: "#F0F0F5".to_string(),
4223            dark: "#252535".to_string(),
4224        },
4225    );
4226    tokens.color.insert(
4227        "active".to_string(),
4228        TokenValue::Adaptive {
4229            light: "#E0E0EB".to_string(),
4230            dark: "#303045".to_string(),
4231        },
4232    );
4233    tokens.color.insert(
4234        "disabled".to_string(),
4235        TokenValue::Adaptive {
4236            light: "#E8E8F0".to_string(),
4237            dark: "#1A1A28".to_string(),
4238        },
4239    );
4240    tokens.color.insert(
4241        "disabled_text".to_string(),
4242        TokenValue::Adaptive {
4243            light: "#B0B0C0".to_string(),
4244            dark: "#404060".to_string(),
4245        },
4246    );
4247    tokens.color.insert(
4248        "focus_ring".to_string(),
4249        TokenValue::Single {
4250            value: "#00FFFF".to_string(),
4251        },
4252    );
4253    tokens.color.insert(
4254        "shadow".to_string(),
4255        TokenValue::Adaptive {
4256            light: "#00000020".to_string(),
4257            dark: "#00000060".to_string(),
4258        },
4259    );
4260    tokens.color.insert(
4261        "code_bg".to_string(),
4262        TokenValue::Adaptive {
4263            light: "#F5F5FA".to_string(),
4264            dark: "#0D0D18".to_string(),
4265        },
4266    );
4267    // Bifrost (Glassmorphism) - Frosted Style
4268    tokens.bifrost.insert(
4269        "blur".to_string(),
4270        TokenValue::Single {
4271            value: "25.0".to_string(),
4272        },
4273    );
4274    tokens.bifrost.insert(
4275        "saturation".to_string(),
4276        TokenValue::Single {
4277            value: "1.2".to_string(),
4278        },
4279    );
4280    tokens.bifrost.insert(
4281        "opacity".to_string(),
4282        TokenValue::Single {
4283            value: "0.65".to_string(),
4284        },
4285    );
4286    // Gungnir (Neon Glow)
4287    tokens.gungnir.insert(
4288        "intensity".to_string(),
4289        TokenValue::Single {
4290            value: "1.0".to_string(),
4291        },
4292    );
4293    tokens.gungnir.insert(
4294        "radius".to_string(),
4295        TokenValue::Single {
4296            value: "15.0".to_string(),
4297        },
4298    );
4299    // Mjolnir (Sharp Geometry)
4300    tokens.mjolnir.insert(
4301        "clip_angle".to_string(),
4302        TokenValue::Single {
4303            value: "12.0".to_string(),
4304        },
4305    );
4306    tokens.mjolnir.insert(
4307        "border_width".to_string(),
4308        TokenValue::Single {
4309            value: "2.0".to_string(),
4310        },
4311    );
4312    // Sleipnir (Spring Animation)
4313    tokens.anim.insert(
4314        "stiffness".to_string(),
4315        TokenValue::Single {
4316            value: "170.0".to_string(),
4317        },
4318    );
4319    tokens.anim.insert(
4320        "damping".to_string(),
4321        TokenValue::Single {
4322            value: "26.0".to_string(),
4323        },
4324    );
4325    tokens.anim.insert(
4326        "mass".to_string(),
4327        TokenValue::Single {
4328            value: "1.0".to_string(),
4329        },
4330    );
4331    // Accessibility
4332    tokens.accessibility.insert(
4333        "reduce_motion".to_string(),
4334        TokenValue::Single {
4335            value: "false".to_string(),
4336        },
4337    );
4338    tokens
4339}
4340/// Environment wrapper for accessing ambient values
4341pub struct Environment<K: EnvKey> {
4342    _marker: std::marker::PhantomData<K>,
4343}
4344impl<K: EnvKey> Default for Environment<K> {
4345    fn default() -> Self {
4346        Self::new()
4347    }
4348}
4349impl<K: EnvKey> Environment<K> {
4350    /// Create a new Environment
4351    pub fn new() -> Self {
4352        Self {
4353            _marker: std::marker::PhantomData,
4354        }
4355    }
4356    /// Get the current value from the environment
4357    pub fn get(&self) -> K::Value {
4358        if let Some(env_store) = ENVIRONMENT.get() {
4359            let env_lock = env_store.lock().unwrap();
4360            if let Some(val) = env_lock.get(&std::any::TypeId::of::<K>()) {
4361                if let Some(typed_val) = val.downcast_ref::<K::Value>() {
4362                    return typed_val.clone();
4363                } else {
4364                    log::warn!(
4365                        "Environment: Downcast failed for key type {:?}",
4366                        std::any::type_name::<K>()
4367                    );
4368                }
4369            } else {
4370                // Lowered to trace to avoid terminal logging overhead under standard debug runs
4371                log::trace!(
4372                    "Environment: Key not found: {:?}. Returning default.",
4373                    std::any::type_name::<K>()
4374                );
4375            }
4376        } else {
4377            // Lowered to trace to avoid terminal logging overhead under standard debug runs
4378            log::trace!(
4379                "Environment: Store not initialized. Key: {:?}. Returning default.",
4380                std::any::type_name::<K>()
4381            );
4382        }
4383        K::default_value()
4384    }
4385}
4386/// Ambient environment management
4387pub mod env {
4388    /// Insert a value into the environment
4389    pub fn insert<K: super::EnvKey>(value: K::Value) {
4390        let store = super::ENVIRONMENT
4391            .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
4392        let mut env_map = store.lock().unwrap();
4393        env_map.insert(std::any::TypeId::of::<K>(), Box::new(value));
4394    }
4395    /// Remove a value from the environment.
4396    pub fn remove<K: super::EnvKey>() {
4397        if let Some(store) = super::ENVIRONMENT.get() {
4398            let mut env_map = store.lock().unwrap();
4399            env_map.remove(&std::any::TypeId::of::<K>());
4400        }
4401    }
4402}
4403/// Geometry modifiers
4404/// Size of the view in logical pixels
4405#[derive(Debug, Clone, Copy, PartialEq)]
4406pub struct Size {
4407    pub width: f32,
4408    pub height: f32,
4409}
4410
4411impl Size {
4412    pub const ZERO: Self = Self {
4413        width: 0.0,
4414        height: 0.0,
4415    };
4416
4417    pub fn new(width: f32, height: f32) -> Self {
4418        Self { width, height }
4419    }
4420}
4421
4422/// Insets for padding
4423#[derive(Debug, Clone, Copy, PartialEq)]
4424pub struct EdgeInsets {
4425    pub top: f32,
4426    pub leading: f32,
4427    pub bottom: f32,
4428    pub trailing: f32,
4429}
4430
4431impl EdgeInsets {
4432    /// Equal insets on all edges
4433    pub fn all(value: f32) -> Self {
4434        Self {
4435            top: value,
4436            leading: value,
4437            bottom: value,
4438            trailing: value,
4439        }
4440    }
4441
4442    /// Vertical insets (top and bottom)
4443    pub fn vertical(value: f32) -> Self {
4444        Self {
4445            top: value,
4446            leading: 0.0,
4447            bottom: value,
4448            trailing: 0.0,
4449        }
4450    }
4451
4452    /// Horizontal insets (leading and trailing)
4453    pub fn horizontal(value: f32) -> Self {
4454        Self {
4455            top: 0.0,
4456            leading: value,
4457            bottom: 0.0,
4458            trailing: value,
4459        }
4460    }
4461}
4462
4463/// Modifier to set the size and alignment constraints of a view.
4464/// This determines the proposal size passed to the child and how the child is aligned
4465/// within the layout rect allocated to the frame.
4466#[derive(Debug, Clone, Copy, PartialEq)]
4467pub struct FrameModifier {
4468    /// Exact width to assign to the child view.
4469    pub width: Option<f32>,
4470    /// Exact height to assign to the child view.
4471    pub height: Option<f32>,
4472    /// Minimum width constraint for the view.
4473    pub min_width: Option<f32>,
4474    /// Maximum width constraint for the view.
4475    pub max_width: Option<f32>,
4476    /// Minimum height constraint for the view.
4477    pub min_height: Option<f32>,
4478    /// Maximum height constraint for the view.
4479    pub max_height: Option<f32>,
4480    /// The alignment strategy for positioning the child view within the frame.
4481    pub alignment: Alignment,
4482}
4483
4484impl Default for FrameModifier {
4485    /// Returns the default frame configuration which has no constraints and center alignment.
4486    fn default() -> Self {
4487        Self::new()
4488    }
4489}
4490
4491impl FrameModifier {
4492    /// Creates a new FrameModifier with all dimensions unspecified and center alignment.
4493    pub fn new() -> Self {
4494        Self {
4495            width: None,
4496            height: None,
4497            min_width: None,
4498            max_width: None,
4499            min_height: None,
4500            max_height: None,
4501            alignment: Alignment::Center,
4502        }
4503    }
4504
4505    /// Sets the fixed width of the frame.
4506    pub fn width(mut self, width: f32) -> Self {
4507        self.width = Some(width);
4508        self
4509    }
4510
4511    /// Sets the fixed height of the frame.
4512    pub fn height(mut self, height: f32) -> Self {
4513        self.height = Some(height);
4514        self
4515    }
4516
4517    /// Sets both the fixed width and height of the frame.
4518    pub fn size(mut self, width: f32, height: f32) -> Self {
4519        self.width = Some(width);
4520        self.height = Some(height);
4521        self
4522    }
4523
4524    /// Sets the minimum width constraint.
4525    pub fn min_width(mut self, min_width: f32) -> Self {
4526        self.min_width = Some(min_width);
4527        self
4528    }
4529
4530    /// Sets the maximum width constraint.
4531    pub fn max_width(mut self, max_width: f32) -> Self {
4532        self.max_width = Some(max_width);
4533        self
4534    }
4535
4536    /// Sets the minimum height constraint.
4537    pub fn min_height(mut self, min_height: f32) -> Self {
4538        self.min_height = Some(min_height);
4539        self
4540    }
4541
4542    /// Sets the maximum height constraint.
4543    pub fn max_height(mut self, max_height: f32) -> Self {
4544        self.max_height = Some(max_height);
4545        self
4546    }
4547
4548    /// Sets the alignment strategy for the child within the frame's layout bounds.
4549    pub fn alignment(mut self, alignment: Alignment) -> Self {
4550        self.alignment = alignment;
4551        self
4552    }
4553}
4554
4555impl ViewModifier for FrameModifier {
4556    /// Wraps the child view in a ModifiedView using this frame modifier.
4557    fn modify<V: View>(self, content: V) -> impl View {
4558        ModifiedView::new(content, self)
4559    }
4560
4561    /// Transforms the layout size proposal offered to the child to comply with frame constraints.
4562    fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
4563        let w = if let Some(width) = self.width {
4564            Some(width)
4565        } else {
4566            proposal.width.map(|pw| {
4567                pw.clamp(
4568                    self.min_width.unwrap_or(0.0),
4569                    self.max_width.unwrap_or(f32::INFINITY),
4570                )
4571            })
4572        };
4573        let h = if let Some(height) = self.height {
4574            Some(height)
4575        } else {
4576            proposal.height.map(|ph| {
4577                ph.clamp(
4578                    self.min_height.unwrap_or(0.0),
4579                    self.max_height.unwrap_or(f32::INFINITY),
4580                )
4581            })
4582        };
4583        SizeProposal {
4584            width: w,
4585            height: h,
4586        }
4587    }
4588
4589    /// Constraints and transforms the child's resulting size to fit the frame's bounds.
4590    fn transform_size(&self, child_size: Size) -> Size {
4591        let w = if let Some(width) = self.width {
4592            width
4593        } else {
4594            child_size.width.clamp(
4595                self.min_width.unwrap_or(0.0),
4596                self.max_width.unwrap_or(f32::INFINITY),
4597            )
4598        };
4599        let h = if let Some(height) = self.height {
4600            height
4601        } else {
4602            child_size.height.clamp(
4603                self.min_height.unwrap_or(0.0),
4604                self.max_height.unwrap_or(f32::INFINITY),
4605            )
4606        };
4607        Size {
4608            width: w,
4609            height: h,
4610        }
4611    }
4612
4613    /// Renders the frame's child view aligned within the layout rect.
4614    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4615        self.render(renderer, rect);
4616        let child_proposal =
4617            self.transform_proposal(SizeProposal::new(Some(rect.width), Some(rect.height)));
4618        let child_size = view.intrinsic_size(renderer, child_proposal);
4619
4620        let mut child_x = rect.x;
4621        let mut child_y = rect.y;
4622
4623        match self.alignment {
4624            Alignment::Leading => {
4625                child_y = rect.y + (rect.height - child_size.height) / 2.0;
4626            }
4627            Alignment::Trailing => {
4628                child_x = rect.x + rect.width - child_size.width;
4629                child_y = rect.y + (rect.height - child_size.height) / 2.0;
4630            }
4631            Alignment::Top => {
4632                child_x = rect.x + (rect.width - child_size.width) / 2.0;
4633            }
4634            Alignment::Bottom => {
4635                child_x = rect.x + (rect.width - child_size.width) / 2.0;
4636                child_y = rect.y + rect.height - child_size.height;
4637            }
4638            Alignment::Center => {
4639                child_x = rect.x + (rect.width - child_size.width) / 2.0;
4640                child_y = rect.y + (rect.height - child_size.height) / 2.0;
4641            }
4642        }
4643
4644        let child_rect = Rect {
4645            x: child_x,
4646            y: child_y,
4647            width: child_size.width,
4648            height: child_size.height,
4649        };
4650
4651        view.render(renderer, child_rect);
4652        self.post_render(renderer, rect);
4653    }
4654}
4655
4656/// Modifier to set the flex weight of a view
4657#[derive(Debug, Clone, Copy, PartialEq)]
4658pub struct FlexModifier {
4659    pub weight: f32,
4660}
4661
4662impl ViewModifier for FlexModifier {
4663    fn modify<V: View>(self, content: V) -> impl View {
4664        ModifiedView::new(content, self)
4665    }
4666
4667    fn child_flex_weight<V: View>(&self, _view: &V) -> f32 {
4668        self.weight
4669    }
4670}
4671
4672/// Modifier that specifies the column and row placement of a view inside a Grid layout.
4673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4674pub struct GridPlacementModifier {
4675    /// The grid placement settings containing column/row indexes and spans.
4676    pub placement: GridPlacement,
4677}
4678
4679impl ViewModifier for GridPlacementModifier {
4680    /// Wraps the child view in a ModifiedView using this modifier.
4681    fn modify<V: View>(self, content: V) -> impl View {
4682        ModifiedView::new(content, self)
4683    }
4684
4685    /// Exposes the grid placement metadata to parent layout engines.
4686    fn get_grid_placement(&self) -> Option<GridPlacement> {
4687        Some(self.placement)
4688    }
4689}
4690
4691/// Modifier to render a popover, tooltip, or menu view overlaying an anchored view.
4692/// It supports alignment positioning and outside-click dismissal.
4693#[derive(Clone)]
4694pub struct OverlayModifier {
4695    /// The overlay content view.
4696    pub overlay: AnyView,
4697    /// Where the overlay is aligned relative to the anchored view.
4698    pub alignment: Alignment,
4699    /// Additional offset in logical pixels.
4700    pub offset: [f32; 2],
4701    /// Optional dismissal callback triggered by click-outside events.
4702    pub on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
4703}
4704
4705impl ViewModifier for OverlayModifier {
4706    /// Wraps the child view in a ModifiedView using this overlay modifier.
4707    fn modify<V: View>(self, content: V) -> impl View {
4708        ModifiedView::new(content, self)
4709    }
4710
4711    /// Renders the overlay content positioned above the child view.
4712    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4713        // 1. Render primary anchored view
4714        view.render(renderer, rect);
4715
4716        // 2. Measure overlay content
4717        let overlay_size = self
4718            .overlay
4719            .intrinsic_size(renderer, SizeProposal::unspecified());
4720
4721        // 3. Align overlay rect relative to anchored rect
4722        let mut overlay_x;
4723        let mut overlay_y;
4724
4725        match self.alignment {
4726            Alignment::Leading => {
4727                overlay_x = rect.x - overlay_size.width;
4728                overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4729            }
4730            Alignment::Trailing => {
4731                overlay_x = rect.x + rect.width;
4732                overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4733            }
4734            Alignment::Top => {
4735                overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4736                overlay_y = rect.y - overlay_size.height;
4737            }
4738            Alignment::Bottom => {
4739                overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4740                overlay_y = rect.y + rect.height;
4741            }
4742            Alignment::Center => {
4743                overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4744                overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4745            }
4746        }
4747
4748        overlay_x += self.offset[0];
4749        overlay_y += self.offset[1];
4750
4751        let overlay_rect = Rect {
4752            x: overlay_x,
4753            y: overlay_y,
4754            width: overlay_size.width,
4755            height: overlay_size.height,
4756        };
4757
4758        // 4. Handle click-outside dismissal
4759        if let Some(on_dismiss) = &self.on_dismiss {
4760            let dismiss = on_dismiss.clone();
4761            renderer.register_handler(
4762                "pointerdown",
4763                Arc::new(move |event| {
4764                    if let Event::PointerDown { x, y, .. } = event {
4765                        let click_inside = x >= overlay_rect.x
4766                            && x <= overlay_rect.x + overlay_rect.width
4767                            && y >= overlay_rect.y
4768                            && y <= overlay_rect.y + overlay_rect.height;
4769                        if !click_inside {
4770                            dismiss();
4771                        }
4772                    }
4773                }),
4774            );
4775        }
4776
4777        // 5. Render overlay view
4778        self.overlay.render(renderer, overlay_rect);
4779    }
4780}
4781
4782/// Modifier to offset a view
4783#[derive(Debug, Clone, Copy, PartialEq)]
4784pub struct OffsetModifier {
4785    pub x: f32,
4786    pub y: f32,
4787}
4788
4789impl OffsetModifier {
4790    pub fn new(x: f32, y: f32) -> Self {
4791        Self { x, y }
4792    }
4793}
4794
4795impl ViewModifier for OffsetModifier {
4796    fn modify<V: View>(self, content: V) -> impl View {
4797        ModifiedView::new(content, self)
4798    }
4799}
4800
4801/// Modifier to set the z-index of a view
4802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4803pub struct ZIndexModifier {
4804    pub z_index: i32,
4805}
4806
4807impl ZIndexModifier {
4808    pub fn new(z_index: i32) -> Self {
4809        Self { z_index }
4810    }
4811}
4812
4813impl ViewModifier for ZIndexModifier {
4814    fn modify<V: View>(self, content: V) -> impl View {
4815        ModifiedView::new(content, self)
4816    }
4817}
4818
4819/// Layout constraints for views
4820#[derive(Debug, Clone, Copy, PartialEq, Default)]
4821pub struct LayoutConstraints {
4822    pub min_width: Option<f32>,
4823    pub max_width: Option<f32>,
4824    pub min_height: Option<f32>,
4825    pub max_height: Option<f32>,
4826}
4827
4828/// Modifier to set layout constraints
4829#[derive(Debug, Clone, Copy, PartialEq)]
4830pub struct LayoutModifier {
4831    pub constraints: LayoutConstraints,
4832}
4833
4834impl LayoutModifier {
4835    pub fn new(constraints: LayoutConstraints) -> Self {
4836        Self { constraints }
4837    }
4838}
4839
4840impl ViewModifier for LayoutModifier {
4841    fn modify<V: View>(self, content: V) -> impl View {
4842        ModifiedView::new(content, self)
4843    }
4844}
4845
4846/// Modifier to handle platform safe areas
4847#[derive(Debug, Clone, Copy, PartialEq)]
4848pub struct SafeAreaModifier {
4849    pub ignores: bool,
4850}
4851
4852impl ViewModifier for SafeAreaModifier {
4853    fn modify<V: View>(self, content: V) -> impl View {
4854        ModifiedView::new(content, self)
4855    }
4856}
4857
4858/// Modifier to add elevation (shadow) to a view
4859#[derive(Debug, Clone, Copy, PartialEq)]
4860pub struct ElevationModifier {
4861    pub level: f32,
4862}
4863
4864impl ViewModifier for ElevationModifier {
4865    fn modify<V: View>(self, content: V) -> impl View {
4866        ModifiedView::new(content, self)
4867    }
4868
4869    fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4870        if self.level > 0.0 {
4871            let radius = self.level * 2.0;
4872            let offset_y = self.level * 0.5;
4873            let shadow_color = [0.0, 0.0, 0.0, 0.3];
4874            renderer.push_shadow(radius, shadow_color, [0.0, offset_y]);
4875            view.render(renderer, rect);
4876            renderer.pop_shadow();
4877        } else {
4878            view.render(renderer, rect);
4879        }
4880    }
4881}
4882
4883// Layout subsystem
4884pub mod layout {
4885    use super::*;
4886
4887    /// Key used to identify a cached layout entry.
4888    /// Combines a view hash with a generation counter for cache invalidation.
4889    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4890    pub struct LayoutKey {
4891        pub view_hash: u64,
4892        pub generation: u64,
4893    }
4894
4895    // Layout pass scratch space
4896    pub struct LayoutCache {
4897        pub safe_area: SafeArea,
4898        pub delta_time: f32,
4899        /// Device scale factor for HiDPI / retina snapping. Defaults to 1.0.
4900        pub scale_factor: f32,
4901        /// The visible viewport bounds in logical pixels.
4902        /// If Some, layout execution can cull offscreen subtrees.
4903        pub viewport: Option<Rect>,
4904        /// Time budget for the layout pass. Defaults to 4.0ms.
4905        pub layout_time_budget: std::time::Duration,
4906        /// Start of the layout pass, captured at the beginning of the frame/layout run.
4907        pub layout_start_time: Option<std::time::Instant>,
4908        size_cache: HashMap<(u64, u32, u32), Size>, // (ViewHash, ProposalW, ProposalH)
4909        /// Map tracking child-to-parent view hash relationships for bottom-up invalidation.
4910        pub parent_map: HashMap<u64, u64>,
4911        /// Monotonically increasing generation counter for cache invalidation.
4912        /// When a view tree changes, bumping the generation causes stale entries
4913        /// to be treated as invalid without eagerly clearing the entire cache.
4914        generation: u64,
4915        /// Opaque pointer to the active layout engine (e.g. Taffy)
4916        pub engine: Option<Box<dyn std::any::Any + Send + Sync>>,
4917        /// Opaque pointer to the active animation orchestrator
4918        pub animators: Option<Box<dyn std::any::Any + Send + Sync>>,
4919        /// Cached previous rects for view transitions
4920        pub previous_rects: HashMap<u64, Rect>,
4921        /// Generation counter for cache eviction.
4922        /// Incremented each frame; entries not touched for N frames are evicted.
4923        pub eviction_generation: u64,
4924        /// Tracks which generation each previous_rects entry was last touched in.
4925        pub previous_rects_generation: HashMap<u64, u64>,
4926        /// Number of generations an entry can go untouched before eviction.
4927        eviction_threshold: u64,
4928    }
4929
4930    thread_local! {
4931        static LAYOUT_BUDGET_DEADLINE: std::cell::RefCell<Option<std::time::Instant>> =
4932            const { std::cell::RefCell::new(None) };
4933    }
4934
4935    impl Default for LayoutCache {
4936        fn default() -> Self {
4937            Self::new()
4938        }
4939    }
4940
4941    impl LayoutCache {
4942        pub fn new() -> Self {
4943            Self {
4944                safe_area: SafeArea::default(),
4945                delta_time: 0.016,
4946                scale_factor: 1.0,
4947                viewport: None,
4948                layout_time_budget: std::time::Duration::from_millis(4),
4949                layout_start_time: None,
4950                size_cache: HashMap::new(),
4951                parent_map: HashMap::new(),
4952                generation: 0,
4953                engine: None,
4954                animators: None,
4955                previous_rects: HashMap::new(),
4956                eviction_generation: 0,
4957                previous_rects_generation: HashMap::new(),
4958                eviction_threshold: 300, // ~5 seconds at 60fps
4959            }
4960        }
4961
4962        /// Returns the current generation counter.
4963        pub fn generation(&self) -> u64 {
4964            self.generation
4965        }
4966
4967        /// Evict entries from previous_rects that haven't been touched for N generations.
4968        pub fn evict_stale_entries(&mut self) {
4969            self.eviction_generation += 1;
4970            let threshold = self.eviction_threshold;
4971            let current_gen = self.eviction_generation;
4972            self.previous_rects.retain(|hash, _| {
4973                self.previous_rects_generation
4974                    .get(hash)
4975                    .map_or(false, |g| current_gen - *g < threshold)
4976            });
4977            self.previous_rects_generation
4978                .retain(|hash, _| self.previous_rects.contains_key(hash));
4979        }
4980
4981        /// Checks if the layout pass is currently running over its allocated time budget.
4982        pub fn is_over_budget(&self) -> bool {
4983            let deadline_red = LAYOUT_BUDGET_DEADLINE.with(|deadline| {
4984                deadline.borrow().as_ref().is_some_and(|deadline| std::time::Instant::now() >= *deadline)
4985            });
4986            if deadline_red {
4987                return true;
4988            }
4989            if let Some(start) = self.layout_start_time {
4990                start.elapsed() > self.layout_time_budget
4991            } else {
4992                false
4993            }
4994        }
4995
4996        /// Set a process-local deadline for layout cache consumers.
4997        /// When this deadline is exceeded, caches should reuse previous
4998        /// rects instead of recomputing expensive layout work.
4999        pub fn set_layout_budget_deadline(deadline: Option<std::time::Instant>) {
5000            LAYOUT_BUDGET_DEADLINE.with(|slot| {
5001                *slot.borrow_mut() = deadline;
5002            });
5003        }
5004
5005        /// Clear any process-local layout budget deadline.
5006        pub fn clear_layout_budget_deadline() {
5007            Self::set_layout_budget_deadline(None);
5008        }
5009
5010        /// Bump the generation counter, logically invalidating all cached entries
5011        /// without eagerly clearing them. Subsequent lookups with the old generation
5012        /// will miss until re-populated.
5013        pub fn invalidate(&mut self) {
5014            self.generation = self.generation.wrapping_add(1);
5015        }
5016
5017        /// Check whether a cached entry for the given key is still valid
5018        /// against the current generation.
5019        pub fn is_valid(&self, key: LayoutKey, current_gen: u64) -> bool {
5020            key.generation == current_gen && key.generation == self.generation
5021        }
5022
5023        pub fn clear(&mut self) {
5024            self.safe_area = SafeArea::default();
5025            self.viewport = None;
5026            self.layout_start_time = None;
5027            self.size_cache.clear();
5028            self.parent_map.clear();
5029        }
5030
5031        pub fn get_size(&self, view_hash: u64, proposal: SizeProposal) -> Option<Size> {
5032            let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5033            let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5034            self.size_cache.get(&(view_hash, pw, ph)).copied()
5035        }
5036
5037        pub fn set_size(&mut self, view_hash: u64, proposal: SizeProposal, size: Size) {
5038            let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5039            let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5040            self.size_cache.insert((view_hash, pw, ph), size);
5041        }
5042
5043        /// Register a child-to-parent layout relationship for bottom-up invalidation propagation.
5044        pub fn register_parent(&mut self, child_hash: u64, parent_hash: u64) {
5045            if child_hash != 0 && parent_hash != 0 {
5046                self.parent_map.insert(child_hash, parent_hash);
5047            }
5048        }
5049
5050        /// Remove all cached size entries for a specific view hash and propagate the invalidation
5051        /// bottom-up to all its layout ancestors to ensure consistent layout updates.
5052        pub fn invalidate_view(&mut self, view_hash: u64) {
5053            let mut to_invalidate = vec![view_hash];
5054            let mut visited = std::collections::HashSet::new();
5055            while let Some(hash) = to_invalidate.pop() {
5056                if !visited.insert(hash) {
5057                    continue;
5058                }
5059                self.size_cache.retain(|&(h, _, _), _| h != hash);
5060                if let Some(&parent) = self.parent_map.get(&hash) {
5061                    to_invalidate.push(parent);
5062                }
5063            }
5064        }
5065    }
5066
5067    /// Proposed size from parent view
5068    #[derive(Debug, Clone, Copy, PartialEq)]
5069    pub struct SizeProposal {
5070        pub width: Option<f32>,
5071        pub height: Option<f32>,
5072    }
5073
5074    impl SizeProposal {
5075        pub fn unspecified() -> Self {
5076            Self {
5077                width: None,
5078                height: None,
5079            }
5080        }
5081
5082        pub fn width(width: f32) -> Self {
5083            Self {
5084                width: Some(width),
5085                height: None,
5086            }
5087        }
5088
5089        pub fn height(height: f32) -> Self {
5090            Self {
5091                width: None,
5092                height: Some(height),
5093            }
5094        }
5095
5096        pub fn tight(width: f32, height: f32) -> Self {
5097            Self {
5098                width: Some(width),
5099                height: Some(height),
5100            }
5101        }
5102
5103        pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
5104            Self { width, height }
5105        }
5106    }
5107
5108    /// A view that can participate in layout
5109    pub trait LayoutView: Send {
5110        /// Propose a size for this view given the available space
5111        fn size_that_fits(
5112            &self,
5113            proposal: SizeProposal,
5114            subviews: &[&dyn LayoutView],
5115            cache: &mut LayoutCache,
5116        ) -> Size;
5117
5118        /// Place subviews within the given bounds
5119        fn place_subviews(
5120            &self,
5121            bounds: Rect,
5122            subviews: &mut [&mut dyn LayoutView],
5123            cache: &mut LayoutCache,
5124        );
5125
5126        /// Returns the flex weight of this view (default is 0.0, which means fixed/intrinsic)
5127        fn flex_weight(&self) -> f32 {
5128            0.0
5129        }
5130
5131        /// Returns a persistent unique identifier for this view to enable Layout View Transitions.
5132        /// Return 0 (default) to disable layout animations for this node.
5133        fn view_hash(&self) -> u64 {
5134            0
5135        }
5136
5137        /// Return true when this view's layout may have changed since the last pass.
5138        ///
5139        /// The layout engine uses this to skip cache lookups for views that are
5140        /// guaranteed static (e.g., chrome elements that never change between frames).
5141        /// Default true for backward compatibility -- override false for static subtrees.
5142        ///
5143        /// When false, the engine may skip `size_that_fits` entirely and reuse the
5144        /// cached rect from `LayoutCache::previous_rects`.
5145        fn changed(&self) -> bool {
5146            true
5147        }
5148
5149        /// Return a debug representation of this layout subtree.
5150        /// The `indent` parameter controls the indentation level for nested display.
5151        fn debug_layout(&self, indent: usize) -> String {
5152            let prefix = " ".repeat(indent);
5153            format!("{}LayoutView", prefix)
5154        }
5155    }
5156    /// Edge insets for padding, margins, and safe areas
5157    #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5158    pub struct EdgeInsets {
5159        pub top: f32,
5160        pub leading: f32,
5161        pub bottom: f32,
5162        pub trailing: f32,
5163    }
5164
5165    impl EdgeInsets {
5166        pub fn new(top: f32, leading: f32, bottom: f32, trailing: f32) -> Self {
5167            Self {
5168                top,
5169                leading,
5170                bottom,
5171                trailing,
5172            }
5173        }
5174
5175        pub fn all(value: f32) -> Self {
5176            Self {
5177                top: value,
5178                leading: value,
5179                bottom: value,
5180                trailing: value,
5181            }
5182        }
5183    }
5184
5185    /// SafeArea constraints provided by the platform
5186    #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5187    pub struct SafeArea {
5188        pub insets: EdgeInsets,
5189    }
5190
5191    /// SDF Shape definitions for Vili Interaction Paradigm hit-testing.
5192    #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5193    pub enum SdfShape {
5194        Rect(Rect),
5195        RoundedRect { rect: Rect, radius: f32 },
5196        Circle { center: [f32; 2], radius: f32 },
5197    }
5198
5199    /// Rectangle in logical pixels
5200    #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5201    pub struct Rect {
5202        pub x: f32,
5203        pub y: f32,
5204        pub width: f32,
5205        pub height: f32,
5206    }
5207
5208    impl Rect {
5209        pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
5210            Self {
5211                x,
5212                y,
5213                width,
5214                height,
5215            }
5216        }
5217
5218        pub fn inset(&self, amount: f32) -> Self {
5219            Self {
5220                x: self.x + amount,
5221                y: self.y + amount,
5222                width: (self.width - amount * 2.0).max(0.0),
5223                height: (self.height - amount * 2.0).max(0.0),
5224            }
5225        }
5226
5227        pub fn offset(&self, dx: f32, dy: f32) -> Self {
5228            Self {
5229                x: self.x + dx,
5230                y: self.y + dy,
5231                ..*self
5232            }
5233        }
5234
5235        pub fn zero() -> Self {
5236            Self {
5237                x: 0.0,
5238                y: 0.0,
5239                width: 0.0,
5240                height: 0.0,
5241            }
5242        }
5243
5244        pub fn contains(&self, x: f32, y: f32) -> bool {
5245            x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
5246        }
5247
5248        /// Determines whether this rectangle overlaps with another rectangle.
5249        ///
5250        /// # Contract
5251        /// Two rectangles overlap if their projection intervals on both the X
5252        /// and Y axes overlap. This is used for viewport intersection checks
5253        /// to determine visibility constraints during layout culling.
5254        pub fn intersects(&self, other: &Rect) -> bool {
5255            self.x < other.x + other.width
5256                && self.x + self.width > other.x
5257                && self.y < other.y + other.height
5258                && self.y + self.height > other.y
5259        }
5260
5261        pub fn size(&self) -> Size {
5262            Size {
5263                width: self.width,
5264                height: self.height,
5265            }
5266        }
5267
5268        /// Split the rect horizontally into N equal pieces
5269        pub fn split_horizontal(&self, n: usize) -> Vec<Rect> {
5270            if n == 0 {
5271                return vec![];
5272            }
5273            let item_width = self.width / n as f32;
5274            (0..n)
5275                .map(|i| Rect {
5276                    x: self.x + i as f32 * item_width,
5277                    y: self.y,
5278                    width: item_width,
5279                    height: self.height,
5280                })
5281                .collect()
5282        }
5283
5284        /// Split the rect vertically into N equal pieces
5285        pub fn split_vertical(&self, n: usize) -> Vec<Rect> {
5286            if n == 0 {
5287                return vec![];
5288            }
5289            let item_height = self.height / n as f32;
5290            (0..n)
5291                .map(|i| Rect {
5292                    x: self.x,
5293                    y: self.y + i as f32 * item_height,
5294                    width: self.width,
5295                    height: item_height,
5296                })
5297                .collect()
5298        }
5299    }
5300}
5301
5302// Size and FrameRenderer are pub items in this module; no re-export alias needed.
5303
5304pub mod agents;
5305pub mod animation;
5306pub mod gpu;
5307pub mod material;
5308pub mod runtime;
5309pub mod scene_graph;
5310pub mod sdf_shadow;
5311
5312// Re-export commonly used types
5313pub use layout::{LayoutCache, LayoutKey, LayoutView, Rect, SizeProposal};
5314pub use material::DrawMaterial;
5315pub use scene_graph::{NodeId, bifrost_registry};
5316pub use color::SemanticColors;
5317
5318// Duplicate AssetState removed - original definition at line 67
5319
5320/// AssetManager defines the interface for loading and caching external resources.
5321pub trait AssetManager: Send + Sync {
5322    /// Request an image asset. Returns the current state (Loading, Ready, or Error).
5323    fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>>;
5324
5325    /// Pre-load an image into the cache.
5326    fn preload_image(&self, url: &str);
5327}
5328
5329/// The phase of a touch or gesture event in its lifecycle.
5330#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5331pub enum TouchPhase {
5332    /// The touch/gesture has just begun.
5333    Began,
5334    /// The touch/gesture is moving.
5335    Moved,
5336    /// The touch/gesture has ended normally.
5337    Ended,
5338    /// The touch/gesture was cancelled (e.g., by the system).
5339    Cancelled,
5340}
5341
5342/// User input event types
5343#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
5344pub enum Event {
5345    PointerDown {
5346        x: f32,
5347        y: f32,
5348        button: u32,
5349        proximity_field: f32,
5350        tilt: Option<f32>,
5351        azimuth: Option<f32>,
5352        pressure: Option<f32>,
5353        barrel_rotation: Option<f32>,
5354        pointer_precision: f32,
5355    },
5356    PointerUp {
5357        x: f32,
5358        y: f32,
5359        button: u32,
5360        tilt: Option<f32>,
5361        azimuth: Option<f32>,
5362        pressure: Option<f32>,
5363        barrel_rotation: Option<f32>,
5364        pointer_precision: f32,
5365    },
5366    PointerMove {
5367        x: f32,
5368        y: f32,
5369        proximity_field: f32,
5370        tilt: Option<f32>,
5371        azimuth: Option<f32>,
5372        pressure: Option<f32>,
5373        barrel_rotation: Option<f32>,
5374        pointer_precision: f32,
5375    },
5376    PointerClick {
5377        x: f32,
5378        y: f32,
5379        button: u32,
5380        tilt: Option<f32>,
5381        azimuth: Option<f32>,
5382        pressure: Option<f32>,
5383        barrel_rotation: Option<f32>,
5384        pointer_precision: f32,
5385    },
5386    PointerEnter,
5387    PointerLeave,
5388    /// Mouse wheel / trackpad scroll event.
5389    /// `delta_x` is the horizontal scroll amount, `delta_y` is the vertical scroll amount (positive = scroll down).
5390    PointerWheel {
5391        x: f32,
5392        y: f32,
5393        delta_x: f32,
5394        delta_y: f32,
5395        pointer_precision: f32,
5396    },
5397    /// Double-click event (rapid successive clicks).
5398    PointerDoubleClick {
5399        x: f32,
5400        y: f32,
5401        button: u32,
5402        pointer_precision: f32,
5403    },
5404    /// Drag-and-drop: drag started (pointer moved while button held past threshold).
5405    DragStart {
5406        x: f32,
5407        y: f32,
5408        button: u32,
5409        pointer_precision: f32,
5410    },
5411    /// Drag-and-drop: drag in progress.
5412    DragMove {
5413        x: f32,
5414        y: f32,
5415        pointer_precision: f32,
5416    },
5417    /// Drag-and-drop: drag ended (pointer released).
5418    DragEnd {
5419        x: f32,
5420        y: f32,
5421        pointer_precision: f32,
5422    },
5423    KeyDown {
5424        key: String,
5425        modifiers: KeyModifiers,
5426    },
5427    KeyUp {
5428        key: String,
5429        modifiers: KeyModifiers,
5430    },
5431    /// Focus gained by a node.
5432    FocusIn,
5433    /// Focus lost by a node.
5434    FocusOut,
5435    /// Clipboard copy event.
5436    Copy,
5437    /// Clipboard cut event.
5438    Cut,
5439    /// Clipboard paste event with the pasted text content.
5440    Paste(String),
5441    /// Input Method Editor event (e.g. CJK character composition)
5442    Ime(String),
5443    /// Touch began at the given position.
5444    TouchStart {
5445        x: f32,
5446        y: f32,
5447        touch_id: u64,
5448    },
5449    /// Touch moved to a new position.
5450    TouchMove {
5451        x: f32,
5452        y: f32,
5453        touch_id: u64,
5454    },
5455    /// Touch ended at the given position.
5456    TouchEnd {
5457        x: f32,
5458        y: f32,
5459        touch_id: u64,
5460    },
5461    /// Touch cancelled.
5462    TouchCancel {
5463        touch_id: u64,
5464    },
5465    /// Multi-touch pinch gesture.
5466    /// `center` is the gesture anchor point in device-independent pixels.
5467    /// `scale` is the relative pinch scale (>1 = expand, <1 = contract).
5468    /// `velocity` is the instantaneous velocity of the pinch.
5469    /// `phase` indicates the current phase of the gesture lifecycle.
5470    GesturePinch {
5471        center: [f32; 2],
5472        scale: f32,
5473        velocity: f32,
5474        phase: TouchPhase,
5475    },
5476    /// Multi-touch swipe/pan gesture.
5477    /// `direction` is the normalized direction vector [dx, dy].
5478    /// `velocity` is the instantaneous velocity of the swipe.
5479    /// `phase` indicates the current phase of the gesture lifecycle.
5480    GestureSwipe {
5481        direction: [f32; 2],
5482        velocity: f32,
5483        phase: TouchPhase,
5484    },
5485    /// Drag-and-drop: external file dropped onto window.
5486    FileDrop {
5487        x: f32,
5488        y: f32,
5489        path: String,
5490    },
5491}
5492
5493impl Event {
5494    /// Returns the input pointer precision value in physical pixels if applicable.
5495    ///
5496    /// WHY: Used to scale hit-testing bounding boxes for proximity matching.
5497    /// CONTRACT: Mouse pointer inputs return low precision values (close to 0.0px),
5498    /// whereas touch inputs return larger values (e.g., 150.0px) for finger emulation.
5499    pub fn pointer_precision(&self) -> f32 {
5500        match self {
5501            Self::PointerDown {
5502                pointer_precision, ..
5503            }
5504            | Self::PointerUp {
5505                pointer_precision, ..
5506            }
5507            | Self::PointerMove {
5508                pointer_precision, ..
5509            }
5510            | Self::PointerClick {
5511                pointer_precision, ..
5512            }
5513            | Self::PointerWheel {
5514                pointer_precision, ..
5515            }
5516            | Self::PointerDoubleClick {
5517                pointer_precision, ..
5518            }
5519            | Self::DragStart {
5520                pointer_precision, ..
5521            }
5522            | Self::DragMove {
5523                pointer_precision, ..
5524            }
5525            | Self::DragEnd {
5526                pointer_precision, ..
5527            } => *pointer_precision,
5528            _ => 0.0,
5529        }
5530    }
5531
5532    /// Returns the canonical string name of the event for lookup in handler maps.
5533    pub fn name(&self) -> &'static str {
5534        match self {
5535            Self::PointerDown { .. } => "pointerdown",
5536            Self::PointerUp { .. } => "pointerup",
5537            Self::PointerMove { .. } => "pointermove",
5538            Self::PointerClick { .. } => "pointerclick",
5539            Self::PointerEnter => "pointerenter",
5540            Self::PointerLeave => "pointerleave",
5541            Self::PointerWheel { .. } => "pointerwheel",
5542            Self::PointerDoubleClick { .. } => "pointerdoubleclick",
5543            Self::DragStart { .. } => "dragstart",
5544            Self::DragMove { .. } => "dragmove",
5545            Self::DragEnd { .. } => "dragend",
5546            Self::KeyDown { .. } => "keydown",
5547            Self::KeyUp { .. } => "keyup",
5548            Self::FocusIn => "focusin",
5549            Self::FocusOut => "focusout",
5550            Self::Copy => "copy",
5551            Self::Cut => "cut",
5552            Self::Paste(_) => "paste",
5553            Self::Ime(_) => "ime",
5554            Self::TouchStart { .. } => "touchstart",
5555            Self::TouchMove { .. } => "touchmove",
5556            Self::TouchEnd { .. } => "touchend",
5557            Self::TouchCancel { .. } => "touchcancel",
5558            Self::GesturePinch { .. } => "gesturepinch",
5559            Self::GestureSwipe { .. } => "gestureswipe",
5560            Self::FileDrop { .. } => "filedrop",
5561        }
5562    }
5563}
5564
5565
5566/// Response from an event handler
5567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5568pub enum EventResponse {
5569    Handled,
5570    Ignored,
5571}
5572
5573// =========================================================================
5574// P1-40: EventPhase -- documents event propagation phases
5575// =========================================================================
5576//
5577// The CVKG event system follows the standard capture/target/bubble
5578// model used by the W3C DOM Event spec. When an event fires, it
5579// propagates through 3 phases:
5580//
5581// 1. Capture: the event travels from the root down to the
5582//    target's parent. Listeners registered for the capture
5583//    phase fire first.
5584// 2. Target: the event reaches the target node itself. Listeners
5585//    on the target fire (regardless of capture/bubble).
5586// 3. Bubble: the event travels back up from the target's
5587//    parent to the root. Listeners registered for the bubble
5588//    phase fire last.
5589//
5590// Cancellation: any handler can call Event::stop_propagation()
5591// to prevent the event from continuing to the next phase or
5592// the next node. This affects only the current event instance.
5593//
5594// Example: a click on a button inside a panel:
5595//  - panel's capture handler fires
5596//  - button's capture handler fires
5597//  - button's target handler fires
5598//  - button's bubble handler fires
5599//  - panel's bubble handler fires
5600//
5601// Use this enum when registering listeners to specify which
5602// phase to listen for.
5603#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5604pub enum EventPhase {
5605    /// Event is traveling from the root toward the target.
5606    Capture,
5607    /// Event has reached the target node.
5608    Target,
5609    /// Event is traveling from the target back toward the root.
5610    Bubble,
5611}
5612
5613impl EventPhase {
5614    /// All phases in propagation order.
5615    pub const ALL: [EventPhase; 3] = [
5616        EventPhase::Capture,
5617        EventPhase::Target,
5618        EventPhase::Bubble,
5619    ];
5620}
5621
5622/// A basic implementation of AssetManager that can be overridden by platform backends.
5623pub struct DefaultAssetManager {
5624    cache: AssetCache,
5625}
5626type AssetCache = Arc<arc_swap::ArcSwap<HashMap<String, AssetState<Arc<Vec<u8>>>>>>;
5627
5628impl Default for DefaultAssetManager {
5629    fn default() -> Self {
5630        Self::new()
5631    }
5632}
5633
5634impl DefaultAssetManager {
5635    pub fn new() -> Self {
5636        Self {
5637            cache: Arc::new(arc_swap::ArcSwap::from_pointee(HashMap::new())),
5638        }
5639    }
5640}
5641
5642impl AssetManager for DefaultAssetManager {
5643    fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>> {
5644        if let Some(state) = self.cache.load().get(url) {
5645            return state.clone();
5646        }
5647
5648        self.cache.rcu(|map| {
5649            let mut m = (**map).clone();
5650            m.entry(url.to_string()).or_insert(AssetState::Loading);
5651            m
5652        });
5653        AssetState::Loading
5654    }
5655
5656    fn preload_image(&self, _url: &str) {}
5657}
5658
5659use std::future::Future;
5660
5661/// Suspense wrapper for asynchronous state management.
5662/// Integrates with State<T> to provide loading/error/ready states for async operations.
5663pub struct Suspense<T: Clone + Send + Sync + 'static> {
5664    inner: State<AssetState<T>>,
5665}
5666
5667impl<T: Clone + Send + Sync + 'static> Default for Suspense<T> {
5668    fn default() -> Self {
5669        Self::new()
5670    }
5671}
5672
5673impl<T: Clone + Send + Sync + 'static> Suspense<T> {
5674    pub fn new() -> Self {
5675        Self {
5676            inner: State::new(AssetState::Loading),
5677        }
5678    }
5679
5680    pub fn new_async<F>(future: F) -> Self
5681    where
5682        F: Future<Output = Result<T, String>> + Send + 'static,
5683    {
5684        let suspense = Self::new();
5685        let suspense_clone = suspense.clone();
5686
5687        #[cfg(not(target_arch = "wasm32"))]
5688        {
5689            // P1-17 fix: use the shared fallback runtime instead of
5690            // spawning a new OS thread + runtime per call. If an
5691            // ambient tokio runtime exists, prefer it (preserves
5692            // caller intent). Otherwise use the shared fallback
5693            // runtime which is bounded to a small worker count.
5694            if let Ok(handle) = tokio::runtime::Handle::try_current() {
5695                handle.spawn(async move {
5696                    let result = future.await;
5697                    match result {
5698                        Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5699                        Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5700                    }
5701                });
5702            } else {
5703                fallback_runtime().spawn(async move {
5704                    let result = future.await;
5705                    match result {
5706                        Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5707                        Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5708                    }
5709                });
5710            }
5711        }
5712        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
5713        {
5714            wasm_bindgen_futures::spawn_local(async move {
5715                let result = future.await;
5716                match result {
5717                    Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5718                    Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5719                }
5720            });
5721        }
5722
5723        suspense
5724    }
5725
5726    pub fn ready(value: T) -> Self {
5727        Self {
5728            inner: State::new(AssetState::Ready(value)),
5729        }
5730    }
5731
5732    pub fn error(message: impl Into<String>) -> Self {
5733        Self {
5734            inner: State::new(AssetState::Error(message.into())),
5735        }
5736    }
5737
5738    pub fn get(&self) -> AssetState<T> {
5739        self.inner.get()
5740    }
5741
5742    pub fn get_ref(&self) -> AssetState<T> {
5743        self.inner.get()
5744    }
5745
5746    pub fn is_loading(&self) -> bool {
5747        matches!(self.get(), AssetState::Loading)
5748    }
5749
5750    pub fn is_ready(&self) -> bool {
5751        matches!(self.get(), AssetState::Ready(_))
5752    }
5753
5754    pub fn is_error(&self) -> bool {
5755        matches!(self.get(), AssetState::Error(_))
5756    }
5757
5758    pub fn ready_value(&self) -> Option<T> {
5759        match self.get() {
5760            AssetState::Ready(value) => Some(value),
5761            _ => None,
5762        }
5763    }
5764
5765    pub fn error_message(&self) -> Option<String> {
5766        match self.get() {
5767            AssetState::Error(message) => Some(message),
5768            _ => None,
5769        }
5770    }
5771
5772    pub fn subscribe<F: Fn(&AssetState<T>) + Send + Sync + 'static>(&self, callback: F) {
5773        self.inner.subscribe(callback)
5774    }
5775
5776    pub fn inner_state(&self) -> &State<AssetState<T>> {
5777        &self.inner
5778    }
5779}
5780
5781impl<T: Clone + Send + Sync + 'static> Clone for Suspense<T> {
5782    fn clone(&self) -> Self {
5783        Self {
5784            inner: self.inner.clone(),
5785        }
5786    }
5787}
5788
5789impl<T: Clone + Send + Sync + 'static> From<T> for Suspense<T> {
5790    fn from(value: T) -> Self {
5791        Self::ready(value)
5792    }
5793}
5794
5795impl<T: Clone + Send + Sync + 'static> From<Result<T, String>> for Suspense<T> {
5796    fn from(result: Result<T, String>) -> Self {
5797        match result {
5798            Ok(value) => Self::ready(value),
5799            Err(error) => Self::error(error),
5800        }
5801    }
5802}
5803
5804#[cfg(test)]
5805mod phase1_test;
5806
5807/// Berserker mode states for the rendering pipeline.
5808#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5809pub enum BerserkerMode {
5810    Normal,
5811    Rage,    // Red tint, slight shake
5812    Frenzy,  // Heavy red tint, motion blur, aggressive shake
5813    GodMode, // Golden aura, lightning arcs
5814}
5815
5816/// Seer trait for AI-assisted UI components.
5817/// Allows components to receive "prophecies" (predictions) from an AI backend.
5818pub trait Seer: Send + Sync {
5819    /// Provide a prediction for the next user action or content.
5820    fn predict(&self, context: &str) -> String;
5821    /// Stream real-time "whispers" (transcriptions/intent).
5822    fn whispers(&self) -> Vec<String>;
5823}
5824
5825#[cfg(test)]
5826mod vili_tests {
5827    use super::*;
5828
5829    struct DummyRenderer;
5830    impl ElapsedTime for DummyRenderer {
5831        fn elapsed_time(&self) -> f32 {
5832            0.0
5833        }
5834        fn delta_time(&self) -> f32 {
5835            0.0
5836        }
5837    }
5838    impl Renderer for DummyRenderer {
5839        fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
5840        fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
5841        fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
5842        fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5843        fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
5844        fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5845        fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
5846
5847        fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
5848        fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
5849        }
5850        fn set_camera_3d(&mut self, _camera: &Camera3D) {}
5851        fn push_transform_3d(&mut self, _transform: &Transform3D) {}
5852        fn pop_transform_3d(&mut self) {}
5853    }
5854
5855    #[test]
5856    fn test_magnetic_warp() {
5857        let renderer = DummyRenderer;
5858        let anchor = Rect {
5859            x: 100.0,
5860            y: 100.0,
5861            width: 50.0,
5862            height: 50.0,
5863        };
5864        // Pointer is near the anchor (distance < 120)
5865        let pointer = [125.0, 50.0];
5866        // distance from center (125, 125) is 75.
5867        // force = (1.0 - 75/120) * strength
5868        let warp = renderer.magnetic_warp(pointer, anchor, 1.0);
5869        // It should pull closer to (125, 125), so Y should be > 50
5870        assert!(warp[1] > 50.0);
5871
5872        // Out of range pointer should remain unchanged
5873        let far_pointer = [500.0, 500.0];
5874        let far_warp = renderer.magnetic_warp(far_pointer, anchor, 1.0);
5875        assert_eq!(far_pointer, far_warp);
5876    }
5877
5878    #[test]
5879    fn test_mani_glow() {
5880        let renderer = DummyRenderer;
5881        let bounds = Rect {
5882            x: 0.0,
5883            y: 0.0,
5884            width: 100.0,
5885            height: 100.0,
5886        };
5887        let pointer_inside = [50.0, 50.0];
5888        let glow_max = renderer.mani_glow_intensity(pointer_inside, bounds, 120.0);
5889        assert_eq!(glow_max, 1.0);
5890
5891        let pointer_edge = [50.0, -10.0];
5892        let glow_partial = renderer.mani_glow_intensity(pointer_edge, bounds, 120.0);
5893        assert!(glow_partial > 0.0 && glow_partial < 1.0);
5894    }
5895
5896    #[test]
5897    fn test_fafnir_evolve() {
5898        let renderer = DummyRenderer;
5899        let bounds = Rect {
5900            x: 0.0,
5901            y: 0.0,
5902            width: 100.0,
5903            height: 100.0,
5904        };
5905        let pointer_inside = [50.0, 50.0];
5906        let scale = renderer.fafnir_evolve(pointer_inside, bounds, 1.2);
5907        assert_eq!(scale, 1.2); // Full scale when hovering center
5908    }
5909
5910    #[test]
5911    fn test_undo_manager_basic() {
5912        let mut manager = UndoManager::new(3, 0.5);
5913        let val = std::sync::Arc::new(std::sync::Mutex::new(0));
5914
5915        let v1 = val.clone();
5916        let v2 = val.clone();
5917        manager.push(
5918            "Add",
5919            move || *v1.lock().unwrap() -= 1,
5920            move || *v2.lock().unwrap() += 1,
5921        );
5922
5923        assert!(manager.can_undo());
5924        assert!(!manager.can_redo());
5925
5926        let undo = manager.undo().unwrap();
5927        undo();
5928        assert_eq!(*val.lock().unwrap(), -1);
5929        assert!(!manager.can_undo());
5930        assert!(manager.can_redo());
5931
5932        let redo = manager.redo().unwrap();
5933        redo();
5934        assert_eq!(*val.lock().unwrap(), 0);
5935    }
5936
5937    #[test]
5938    fn test_undo_manager_depth_limit() {
5939        let mut manager = UndoManager::new(2, 0.5);
5940        manager.push("1", || {}, || {});
5941        manager.push("2", || {}, || {});
5942        manager.push("3", || {}, || {});
5943
5944        assert_eq!(manager.stack.len(), 2);
5945        assert_eq!(manager.position, 2);
5946    }
5947
5948    #[test]
5949    fn test_undo_manager_coalescing() {
5950        let mut manager = UndoManager::new(10, 1.0);
5951        let count = std::sync::Arc::new(std::sync::Mutex::new(0));
5952
5953        let c = count.clone();
5954        manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
5955
5956        let c = count.clone();
5957        manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
5958
5959        assert_eq!(manager.stack.len(), 1);
5960
5961        let undo = manager.undo().unwrap();
5962        undo();
5963        assert_eq!(*count.lock().unwrap(), -2);
5964    }
5965}
5966
5967#[cfg(test)]
5968mod error_boundary_tests {
5969    use super::*;
5970
5971    /// A trivial view that renders successfully.
5972    struct SuccessView;
5973
5974    impl View for SuccessView {
5975        type Body = Never;
5976        fn body(self) -> Never {
5977            // SAFETY: `Never` is uninhabitable -- test helper with no composable body.
5978            unreachable!()
5979        }
5980        fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
5981            // No-op -- renders successfully.
5982        }
5983    }
5984
5985    /// A view that panics during render.
5986    struct PanicOnRender;
5987
5988    impl View for PanicOnRender {
5989        type Body = Never;
5990        fn body(self) -> Never {
5991            // SAFETY: `Never` is uninhabitable -- test helper that only panics in render().
5992            unreachable!()
5993        }
5994        fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
5995            panic!("intentional render panic");
5996        }
5997    }
5998
5999    /// A view that panics during intrinsic_size.
6000    struct PanicOnSize;
6001
6002    impl View for PanicOnSize {
6003        type Body = Never;
6004        fn body(self) -> Never {
6005            // SAFETY: `Never` is uninhabitable -- test helper that only panics in intrinsic_size().
6006            unreachable!()
6007        }
6008        fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6009            // Render succeeds, but size panics.
6010        }
6011        fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
6012            panic!("intentional size panic");
6013        }
6014    }
6015
6016    /// A view that panics with a String payload.
6017    struct PanicWithString;
6018
6019    impl View for PanicWithString {
6020        type Body = Never;
6021        fn body(self) -> Never {
6022            // SAFETY: `Never` is uninhabitable -- test helper that panics with a String payload.
6023            unreachable!()
6024        }
6025        fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6026            panic!("{}", "custom error message".to_string());
6027        }
6028    }
6029
6030    struct DummyRenderer;
6031    impl ElapsedTime for DummyRenderer {
6032        fn elapsed_time(&self) -> f32 {
6033            0.0
6034        }
6035        fn delta_time(&self) -> f32 {
6036            0.0
6037        }
6038    }
6039    impl Renderer for DummyRenderer {
6040        fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
6041        fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
6042        fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
6043        fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6044        fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
6045        fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6046        fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6047
6048        fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6049        fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
6050        }
6051        fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6052        fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6053        fn pop_transform_3d(&mut self) {}
6054    }
6055
6056    const TEST_RECT: Rect = Rect {
6057        x: 0.0,
6058        y: 0.0,
6059        width: 200.0,
6060        height: 100.0,
6061    };
6062
6063    #[test]
6064    fn error_boundary_renders_child_on_success() {
6065        let boundary = ErrorBoundary::new(SuccessView);
6066        let mut renderer = DummyRenderer;
6067
6068        boundary.render(&mut renderer, TEST_RECT);
6069
6070        assert!(
6071            !boundary.has_error(),
6072            "should not have error after successful render"
6073        );
6074        assert!(
6075            boundary.last_error().is_none(),
6076            "should have no error message"
6077        );
6078    }
6079
6080    #[test]
6081    fn error_boundary_catches_render_panic() {
6082        let boundary = ErrorBoundary::new(PanicOnRender);
6083        let mut renderer = DummyRenderer;
6084
6085        // This must NOT panic -- the boundary catches it.
6086        boundary.render(&mut renderer, TEST_RECT);
6087
6088        assert!(
6089            boundary.has_error(),
6090            "should have error after catching panic"
6091        );
6092        let err = boundary.last_error().expect("should have error message");
6093        assert!(
6094            err.contains("intentional render panic"),
6095            "error message should contain panic message, got: {err}"
6096        );
6097    }
6098
6099    #[test]
6100    fn error_boundary_catches_size_panic() {
6101        let boundary = ErrorBoundary::new(PanicOnSize);
6102        let mut renderer = DummyRenderer;
6103        let proposal = layout::SizeProposal {
6104            width: Some(100.0),
6105            height: Some(50.0),
6106        };
6107
6108        let size = boundary.intrinsic_size(&mut renderer, proposal);
6109
6110        assert!(
6111            boundary.has_error(),
6112            "should have error after catching size panic"
6113        );
6114        assert_eq!(size, Size::ZERO, "fallback size should be zero");
6115    }
6116
6117    #[test]
6118    fn error_boundary_catches_string_panic() {
6119        let boundary = ErrorBoundary::new(PanicWithString);
6120        let mut renderer = DummyRenderer;
6121
6122        boundary.render(&mut renderer, TEST_RECT);
6123
6124        assert!(boundary.has_error());
6125        let err = boundary.last_error().expect("should have error message");
6126        assert!(
6127            err.contains("custom error message"),
6128            "should capture String panic payload, got: {err}"
6129        );
6130    }
6131
6132    #[test]
6133    fn error_boundary_clear_error_resets_state() {
6134        let boundary = ErrorBoundary::new(PanicOnRender);
6135        let mut renderer = DummyRenderer;
6136
6137        boundary.render(&mut renderer, TEST_RECT);
6138        assert!(boundary.has_error());
6139
6140        boundary.clear_error();
6141        assert!(
6142            !boundary.has_error(),
6143            "should be clear after clear_error()"
6144        );
6145        assert!(
6146            boundary.last_error().is_none(),
6147            "error message should be cleared"
6148        );
6149    }
6150
6151    #[test]
6152    fn error_boundary_fallback_color_is_configurable() {
6153        let boundary = ErrorBoundary::new(SuccessView)
6154            .fallback_color([0.0, 0.0, 1.0, 1.0])
6155            .fallback_label("custom label");
6156
6157        assert_eq!(boundary.fallback_color, [0.0, 0.0, 1.0, 1.0]);
6158        assert_eq!(
6159            boundary.fallback_label.as_deref(),
6160            Some("custom label")
6161        );
6162    }
6163
6164    #[test]
6165    fn error_boundary_flex_weight_delegates_to_child() {
6166        let boundary = ErrorBoundary::new(SuccessView);
6167        assert_eq!(boundary.flex_weight(), 0.0, "should delegate to child (default 0.0)");
6168    }
6169
6170    #[test]
6171    fn error_boundary_body_delegates_to_child() {
6172        // body() must be pure and delegate directly.
6173        let _boundary = ErrorBoundary::new(SuccessView);
6174        // Calling body() should not panic and should return Never (unreachable).
6175        // We test this indirectly -- if it compiles and the Never type is correct,
6176        // the body() call would diverge. We just verify the type compiles.
6177        let _boundary_type = std::any::type_name::<ErrorBoundary<SuccessView>>();
6178    }
6179
6180    /// Renderer that tracks stack-pushing operations so tests can verify
6181    /// ErrorBoundary restores renderer state on panic.
6182    struct TrackingRenderer {
6183        clip_depth: u32,
6184        opacity_depth: u32,
6185        shadow_depth: u32,
6186    }
6187
6188    impl TrackingRenderer {
6189        fn new() -> Self {
6190            Self {
6191                clip_depth: 0,
6192                opacity_depth: 0,
6193                shadow_depth: 0,
6194            }
6195        }
6196    }
6197
6198    impl Renderer for TrackingRenderer {
6199        fn fill_rect(&mut self, _rect: Rect, _color: [f32; 4]) {}
6200        fn fill_rounded_rect(&mut self, _rect: Rect, _radius: f32, _color: [f32; 4]) {}
6201        fn fill_ellipse(&mut self, _rect: Rect, _color: [f32; 4]) {}
6202        fn stroke_rect(&mut self, _rect: Rect, _color: [f32; 4], _w: f32) {}
6203        fn stroke_rounded_rect(
6204            &mut self,
6205            _rect: Rect,
6206            _radius: f32,
6207            _color: [f32; 4],
6208            _stroke_width: f32,
6209        ) {
6210        }
6211        fn stroke_ellipse(&mut self, _rect: Rect, _color: [f32; 4], _stroke_width: f32) {}
6212        fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6213
6214        fn push_clip_rect(&mut self, _rect: Rect) {
6215            self.clip_depth += 1;
6216        }
6217        fn pop_clip_rect(&mut self) {
6218            self.clip_depth = self.clip_depth.saturating_sub(1);
6219        }
6220        fn push_opacity(&mut self, _opacity: f32) {
6221            self.opacity_depth += 1;
6222        }
6223        fn pop_opacity(&mut self) {
6224            self.opacity_depth = self.opacity_depth.saturating_sub(1);
6225        }
6226        fn push_shadow(&mut self, _r: f32, _c: [f32; 4], _o: [f32; 2]) {
6227            self.shadow_depth += 1;
6228        }
6229        fn pop_shadow(&mut self) {
6230            self.shadow_depth = self.shadow_depth.saturating_sub(1);
6231        }
6232        fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6233        fn snapshot_render_state(&self) -> RenderStateSnapshot {
6234            // Note: cannot mutate self in &self method; we record that it was
6235            // called via a different channel (the test counts calls on a Cell).
6236            RenderStateSnapshot {
6237                clip_depth: self.clip_depth,
6238                opacity_depth: self.opacity_depth,
6239                slice_depth: 0,
6240                shadow_depth: self.shadow_depth,
6241                transform_depth: 0,
6242                vnode_depth: 0,
6243            }
6244        }
6245        fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
6246            self.clip_depth = snap.clip_depth;
6247            self.opacity_depth = snap.opacity_depth;
6248            self.shadow_depth = snap.shadow_depth;
6249        }
6250        fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
6251        fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6252        fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6253        fn pop_transform_3d(&mut self) {}
6254    }
6255
6256    impl ElapsedTime for TrackingRenderer {
6257        fn elapsed_time(&self) -> f32 {
6258            0.0
6259        }
6260        fn delta_time(&self) -> f32 {
6261            0.0
6262        }
6263    }
6264
6265    /// View that pushes clip/opacity/shadow stacks and then panics.
6266    /// After ErrorBoundary restores state, the renderer should have no leftover
6267    /// pushed items.
6268    struct StackPushingPanicView;
6269
6270    impl View for StackPushingPanicView {
6271        type Body = Never;
6272        fn body(self) -> Never {
6273            unreachable!()
6274        }
6275        fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
6276            renderer.push_clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0));
6277            renderer.push_opacity(0.5);
6278            renderer.push_shadow(2.0, [0.0, 0.0, 0.0, 0.5], [0.0, 0.0]);
6279            panic!("intentional stack-pushing panic");
6280        }
6281    }
6282
6283    #[test]
6284    fn error_boundary_restores_renderer_state_on_panic() {
6285        // Regression test for P0-5: ErrorBoundary must restore renderer
6286        // stack state after a mid-render panic so siblings drawn afterward
6287        // don't inherit leaked clip/opacity/transform/etc. state.
6288        let boundary = ErrorBoundary::new(StackPushingPanicView);
6289        let mut renderer = TrackingRenderer::new();
6290
6291        // Pre-snapshot: empty stacks.
6292        let snap_before = renderer.snapshot_render_state();
6293        assert_eq!(snap_before.clip_depth, 0);
6294        assert_eq!(snap_before.opacity_depth, 0);
6295        assert_eq!(snap_before.shadow_depth, 0);
6296
6297        // Render -- child panics, boundary must catch and restore.
6298        boundary.render(&mut renderer, TEST_RECT);
6299
6300        // Verify the panic was caught and state was restored.
6301        assert!(boundary.has_error(), "should have caught the panic");
6302        let snap_after = renderer.snapshot_render_state();
6303        assert_eq!(
6304            snap_after.clip_depth, 0,
6305            "clip stack should be restored to empty after panic"
6306        );
6307        assert_eq!(
6308            snap_after.opacity_depth, 0,
6309            "opacity stack should be restored to empty after panic"
6310        );
6311        assert_eq!(
6312            snap_after.shadow_depth, 0,
6313            "shadow stack should be restored to empty after panic"
6314        );
6315    }
6316
6317    #[test]
6318    fn render_state_snapshot_default_is_zeroed() {
6319        // The default snapshot must be all-zero so backends without
6320        // stack state can use it as a sentinel.
6321        let snap = RenderStateSnapshot::default();
6322        assert_eq!(snap.clip_depth, 0);
6323        assert_eq!(snap.opacity_depth, 0);
6324        assert_eq!(snap.slice_depth, 0);
6325        assert_eq!(snap.shadow_depth, 0);
6326        assert_eq!(snap.transform_depth, 0);
6327        assert_eq!(snap.vnode_depth, 0);
6328    }
6329
6330    #[test]
6331    fn render_state_snapshot_round_trip() {
6332        let snap = RenderStateSnapshot {
6333            clip_depth: 3,
6334            opacity_depth: 2,
6335            slice_depth: 1,
6336            shadow_depth: 0,
6337            transform_depth: 4,
6338            vnode_depth: 5,
6339        };
6340        let copied = snap;
6341        assert_eq!(copied, snap);
6342    }
6343}
6344
6345// =============================================================================
6346// THEME CONTEXT -- Thread-local theme access for components
6347// =============================================================================
6348//
6349// Components call `use_theme()` to get the current SemanticColors.
6350// The native renderer sets this via `set_current_theme()` before each frame.
6351// Falls back to dark theme defaults if no theme has been set.
6352//
6353// We store SemanticColors directly (not the full Theme) to avoid depending
6354// on cvkg-themes from cvkg-core. The colors are cloned into thread-local storage.
6355
6356use std::cell::RefCell;
6357
6358thread_local! {
6359    /// Thread-local semantic colors for the current frame.
6360    static THEME_CONTEXT: RefCell<Option<color::SemanticColors>> = const { RefCell::new(None) };
6361}
6362
6363/// Semantic colors extracted from the theme for use by components.
6364/// This is a standalone type defined in cvkg-core so cvkg-components
6365/// can use it without depending on cvkg-themes.
6366///
6367/// Components should access these via `use_theme()` rather than hardcoding RGBA.
6368
6369/// Set the current semantic colors for this thread.
6370/// Called by the native renderer before each frame.
6371pub fn set_current_theme(colors: color::SemanticColors) {
6372    THEME_CONTEXT.with(|cell| {
6373        *cell.borrow_mut() = Some(colors);
6374    });
6375}
6376
6377/// Clear the current theme. Called after each frame.
6378pub fn clear_current_theme() {
6379    THEME_CONTEXT.with(|cell| {
6380        *cell.borrow_mut() = None;
6381    });
6382}
6383
6384/// Access the current semantic colors from within a component's `render()` method.
6385///
6386/// Returns the colors set by the most recent `set_current_theme()` call.
6387/// Falls back to dark theme defaults if no theme has been set.
6388///
6389/// # Example
6390/// ```no_run
6391/// use cvkg_core::{use_theme, Renderer, Rect};
6392///
6393/// fn render_button(renderer: &mut dyn Renderer, rect: Rect) {
6394///     let colors = use_theme();
6395///     renderer.fill_rounded_rect(rect, 8.0, [colors.accent.r, colors.accent.g, colors.accent.b, colors.accent.a]);
6396/// }
6397/// ```
6398pub fn use_theme() -> color::SemanticColors {
6399    THEME_CONTEXT.with(|cell| {
6400        cell.borrow()
6401            .clone()
6402            .unwrap_or_else(color::SemanticColors::dark)
6403    })
6404}
6405
6406// =============================================================================
6407// COLOR MODULE -- Standalone semantic colors type
6408// =============================================================================
6409//
6410// This module provides `SemanticColors`, a self-contained color palette that
6411// components can use without depending on `cvkg-themes`. The `use_theme()`
6412// function returns the current `SemanticColors` from thread-local storage.
6413
6414pub mod color {
6415    use super::Color;
6416
6417    /// A complete set of semantic colors for UI components.
6418    ///
6419    /// Each color serves a specific role in the UI. Components should reference
6420    /// these semantic roles rather than hardcoding RGBA values.
6421    ///
6422    /// # Example
6423    /// ```no_run
6424    /// use cvkg_core::{use_theme, Renderer, Rect};
6425    ///
6426    /// fn render_button(renderer: &mut dyn Renderer, rect: Rect) {
6427    ///     let colors = use_theme();
6428    ///     // Use accent color for the button background
6429    ///     renderer.fill_rounded_rect(rect, 8.0,
6430    ///         [colors.accent.r, colors.accent.g, colors.accent.b, colors.accent.a]);
6431    /// }
6432    /// ```
6433    #[derive(Debug, Clone)]
6434    pub struct SemanticColors {
6435        /// Primary brand color -- used for key interactive elements.
6436        pub primary: Color,
6437        /// Secondary color -- used for less prominent interactive elements.
6438        pub secondary: Color,
6439        /// Accent color -- used for highlights, focus rings, CTAs.
6440        pub accent: Color,
6441        /// Page/window background color.
6442        pub background: Color,
6443        /// Surface color -- used for cards, panels, sheets.
6444        pub surface: Color,
6445        /// Error color -- used for destructive actions, error messages.
6446        pub error: Color,
6447        /// Warning color -- used for caution indicators.
6448        pub warning: Color,
6449        /// Success color -- used for positive feedback.
6450        pub success: Color,
6451        /// Primary text color.
6452        pub text: Color,
6453        /// Dimmed/disabled text color.
6454        pub text_dim: Color,
6455    }
6456
6457    impl SemanticColors {
6458        /// Dark theme semantic colors (default fallback).
6459        pub fn dark() -> Self {
6460            Self {
6461                primary: Color::new(1.0, 0.84, 0.0, 1.0),      // Viking Gold
6462                secondary: Color::new(1.0, 0.0, 1.0, 1.0),     // Magenta Liquid
6463                accent: Color::new(1.0, 0.0, 0.4, 1.0),        // Crimson Flash
6464                background: Color::new(0.02, 0.02, 0.05, 1.0), // Deep Void
6465                surface: Color::new(0.05, 0.05, 0.07, 1.0),    // Tactical Obsidian
6466                error: Color::new(1.0, 0.2, 0.2, 1.0),         // Red
6467                warning: Color::new(1.0, 0.8, 0.0, 1.0),       // Yellow
6468                success: Color::new(0.0, 1.0, 0.5, 1.0),       // Green
6469                text: Color::new(0.95, 0.95, 1.0, 1.0),        // Near-white
6470                text_dim: Color::new(0.6, 0.6, 0.7, 1.0),      // Gray
6471            }
6472        }
6473
6474        /// Light theme semantic colors.
6475        pub fn light() -> Self {
6476            Self {
6477                primary: Color::new(0.35, 0.30, 0.70, 1.0),
6478                secondary: Color::new(0.30, 0.50, 0.30, 1.0),
6479                accent: Color::new(0.30, 0.35, 0.75, 1.0),
6480                background: Color::new(0.97, 0.97, 0.98, 1.0),
6481                surface: Color::new(0.93, 0.93, 0.95, 1.0),
6482                error: Color::new(0.75, 0.15, 0.15, 1.0),
6483                warning: Color::new(0.80, 0.60, 0.0, 1.0),
6484                success: Color::new(0.15, 0.65, 0.30, 1.0),
6485                text: Color::new(0.08, 0.08, 0.10, 1.0),
6486                text_dim: Color::new(0.40, 0.40, 0.45, 1.0),
6487            }
6488        }
6489
6490        /// Convert the accent color semantic color into interactive state colors.
6491        ///
6492        /// This provides hover/active/focus/disabled variants derived from the
6493        /// accent color, matching the pattern that `cvkg-themes::StateColors` uses.
6494        pub fn accent_states(&self) -> InteractiveColorStates {
6495            InteractiveColorStates::from_color(self.accent)
6496        }
6497
6498        /// Convert the primary color into interactive state colors.
6499        pub fn primary_states(&self) -> InteractiveColorStates {
6500            InteractiveColorStates::from_color(self.primary)
6501        }
6502
6503        /// Convert the error color into interactive state colors.
6504        pub fn error_states(&self) -> InteractiveColorStates {
6505            InteractiveColorStates::from_color(self.error)
6506        }
6507
6508        /// Convert the success color into interactive state colors.
6509        pub fn success_states(&self) -> InteractiveColorStates {
6510            InteractiveColorStates::from_color(self.success)
6511        }
6512    }
6513
6514    /// Interactive state colors derived from a single base color.
6515    ///
6516    /// Provides hover/active/focus/disabled variants for any color,
6517    /// derived via simple lightness adjustments in sRGB space.
6518    #[derive(Debug, Clone)]
6519    pub struct InteractiveColorStates {
6520        pub default: Color,
6521        pub hover: Color,
6522        pub active: Color,
6523        pub focus: Color,
6524        pub disabled: Color,
6525        pub focus_ring: Color,
6526    }
6527
6528    impl InteractiveColorStates {
6529        /// Derive interactive state colors from a base sRGB color.
6530        ///
6531        /// Uses simple lightness adjustments:
6532        /// - Hover: +15% lightness
6533        /// - Active: -15% lightness
6534        /// - Focus: same as default
6535        /// - Disabled: 40% opacity
6536        /// - Focus ring: base color at 70% opacity
6537        pub fn from_color(base: Color) -> Self {
6538            Self {
6539                default: base,
6540                hover: base.lighten(0.15),
6541                active: base.darken(0.15),
6542                focus: base,
6543                disabled: Color::new(base.r, base.g, base.b, base.a * 0.4),
6544                focus_ring: Color::new(base.r, base.g, base.b, base.a * 0.7),
6545            }
6546        }
6547
6548        /// Get the color for a specific interactive state.
6549        pub fn color_for(&self, state: InteractiveState) -> Color {
6550            match state {
6551                InteractiveState::Default => self.default,
6552                InteractiveState::Hover => self.hover,
6553                InteractiveState::Active => self.active,
6554                InteractiveState::Focus => self.focus,
6555                InteractiveState::Disabled => self.disabled,
6556            }
6557        }
6558    }
6559
6560    /// Interactive state for a component.
6561    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6562    pub enum InteractiveState {
6563        Default,
6564        Hover,
6565        Active,
6566        Focus,
6567        Disabled,
6568    }
6569}
6570
6571// =============================================================================
6572// USE_STATE HOOK -- Local component state with automatic re-render
6573// =============================================================================
6574//
6575// Components call `use_state(id, initial)` to get a `(getter, setter)` pair.
6576// The setter updates the global system state and triggers a re-render.
6577//
6578// This is the minimal state primitive needed for interactive components.
6579// For complex state, use the global `KnowledgeState` directly.
6580
6581/// Local state hook for components.
6582///
6583/// Returns a `(getter, setter)` pair:
6584/// - `getter()` returns the current value of type `T`
6585/// - `setter(value)` updates the value and triggers a re-render
6586///
6587/// The `id` must be unique per component instance (use a hash of the
6588/// component's label or a generated UUID).
6589pub fn use_state<T: Clone + Send + Sync + 'static>(
6590    id: u64,
6591    initial: T,
6592) -> (impl Fn() -> T, impl Fn(T)) {
6593    // Initialize the state if not already present
6594    let already_exists = load_system_state().get_component_state::<T>(id).is_some();
6595    if !already_exists {
6596        update_system_state(|s| {
6597            let mut ns = s.clone();
6598            ns.set_component_state(id, initial.clone());
6599            ns
6600        });
6601    }
6602
6603    let getter = move || -> T {
6604        load_system_state()
6605            .get_component_state::<T>(id)
6606            .map(|arc_lock| {
6607                arc_lock
6608                    .read()
6609                    .ok()
6610                    .map(|guard| (*guard).clone())
6611                    .unwrap_or_else(|| initial.clone())
6612            })
6613            .unwrap_or_else(|| initial.clone())
6614    };
6615
6616    let setter = {
6617        move |value| {
6618            update_system_state(|s| {
6619                let mut ns = s.clone();
6620                ns.set_component_state(id, value);
6621                ns
6622            });
6623        }
6624    };
6625
6626    (getter, setter)
6627}
6628
6629/// Generate a stable hash ID from a string key.
6630///
6631/// Use this to create unique IDs for `use_state` based on component labels
6632/// or other stable identifiers.
6633///
6634/// # Example
6635/// ```no_run
6636/// use cvkg_core::{use_state, use_state_hash};
6637/// let id = use_state_hash("my-checkbox");
6638/// let (value, set_value) = use_state(id, false);
6639/// ```
6640pub fn use_state_hash(key: &str) -> u64 {
6641    use std::hash::{Hash, Hasher};
6642    let mut s = std::collections::hash_map::DefaultHasher::new();
6643    key.hash(&mut s);
6644    s.finish()
6645}
6646
6647// =============================================================================
6648// ACCESSIBILITY PREFERENCES -- System accessibility settings
6649// =============================================================================
6650//
6651// Components and the renderer query these to adapt behavior:
6652// - Reduce Motion: disable non-essential animations
6653// - Reduce Transparency: replace glass materials with opaque surfaces
6654// - Increase Contrast: make borders visible, minimum alpha 0.5
6655
6656thread_local! {
6657    /// Thread-local accessibility preferences.
6658    /// Defaults to no restrictions (all false).
6659    static ACCESSIBILITY_PREFS: std::cell::RefCell<AccessibilityPreferences> =
6660        std::cell::RefCell::new(AccessibilityPreferences::default());
6661}
6662
6663/// System accessibility preferences that components and the renderer must honor.
6664///
6665/// These map to macOS System Settings > Accessibility:
6666/// - `reduce_motion`: Disables non-essential animations (spring, bounce, etc.)
6667/// - `reduce_transparency`: Replaces glass/transparent materials with opaque surfaces
6668/// - `increase_contrast`: Makes all borders visible, minimum alpha 0.5 for all elements
6669#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6670pub struct AccessibilityPreferences {
6671    /// User prefers reduced motion. Animations should be instant or very short.
6672    pub reduce_motion: bool,
6673    /// User prefers reduced transparency. Glass materials should be opaque.
6674    pub reduce_transparency: bool,
6675    /// User prefers increased contrast. Borders must be visible, min alpha 0.5.
6676    pub increase_contrast: bool,
6677}
6678
6679impl AccessibilityPreferences {
6680    /// Detect system accessibility preferences (macOS).
6681    ///
6682    /// On non-macOS platforms, returns defaults (all false).
6683    /// In a production implementation, this would query the OS APIs.
6684    pub fn detect_from_system() -> Self {
6685        #[cfg(target_os = "macos")]
6686        {
6687            // Try to read macOS accessibility preferences via defaults command
6688            let reduce_motion = std::process::Command::new("defaults")
6689                .args(["read", "-g", "com.apple.universalaccess", "reduceMotion"])
6690                .output()
6691                .ok()
6692                .and_then(|o| String::from_utf8(o.stdout).ok())
6693                .map(|s| s.trim() == "1")
6694                .unwrap_or(false);
6695
6696            let reduce_transparency = std::process::Command::new("defaults")
6697                .args([
6698                    "read",
6699                    "-g",
6700                    "com.apple.universalaccess",
6701                    "reduceTransparency",
6702                ])
6703                .output()
6704                .ok()
6705                .and_then(|o| String::from_utf8(o.stdout).ok())
6706                .map(|s| s.trim() == "1")
6707                .unwrap_or(false);
6708
6709            let increase_contrast = std::process::Command::new("defaults")
6710                .args([
6711                    "read",
6712                    "-g",
6713                    "com.apple.universalaccess",
6714                    "increaseContrast",
6715                ])
6716                .output()
6717                .ok()
6718                .and_then(|o| String::from_utf8(o.stdout).ok())
6719                .map(|s| s.trim() == "1")
6720                .unwrap_or(false);
6721
6722            Self {
6723                reduce_motion,
6724                reduce_transparency,
6725                increase_contrast,
6726            }
6727        }
6728
6729        #[cfg(target_os = "linux")]
6730        {
6731            // Reduced motion: check GTK_A11Y env var or GNOME gsettings
6732            let reduce_motion = std::env::var("GTK_A11Y")
6733                .map(|v| v.to_lowercase().contains("reduce-motion"))
6734                .unwrap_or(false)
6735                || {
6736                    // Try gsettings for GNOME desktop animation preference
6737                    std::process::Command::new("gsettings")
6738                        .args([
6739                            "get",
6740                            "org.gnome.desktop.interface",
6741                            "enable-animations",
6742                        ])
6743                        .output()
6744                        .ok()
6745                        .and_then(|o| String::from_utf8(o.stdout).ok())
6746                        .map(|s| s.trim() == "'false'" || s.trim() == "false")
6747                        .unwrap_or(false)
6748                };
6749
6750            // Reduced transparency is not widely supported on Linux desktops
6751            let reduce_transparency = false;
6752
6753            // Increased contrast: check GTK_THEME for high-contrast variants
6754            let increase_contrast = std::env::var("GTK_THEME")
6755                .map(|v| v.to_lowercase().contains("highcontrast"))
6756                .unwrap_or(false);
6757
6758            Self {
6759                reduce_motion,
6760                reduce_transparency,
6761                increase_contrast,
6762            }
6763        }
6764
6765        #[cfg(target_os = "windows")]
6766        {
6767            use std::process::Command;
6768
6769            // Helper: run `reg query` and return the value string if found
6770            fn reg_query(key: &str, value_name: &str) -> Option<String> {
6771                Command::new("reg")
6772                    .args(["query", key, "/v", value_name])
6773                    .output()
6774                    .ok()
6775                    .and_then(|o| {
6776                        if o.status.success() {
6777                            String::from_utf8(o.stdout).ok()
6778                        } else {
6779                            None
6780                        }
6781                    })
6782                    .and_then(|s| {
6783                        // Output format: "    ValueName    REG_SZ    <value>"
6784                        // or REG_DWORD lines; parse the last token on the last non-empty line
6785                        s.lines()
6786                            .last()?
6787                            .split_whitespace()
6788                            .last()
6789                            .map(String::from)
6790                    })
6791            }
6792
6793            // Reduced motion: EffectsAnimationEfficiency = 1 means reduced
6794            let reduce_motion = reg_query(
6795                "HKCU\\Control Panel\\Accessibility\\EffectsAnimationEfficiency",
6796                "EffectsAnimationEfficiency",
6797            )
6798            .map(|v| v == "1")
6799            .unwrap_or(false);
6800
6801            // Reduced transparency: EnableTransparency = 0 means reduced
6802            let reduce_transparency = reg_query(
6803                "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
6804                "EnableTransparency",
6805            )
6806            .map(|v| v == "0")
6807            .unwrap_or(false);
6808
6809            // Increased contrast: HighContrast = 1 means enabled
6810            let increase_contrast = reg_query(
6811                "HKCU\\Control Panel\\Accessibility\\HighContrast",
6812                "HighContrast",
6813            )
6814            .map(|v| v == "1")
6815            .unwrap_or(false);
6816
6817            Self {
6818                reduce_motion,
6819                reduce_transparency,
6820                increase_contrast,
6821            }
6822        }
6823
6824        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
6825        {
6826            Self::default()
6827        }
6828    }
6829
6830    /// Apply a minimum alpha constraint for increase-contrast mode.
6831    pub fn min_alpha(&self, requested: f32) -> f32 {
6832        if self.increase_contrast {
6833            requested.max(0.5)
6834        } else {
6835            requested
6836        }
6837    }
6838
6839    /// Returns true if glass effects should be replaced with opaque surfaces.
6840    pub fn should_disable_glass(&self) -> bool {
6841        self.reduce_transparency
6842    }
6843
6844    /// Returns true if animations should be instant.
6845    pub fn should_reduce_motion(&self) -> bool {
6846        self.reduce_motion
6847    }
6848
6849    /// Returns true if borders should be made visible.
6850    pub fn should_increase_contrast(&self) -> bool {
6851        self.increase_contrast
6852    }
6853}
6854
6855/// Get the current accessibility preferences for this thread.
6856pub fn accessibility_preferences() -> AccessibilityPreferences {
6857    ACCESSIBILITY_PREFS.with(|p| *p.borrow())
6858}
6859
6860/// Set the accessibility preferences for this thread.
6861///
6862/// The native renderer should call this on startup and when system
6863/// preferences change (via `detect_from_system()`).
6864pub fn set_accessibility_preferences(prefs: AccessibilityPreferences) {
6865    ACCESSIBILITY_PREFS.with(|p| {
6866        *p.borrow_mut() = prefs;
6867    });
6868}
6869
6870// =============================================================================
6871// CLIPBOARD -- System clipboard access
6872// =============================================================================
6873
6874/// Trait for clipboard operations.
6875///
6876/// The native renderer implements this via `arboard` on desktop platforms.
6877/// On WASM, it uses the browser Clipboard API.
6878pub trait ClipboardProvider: Send + Sync {
6879    /// Read text from the system clipboard.
6880    fn read_text(&self) -> Option<String>;
6881    /// Write text to the system clipboard.
6882    fn write_text(&self, text: &str);
6883}
6884
6885/// Default clipboard implementation using `arboard`.
6886/// Note: This is only available when the `arboard` feature is enabled.
6887/// The renderer provides the concrete implementation.
6888#[cfg(not(target_arch = "wasm32"))]
6889pub struct SystemClipboard;
6890
6891#[cfg(not(target_arch = "wasm32"))]
6892impl ClipboardProvider for SystemClipboard {
6893    fn read_text(&self) -> Option<String> {
6894        use std::process::Command;
6895        // Fallback: try pbpaste on macOS
6896        Command::new("pbpaste")
6897            .output()
6898            .ok()
6899            .and_then(|o| String::from_utf8(o.stdout).ok())
6900    }
6901
6902    fn write_text(&self, text: &str) {
6903        use std::process::Command;
6904        // Fallback: try pbcopy on macOS
6905        if let Ok(mut child) = Command::new("pbcopy")
6906            .stdin(std::process::Stdio::piped())
6907            .spawn()
6908        {
6909            if let Some(stdin) = child.stdin.as_mut() {
6910                use std::io::Write;
6911                let _ = stdin.write_all(text.as_bytes());
6912            }
6913            let _ = child.wait();
6914        }
6915    }
6916}
6917
6918// =============================================================================
6919// TEXT INPUT -- Direction enum for cursor movement
6920// =============================================================================
6921
6922/// Direction for cursor movement in text input.
6923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6924pub enum TextDirection {
6925    Forward,
6926    Backward,
6927    Up,
6928    Down,
6929    LineStart,
6930    LineEnd,
6931    WordForward,
6932    WordBackward,
6933}
6934
6935/// Text input state managed by the renderer.
6936///
6937/// Components don't store this directly -- the renderer maintains it
6938/// and components query/modify it through the Renderer trait methods.
6939#[derive(Debug, Clone, Default)]
6940pub struct TextInputState {
6941    /// The full text content.
6942    pub text: String,
6943    /// Cursor position as byte offset into the text.
6944    pub cursor_pos: usize,
6945    /// Selection anchor. If Some, the selection is from anchor to cursor.
6946    /// If None, there is no selection.
6947    pub selection_anchor: Option<usize>,
6948    /// Whether the input is focused (shows cursor, accepts keyboard).
6949    pub focused: bool,
6950    /// Whether the caret is currently visible (for blinking).
6951    pub caret_visible: bool,
6952    /// Last edit timestamp for undo coalescing.
6953    pub last_edit_time: f32,
6954}
6955
6956impl TextInputState {
6957    /// Create a new TextInputState with the given initial text.
6958    pub fn new(text: impl Into<String>) -> Self {
6959        let text = text.into();
6960        let cursor_pos = text.len();
6961        Self {
6962            text,
6963            cursor_pos,
6964            selection_anchor: None,
6965            focused: false,
6966            caret_visible: true,
6967            last_edit_time: 0.0,
6968        }
6969    }
6970
6971    /// Get the selection range as (start, end) byte offsets.
6972    /// Returns None if there is no selection.
6973    pub fn selection_range(&self) -> Option<(usize, usize)> {
6974        self.selection_anchor.map(|anchor| {
6975            if anchor <= self.cursor_pos {
6976                (anchor, self.cursor_pos)
6977            } else {
6978                (self.cursor_pos, anchor)
6979            }
6980        })
6981    }
6982
6983    /// Get the selected text, or empty string if no selection.
6984    pub fn selected_text(&self) -> String {
6985        self.selection_range()
6986            .map(|(start, end)| self.text[start..end].to_string())
6987            .unwrap_or_default()
6988    }
6989
6990    /// Insert text at the current cursor position, replacing any selection.
6991    pub fn insert(&mut self, new_text: &str) {
6992        if let Some((start, end)) = self.selection_range() {
6993            self.text.replace_range(start..end, new_text);
6994            self.cursor_pos = start + new_text.len();
6995        } else {
6996            self.text.insert_str(self.cursor_pos, new_text);
6997            self.cursor_pos += new_text.len();
6998        }
6999        self.selection_anchor = None;
7000    }
7001
7002    /// Delete characters. If there's a selection, delete it.
7003    /// Otherwise delete `count` characters backward (backspace) or forward (delete).
7004    pub fn delete(&mut self, backward: bool, count: usize) -> String {
7005        if let Some((start, end)) = self.selection_range() {
7006            let deleted = self.text[start..end].to_string();
7007            self.text.replace_range(start..end, "");
7008            self.cursor_pos = start;
7009            self.selection_anchor = None;
7010            return deleted;
7011        }
7012
7013        if backward && self.cursor_pos > 0 {
7014            let start = self.cursor_pos.saturating_sub(count);
7015            let deleted = self.text[start..self.cursor_pos].to_string();
7016            self.text.replace_range(start..self.cursor_pos, "");
7017            self.cursor_pos = start;
7018            deleted
7019        } else if !backward && self.cursor_pos < self.text.len() {
7020            let end = (self.cursor_pos + count).min(self.text.len());
7021            let deleted = self.text[self.cursor_pos..end].to_string();
7022            self.text.replace_range(self.cursor_pos..end, "");
7023            deleted
7024        } else {
7025            String::new()
7026        }
7027    }
7028
7029    /// Move the cursor in the given direction.
7030    pub fn move_cursor(&mut self, direction: TextDirection, extend_selection: bool) {
7031        if !extend_selection {
7032            self.selection_anchor = None;
7033        } else if self.selection_anchor.is_none() {
7034            self.selection_anchor = Some(self.cursor_pos);
7035        }
7036
7037        match direction {
7038            TextDirection::Forward if self.cursor_pos < self.text.len() => {
7039                // Move to next character boundary (UTF-8 safe)
7040                let next = self.text[self.cursor_pos..]
7041                    .char_indices()
7042                    .nth(1)
7043                    .map(|(i, _)| self.cursor_pos + i)
7044                    .unwrap_or(self.text.len());
7045                self.cursor_pos = next;
7046            }
7047            TextDirection::Backward if self.cursor_pos > 0 => {
7048                let prev = self.text[..self.cursor_pos]
7049                    .char_indices()
7050                    .next_back()
7051                    .map(|(i, _)| i)
7052                    .unwrap_or(0);
7053                self.cursor_pos = prev;
7054            }
7055            TextDirection::LineStart => {
7056                self.cursor_pos = 0;
7057            }
7058            TextDirection::LineEnd => {
7059                self.cursor_pos = self.text.len();
7060            }
7061            TextDirection::WordForward => {
7062                // Find next word boundary
7063                let rest = &self.text[self.cursor_pos..];
7064                // Skip current word chars
7065                let after_word = rest
7066                    .char_indices()
7067                    .find(|(_, c)| !c.is_alphanumeric())
7068                    .map(|(i, _)| i)
7069                    .unwrap_or(rest.len());
7070                // Skip whitespace
7071                let after_space = rest[after_word..]
7072                    .char_indices()
7073                    .find(|(_, c)| !c.is_whitespace())
7074                    .map(|(i, _)| after_word + i)
7075                    .unwrap_or(rest.len());
7076                self.cursor_pos = (self.cursor_pos + after_space).min(self.text.len());
7077            }
7078            TextDirection::WordBackward => {
7079                let before = &self.text[..self.cursor_pos];
7080                // Skip whitespace going backward
7081                let before_word = before
7082                    .char_indices()
7083                    .rev()
7084                    .find(|(_, c)| !c.is_whitespace())
7085                    .map(|(i, _)| i)
7086                    .unwrap_or(0);
7087                // Skip word chars going backward
7088                let word_start = before[..before_word]
7089                    .char_indices()
7090                    .rev()
7091                    .find(|(_, c)| !c.is_alphanumeric())
7092                    .map(|(i, _)| i)
7093                    .unwrap_or(0);
7094                self.cursor_pos = word_start;
7095            }
7096            _ => {} // Up/Down handled by multi-line components
7097        }
7098
7099        if !extend_selection {
7100            self.selection_anchor = None;
7101        }
7102    }
7103
7104    /// Select all text.
7105    pub fn select_all(&mut self) {
7106        self.cursor_pos = self.text.len();
7107        self.selection_anchor = Some(0);
7108    }
7109
7110    /// Get the byte offset of the cursor.
7111    pub fn cursor_byte_pos(&self) -> usize {
7112        self.cursor_pos
7113    }
7114}
7115
7116/// Action details for interactive buttons inside a notification.
7117#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7118pub struct NotificationAction {
7119    /// Unique identifier of the action.
7120    pub id: String,
7121    /// The text label to display on the action button.
7122    pub title: String,
7123    /// Indicates whether the action performs a destructive task (e.g. Delete).
7124    pub is_destructive: bool,
7125}
7126
7127/// Priority tier of a notification.
7128#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7129pub enum NotificationPriority {
7130    /// Placed silently into the notification center without visual alerts.
7131    Passive,
7132    /// Triggers a visual alert (toast) but does not interrupt focus.
7133    #[default]
7134    Active,
7135    /// Important alert that bypasses standard DND/Focus bounds.
7136    TimeSensitive,
7137}
7138
7139/// A structured notification representation.
7140#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
7141pub struct Notification {
7142    /// Unique identifier for this notification.
7143    pub id: String,
7144    /// App or source identifier spawning this notification.
7145    pub app_name: Option<String>,
7146    /// The bold heading/title text.
7147    pub title: String,
7148    /// The detailed descriptive body text.
7149    pub body: String,
7150    /// Optional URI or path to an icon asset.
7151    pub icon: Option<String>,
7152    /// Optional sound identifier to play when posting.
7153    pub sound: Option<String>,
7154    /// Interactive actions available on this notification.
7155    pub actions: Vec<NotificationAction>,
7156    /// Timer duration in seconds after which the toast auto-dismisses.
7157    pub timeout: Option<f32>,
7158    /// Priority level for delivery logic.
7159    pub priority: NotificationPriority,
7160    /// Time (in seconds since renderer startup) when this notification was posted.
7161    pub timestamp: f32,
7162    /// Whether the notification has been dismissed/read.
7163    pub dismissed: bool,
7164}
7165
7166/// Error type indicating a failure in generating or posting a notification.
7167#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, thiserror::Error)]
7168pub enum NotificationError {
7169    /// Permissions denied.
7170    #[error("Notification permission denied")]
7171    PermissionDenied,
7172    /// Failed to post the notification.
7173    #[error("Failed to post notification")]
7174    PostFailed,
7175}
7176
7177/// State of notification permissions.
7178#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7179pub enum NotificationPermission {
7180    /// Explicitly allowed.
7181    Granted,
7182    /// Explicitly blocked.
7183    Denied,
7184    /// Prompt has not been shown or decided yet.
7185    #[default]
7186    NotDetermined,
7187}
7188
7189/// Core interface for routing and dispatching notification events.
7190pub trait NotificationHandler: Send + Sync {
7191    /// Posts a new notification.
7192    fn show(&self, notification: Notification) -> Result<(), NotificationError>;
7193    /// Dismisses a notification by ID.
7194    fn dismiss(&self, id: &str) -> Result<(), NotificationError>;
7195    /// Requests delivery permission.
7196    fn request_permission(&self) -> NotificationPermission;
7197}
7198
7199static NEXT_NOTIFICATION_ID: std::sync::atomic::AtomicUsize =
7200    std::sync::atomic::AtomicUsize::new(1);
7201
7202/// Default in-app notification handler that writes state to KnowledgeState.
7203#[derive(Clone, Copy, Debug, Default)]
7204pub struct DefaultNotificationHandler;
7205
7206impl NotificationHandler for DefaultNotificationHandler {
7207    /// Save the notification to the global system state (history) and auto-assign an ID if empty.
7208    fn show(&self, notification: Notification) -> Result<(), NotificationError> {
7209        let mut notif = notification;
7210        if notif.id.is_empty() {
7211            let id = NEXT_NOTIFICATION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7212            notif.id = format!("notif_{}", id);
7213        }
7214        update_system_state(|state| {
7215            let mut new_state = state.clone();
7216            new_state.notifications.push(notif.clone());
7217            new_state
7218        });
7219        Ok(())
7220    }
7221
7222    /// Mark a notification as dismissed/read in the global system state.
7223    fn dismiss(&self, id: &str) -> Result<(), NotificationError> {
7224        update_system_state(|state| {
7225            let mut new_state = state.clone();
7226            for notif in &mut new_state.notifications {
7227                if notif.id == id {
7228                    notif.dismissed = true;
7229                }
7230            }
7231            new_state
7232        });
7233        Ok(())
7234    }
7235
7236    /// Returns the permission state (always Granted for internal in-app notifications).
7237    fn request_permission(&self) -> NotificationPermission {
7238        NotificationPermission::Granted
7239    }
7240}
7241
7242static NOTIFICATION_HANDLER: once_cell::sync::OnceCell<std::sync::Arc<dyn NotificationHandler>> =
7243    once_cell::sync::OnceCell::new();
7244
7245/// Sets the global notification handler.
7246pub fn set_notification_handler(handler: std::sync::Arc<dyn NotificationHandler>) {
7247    let _ = NOTIFICATION_HANDLER.set(handler);
7248}
7249
7250/// Gets the global notification handler, fallback to DefaultNotificationHandler.
7251pub fn get_notification_handler() -> std::sync::Arc<dyn NotificationHandler> {
7252    NOTIFICATION_HANDLER
7253        .get_or_init(|| std::sync::Arc::new(DefaultNotificationHandler))
7254        .clone()
7255}
7256
7257/// Filter mapping name to extension list for a file dialog.
7258#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7259pub struct FileFilter {
7260    /// Friendly name of the filter (e.g. "Images").
7261    pub name: String,
7262    /// List of file extensions (e.g. ["png", "jpg"]).
7263    pub extensions: Vec<String>,
7264}
7265
7266/// The mode/purpose of the file dialog.
7267#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
7268pub enum FileDialogMode {
7269    /// Pick a single or multiple files to open.
7270    #[default]
7271    OpenFile,
7272    /// Pick a directory path.
7273    OpenDirectory,
7274    /// Prompt for a location/name to save a file.
7275    SaveFile,
7276}
7277
7278/// Dialog options for picking files or directories.
7279#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7280pub struct FileDialog {
7281    /// Title displayed in the dialog window.
7282    pub title: String,
7283    /// Optional starting directory path.
7284    pub default_path: Option<String>,
7285    /// Extensions used to filter selection.
7286    pub filters: Vec<FileFilter>,
7287    /// Open/save mode.
7288    pub mode: FileDialogMode,
7289    /// Allows selecting multiple files if in OpenFile mode.
7290    pub allow_multiple: bool,
7291}
7292
7293/// Errors returned by the file dialog.
7294#[derive(Debug, thiserror::Error)]
7295pub enum FileDialogError {
7296    /// The user closed the dialog without selecting anything.
7297    #[error("File dialog cancelled")]
7298    Cancelled,
7299    /// An input/output error occurred.
7300    #[error("I/O error: {0}")]
7301    Io(#[from] std::io::Error),
7302    /// Platform-specific error.
7303    #[error("Platform error: {0}")]
7304    Platform(String),
7305}
7306
7307impl FileDialog {
7308    /// Creates a new FileDialog with the given mode.
7309    pub fn new(mode: FileDialogMode) -> Self {
7310        Self {
7311            mode,
7312            ..Default::default()
7313        }
7314    }
7315
7316    /// Sets the dialog title.
7317    pub fn title(mut self, title: impl Into<String>) -> Self {
7318        self.title = title.into();
7319        self
7320    }
7321
7322    /// Adds a file filter.
7323    pub fn add_filter(mut self, name: &str, extensions: &[&str]) -> Self {
7324        self.filters.push(FileFilter {
7325            name: name.to_string(),
7326            extensions: extensions.iter().map(|s| s.to_string()).collect(),
7327        });
7328        self
7329    }
7330
7331    /// Sets the default starting directory path.
7332    pub fn default_path(mut self, path: impl Into<String>) -> Self {
7333        self.default_path = Some(path.into());
7334        self
7335    }
7336
7337    /// Sets whether selecting multiple files is allowed.
7338    pub fn allow_multiple(mut self, allow: bool) -> Self {
7339        self.allow_multiple = allow;
7340        self
7341    }
7342}
7343
7344#[cfg(not(target_arch = "wasm32"))]
7345impl FileDialog {
7346    /// Pick file(s) or folder based on current mode configuration.
7347    pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7348        let mut dialog = rfd::FileDialog::new();
7349        dialog = dialog.set_title(&self.title);
7350        if let Some(path) = &self.default_path {
7351            dialog = dialog.set_directory(path);
7352        }
7353        for filter in &self.filters {
7354            let refs: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
7355            dialog = dialog.add_filter(&filter.name, &refs);
7356        }
7357
7358        match self.mode {
7359            FileDialogMode::OpenFile => {
7360                if self.allow_multiple {
7361                    dialog.pick_files().ok_or(FileDialogError::Cancelled)
7362                } else {
7363                    Ok(dialog.pick_file().into_iter().collect())
7364                }
7365            }
7366            FileDialogMode::OpenDirectory => Ok(dialog.pick_folder().into_iter().collect()),
7367            FileDialogMode::SaveFile => Ok(dialog.save_file().into_iter().collect()),
7368        }
7369    }
7370
7371    /// Helper to pick a single file/directory, returning None if cancelled.
7372    pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7373        let results = self.pick()?;
7374        Ok(results.into_iter().next())
7375    }
7376}
7377
7378#[cfg(target_arch = "wasm32")]
7379impl FileDialog {
7380    /// Pick is unsupported/mocked on WASM.
7381    pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7382        Err(FileDialogError::Platform(
7383            "FileDialog is not supported synchronously on WebAssembly".to_string(),
7384        ))
7385    }
7386
7387    /// Helper to pick a single file/directory, returning None if cancelled.
7388    pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7389        Err(FileDialogError::Platform(
7390            "FileDialog is not supported synchronously on WebAssembly".to_string(),
7391        ))
7392    }
7393}
7394
7395/// Error type representing a failure in Document load/save/parse operations.
7396#[derive(Debug, thiserror::Error)]
7397pub enum DocumentError {
7398    /// An input/output error occurred.
7399    #[error("I/O error: {0}")]
7400    Io(#[from] std::io::Error),
7401    /// Failure during deserialization or parsing.
7402    #[error("Parse error: {0}")]
7403    Parse(String),
7404    /// Failure during serialization.
7405    #[error("Serialization error: {0}")]
7406    Serialize(String),
7407}
7408
7409/// A document interface mapping to local filesystem persistence.
7410pub trait Document: Send + Sync {
7411    /// Loads the document from the specified path.
7412    fn read_from(path: &std::path::Path) -> Result<Self, DocumentError>
7413    where
7414        Self: Sized;
7415
7416    /// Saves the document to the specified path.
7417    fn write_to(&self, path: &std::path::Path) -> Result<(), DocumentError>;
7418
7419    /// Returns true if the document has unsaved modifications.
7420    fn is_dirty(&self) -> bool;
7421
7422    /// Marks the document as clean/saved.
7423    fn mark_clean(&mut self);
7424}
7425
7426/// Periodic auto-save coordinator for open Documents.
7427pub struct AutoSaveManager {
7428    /// Time interval in seconds between auto-saves.
7429    pub interval: f32,
7430    /// Elapsed timer tracker.
7431    pub timer: f32,
7432    /// Registered open documents under management.
7433    pub documents: Vec<(std::path::PathBuf, Box<dyn Document>)>,
7434}
7435
7436impl AutoSaveManager {
7437    /// Creates a new AutoSaveManager with the specified check interval.
7438    pub fn new(interval: f32) -> Self {
7439        Self {
7440            interval,
7441            timer: 0.0,
7442            documents: Vec::new(),
7443        }
7444    }
7445
7446    /// Register a document with its current file path.
7447    pub fn register(&mut self, path: std::path::PathBuf, doc: Box<dyn Document>) {
7448        self.documents.push((path, doc));
7449    }
7450
7451    /// Advance the timer and auto-save any dirty documents when the interval is reached.
7452    pub fn tick(&mut self, dt: f32) {
7453        self.timer += dt;
7454        if self.timer >= self.interval {
7455            self.timer = 0.0;
7456            for (path, doc) in &mut self.documents {
7457                if doc.is_dirty() {
7458                    match doc.write_to(path) {
7459                        Ok(()) => {
7460                            doc.mark_clean();
7461                            log::info!("[AutoSaveManager] Auto-saved document to {:?}", path);
7462                        }
7463                        Err(e) => {
7464                            log::error!(
7465                                "[AutoSaveManager] Failed to auto-save document to {:?}: {:?}",
7466                                path,
7467                                e
7468                            );
7469                        }
7470                    }
7471                }
7472            }
7473        }
7474    }
7475}
7476
7477// ── Menu Bar API ──────────────────────────────────────────────────────────────
7478
7479/// Keyboard modifier flags used by [`KeyboardShortcut`].
7480///
7481/// On macOS, `cmd` maps to the Command (⌘) key.
7482/// On all other platforms, `cmd` maps to the Control key.
7483/// This is enforced at the renderer level, not here; the data model is OS-agnostic.
7484#[derive(Debug, Clone, PartialEq, Eq, Default)]
7485pub struct Modifiers {
7486    /// Command on macOS, Control on Windows/Linux.
7487    pub cmd: bool,
7488    /// Shift key.
7489    pub shift: bool,
7490    /// Alt/Option key.
7491    pub alt: bool,
7492    /// Control key (distinct from cmd on all platforms).
7493    pub ctrl: bool,
7494}
7495
7496/// A keyboard shortcut binding to a menu action.
7497#[derive(Debug, Clone)]
7498pub struct KeyboardShortcut {
7499    /// The key character or name, e.g. `"s"`, `"z"`, `"Return"`.
7500    pub key: String,
7501    /// The required modifier combination.
7502    pub modifiers: Modifiers,
7503}
7504
7505impl KeyboardShortcut {
7506    /// Convenience constructor: cmd (or ctrl on non-macOS) + `key`.
7507    pub fn cmd(key: impl Into<String>) -> Self {
7508        Self {
7509            key: key.into(),
7510            modifiers: Modifiers {
7511                cmd: true,
7512                ..Default::default()
7513            },
7514        }
7515    }
7516
7517    /// Convenience constructor: cmd+Shift + `key`.
7518    pub fn cmd_shift(key: impl Into<String>) -> Self {
7519        Self {
7520            key: key.into(),
7521            modifiers: Modifiers {
7522                cmd: true,
7523                shift: true,
7524                ..Default::default()
7525            },
7526        }
7527    }
7528}
7529
7530/// A single entry in a [`MenuBar`].
7531///
7532/// Actions hold a callback that is invoked when the user activates the item
7533/// (either via the menu UI or via the associated keyboard shortcut).
7534/// Separators provide visual grouping. Submenus allow hierarchical menus.
7535pub enum MenuItem {
7536    /// An activatable menu entry with an optional shortcut and enabled/disabled state.
7537    Action {
7538        label: String,
7539        shortcut: Option<KeyboardShortcut>,
7540        action: std::sync::Arc<dyn Fn() + Send + Sync>,
7541        enabled: bool,
7542    },
7543    /// A nested submenu.
7544    Submenu { label: String, items: Vec<MenuItem> },
7545    /// A visual separator line between groups of items.
7546    Separator,
7547}
7548
7549impl std::fmt::Debug for MenuItem {
7550    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7551        match self {
7552            Self::Action { label, enabled, .. } => f
7553                .debug_struct("Action")
7554                .field("label", label)
7555                .field("enabled", enabled)
7556                .finish(),
7557            Self::Submenu { label, items } => f
7558                .debug_struct("Submenu")
7559                .field("label", label)
7560                .field("items", items)
7561                .finish(),
7562            Self::Separator => write!(f, "Separator"),
7563        }
7564    }
7565}
7566
7567/// A top-level menu bar containing [`MenuItem`]s.
7568///
7569/// The menu bar is a data model only; rendering it into an OS-native menu is
7570/// handled by the platform renderer (`cvkg-render-native`).
7571pub struct MenuBar {
7572    /// Ordered list of top-level menu items.
7573    pub items: Vec<MenuItem>,
7574}
7575
7576impl MenuBar {
7577    /// Create an empty menu bar.
7578    pub fn new() -> Self {
7579        Self { items: Vec::new() }
7580    }
7581
7582    /// Append a menu item to the bar.
7583    pub fn add_item(&mut self, item: MenuItem) {
7584        self.items.push(item);
7585    }
7586
7587    /// Build the standard CVKG menu structure with all conventional shortcuts.
7588    ///
7589    /// The `cmd` modifier maps to ⌘ on macOS and Ctrl on Windows/Linux -- this
7590    /// translation is enforced by the renderer, not here.
7591    ///
7592    /// Menus included:
7593    /// - **File**: New, Open, Save, Close
7594    /// - **Edit**: Undo, Redo, Cut, Copy, Paste, Select All, Find
7595    /// - **View**: Zoom In, Zoom Out, Fullscreen
7596    /// - **Window**: Minimize, Zoom, Bring All to Front
7597    /// - **Help**: Search Help
7598    #[allow(clippy::too_many_arguments)]
7599    pub fn standard(
7600        new_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7601        open_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7602        save_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7603        close_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7604        quit_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7605        undo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7606        redo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7607        cut_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7608        copy_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7609        paste_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7610        select_all_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7611        find_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7612    ) -> Self {
7613        let mut bar = Self::new();
7614
7615        // ── File ──────────────────────────────────────────────────────────────
7616        bar.add_item(MenuItem::Submenu {
7617            label: "File".to_string(),
7618            items: vec![
7619                MenuItem::Action {
7620                    label: "New".to_string(),
7621                    shortcut: Some(KeyboardShortcut::cmd("n")),
7622                    action: new_fn,
7623                    enabled: true,
7624                },
7625                MenuItem::Action {
7626                    label: "Open…".to_string(),
7627                    shortcut: Some(KeyboardShortcut::cmd("o")),
7628                    action: open_fn,
7629                    enabled: true,
7630                },
7631                MenuItem::Separator,
7632                MenuItem::Action {
7633                    label: "Save".to_string(),
7634                    shortcut: Some(KeyboardShortcut::cmd("s")),
7635                    action: save_fn,
7636                    enabled: true,
7637                },
7638                MenuItem::Separator,
7639                MenuItem::Action {
7640                    label: "Close".to_string(),
7641                    shortcut: Some(KeyboardShortcut::cmd("w")),
7642                    action: close_fn,
7643                    enabled: true,
7644                },
7645                MenuItem::Separator,
7646                MenuItem::Action {
7647                    label: "Quit".to_string(),
7648                    shortcut: Some(KeyboardShortcut::cmd("q")),
7649                    action: quit_fn,
7650                    enabled: true,
7651                },
7652            ],
7653        });
7654
7655        // ── Edit ──────────────────────────────────────────────────────────────
7656        bar.add_item(MenuItem::Submenu {
7657            label: "Edit".to_string(),
7658            items: vec![
7659                MenuItem::Action {
7660                    label: "Undo".to_string(),
7661                    shortcut: Some(KeyboardShortcut::cmd("z")),
7662                    action: undo_fn,
7663                    enabled: true,
7664                },
7665                MenuItem::Action {
7666                    label: "Redo".to_string(),
7667                    shortcut: Some(KeyboardShortcut::cmd_shift("z")),
7668                    action: redo_fn,
7669                    enabled: true,
7670                },
7671                MenuItem::Separator,
7672                MenuItem::Action {
7673                    label: "Cut".to_string(),
7674                    shortcut: Some(KeyboardShortcut::cmd("x")),
7675                    action: cut_fn,
7676                    enabled: true,
7677                },
7678                MenuItem::Action {
7679                    label: "Copy".to_string(),
7680                    shortcut: Some(KeyboardShortcut::cmd("c")),
7681                    action: copy_fn,
7682                    enabled: true,
7683                },
7684                MenuItem::Action {
7685                    label: "Paste".to_string(),
7686                    shortcut: Some(KeyboardShortcut::cmd("v")),
7687                    action: paste_fn,
7688                    enabled: true,
7689                },
7690                MenuItem::Separator,
7691                MenuItem::Action {
7692                    label: "Select All".to_string(),
7693                    shortcut: Some(KeyboardShortcut::cmd("a")),
7694                    action: select_all_fn,
7695                    enabled: true,
7696                },
7697                MenuItem::Separator,
7698                MenuItem::Action {
7699                    label: "Find…".to_string(),
7700                    shortcut: Some(KeyboardShortcut::cmd("f")),
7701                    action: find_fn,
7702                    enabled: true,
7703                },
7704            ],
7705        });
7706
7707        // ── View ──────────────────────────────────────────────────────────────
7708        // View items carry no application-level callbacks at the model layer;
7709        // zoom and fullscreen are handled by the renderer directly.
7710        let noop: std::sync::Arc<dyn Fn() + Send + Sync> = std::sync::Arc::new(|| {});
7711        bar.add_item(MenuItem::Submenu {
7712            label: "View".to_string(),
7713            items: vec![
7714                MenuItem::Action {
7715                    label: "Zoom In".to_string(),
7716                    shortcut: Some(KeyboardShortcut::cmd("=")),
7717                    action: noop.clone(),
7718                    enabled: true,
7719                },
7720                MenuItem::Action {
7721                    label: "Zoom Out".to_string(),
7722                    shortcut: Some(KeyboardShortcut::cmd("-")),
7723                    action: noop.clone(),
7724                    enabled: true,
7725                },
7726                MenuItem::Separator,
7727                MenuItem::Action {
7728                    label: "Toggle Fullscreen".to_string(),
7729                    shortcut: Some(KeyboardShortcut {
7730                        key: "f".to_string(),
7731                        modifiers: Modifiers {
7732                            ctrl: true,
7733                            ..Default::default()
7734                        },
7735                    }),
7736                    action: noop.clone(),
7737                    enabled: true,
7738                },
7739            ],
7740        });
7741
7742        // ── Window ────────────────────────────────────────────────────────────
7743        bar.add_item(MenuItem::Submenu {
7744            label: "Window".to_string(),
7745            items: vec![
7746                MenuItem::Action {
7747                    label: "Minimize".to_string(),
7748                    shortcut: Some(KeyboardShortcut::cmd("m")),
7749                    action: noop.clone(),
7750                    enabled: true,
7751                },
7752                MenuItem::Action {
7753                    label: "Zoom".to_string(),
7754                    shortcut: None,
7755                    action: noop.clone(),
7756                    enabled: true,
7757                },
7758                MenuItem::Separator,
7759                MenuItem::Action {
7760                    label: "Bring All to Front".to_string(),
7761                    shortcut: None,
7762                    action: noop.clone(),
7763                    enabled: true,
7764                },
7765            ],
7766        });
7767
7768        // ── Help ──────────────────────────────────────────────────────────────
7769        bar.add_item(MenuItem::Submenu {
7770            label: "Help".to_string(),
7771            items: vec![MenuItem::Action {
7772                label: "Search Help".to_string(),
7773                shortcut: None,
7774                action: noop,
7775                enabled: true,
7776            }],
7777        });
7778
7779        bar
7780    }
7781}
7782
7783impl Default for MenuBar {
7784    fn default() -> Self {
7785        Self::new()
7786    }
7787}
7788
7789// =============================================================================
7790// LOCALIZATION -- Item 12: Localization / Internationalization
7791// =============================================================================
7792// OS-agnostic: works on all platforms. No platform-specific string loading.
7793
7794use std::sync::RwLock;
7795
7796/// Layout direction for UI elements (LTR or RTL).
7797#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7798pub enum Direction {
7799    #[default]
7800    LTR,
7801    RTL,
7802    Auto,
7803}
7804
7805impl Direction {
7806    pub fn is_rtl(self) -> bool {
7807        matches!(self, Direction::RTL)
7808    }
7809}
7810#[derive(Clone, Debug)]
7811pub struct L10nBundle {
7812    pub locale: String,
7813    pub strings: HashMap<String, String>,
7814    pub is_rtl: bool,
7815}
7816
7817impl L10nBundle {
7818    pub fn new(locale: impl Into<String>) -> Self {
7819        Self {
7820            locale: locale.into(),
7821            strings: HashMap::new(),
7822            is_rtl: false,
7823        }
7824    }
7825
7826    pub fn add(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
7827        self.strings.insert(key.into(), value.into());
7828        self
7829    }
7830
7831    pub fn from_strings_format(locale: impl Into<String>, input: &str) -> Self {
7832        let mut bundle = Self::new(locale);
7833        for line in input.lines() {
7834            let line = line.trim();
7835            if line.is_empty() || line.starts_with("//") {
7836                continue;
7837            }
7838            if let Some(eq_pos) = line.find(" = ") {
7839                let key = line[..eq_pos].trim_matches('"').to_string();
7840                let val = line[eq_pos + 3..]
7841                    .trim_end_matches(';')
7842                    .trim_matches('"')
7843                    .to_string();
7844                bundle.strings.insert(key, val);
7845            }
7846        }
7847        bundle
7848    }
7849    /// Get a translated string by key. Returns the key itself if not found.
7850    pub fn t(&self, key: &str) -> String {
7851        self.strings
7852            .get(key)
7853            .map(|s| s.to_string())
7854            .unwrap_or_else(|| key.to_string())
7855    }
7856
7857    /// Translate with interpolation. Replaces {0}, {1}, etc. with args.
7858    pub fn tf(&self, key: &str, args: &[&str]) -> String {
7859        let mut result = self.t(key);
7860        for (i, arg) in args.iter().enumerate() {
7861            result = result.replace(&format!("{{{}}}", i), arg);
7862        }
7863        result
7864    }
7865}
7866
7867/// Global localization manager.
7868pub struct L10n {
7869    bundles: HashMap<String, L10nBundle>,
7870    current: String,
7871}
7872
7873impl L10n {
7874    pub fn new(default_locale: &str) -> Self {
7875        Self {
7876            bundles: HashMap::new(),
7877            current: default_locale.to_string(),
7878        }
7879    }
7880
7881    pub fn add_bundle(&mut self, bundle: L10nBundle) {
7882        self.bundles.insert(bundle.locale.clone(), bundle);
7883    }
7884
7885    pub fn set_locale(&mut self, locale: &str) {
7886        self.current = locale.to_string();
7887    }
7888    pub fn current_locale(&self) -> &str {
7889        &self.current
7890    }
7891
7892    pub fn is_rtl(&self) -> bool {
7893        self.bundles
7894            .get(self.current.as_str())
7895            .map(|b| b.is_rtl)
7896            .unwrap_or(false)
7897    }
7898
7899    pub fn t(&self, key: &str) -> String {
7900        self.bundles
7901            .get(self.current.as_str())
7902            .map(|b| b.t(key))
7903            .unwrap_or_else(|| key.to_string())
7904    }
7905
7906    pub fn tf(&self, key: &str, args: &[&str]) -> String {
7907        let mut result = self.t(key);
7908        for (i, arg) in args.iter().enumerate() {
7909            result = result.replace(&format!("{{{}}}", i), arg);
7910        }
7911        result
7912    }
7913
7914    pub fn direction(&self) -> Direction {
7915        if self.is_rtl() {
7916            Direction::RTL
7917        } else {
7918            Direction::LTR
7919        }
7920    }
7921}
7922
7923static L10N: once_cell::sync::Lazy<Arc<RwLock<L10n>>> =
7924    once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(L10n::new("en"))));
7925
7926pub fn init_l10n(l10n: L10n) {
7927    if let Ok(mut guard) = L10N.write() {
7928        *guard = l10n;
7929    }
7930}
7931
7932pub fn l10n() -> Arc<RwLock<L10n>> {
7933    L10N.clone()
7934}
7935
7936pub fn t(key: &str) -> String {
7937    L10N.read()
7938        .map(|g| g.t(key).to_string())
7939        .unwrap_or_else(|_| key.to_string())
7940}
7941
7942pub fn tf(key: &str, args: &[&str]) -> String {
7943    L10N.read()
7944        .map(|g| g.tf(key, args))
7945        .unwrap_or_else(|_| key.to_string())
7946}
7947
7948pub fn set_locale(locale: &str) {
7949    if let Ok(mut guard) = L10N.write() {
7950        guard.set_locale(locale);
7951    }
7952}
7953
7954pub fn current_locale() -> String {
7955    L10N.read()
7956        .map(|g| g.current_locale().to_string())
7957        .unwrap_or_else(|_| "en".to_string())
7958}
7959
7960pub fn is_rtl() -> bool {
7961    L10N.read().map(|g| g.is_rtl()).unwrap_or(false)
7962}
7963
7964// =============================================================================
7965// SYSTEM THEME DETECTION -- Dark/Light mode detection
7966// =============================================================================
7967//
7968// OS-agnostic theme detection. Checks the CVKG_THEME environment variable first,
7969// then falls back to dark mode (safe default).
7970//
7971// Platform backends may override this with native OS queries (e.g.,
7972// dark-light crate on desktop, prefers-color-scheme on web).
7973
7974/// The detected system theme.
7975#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7976pub enum SystemTheme {
7977    /// Dark mode (default).
7978    #[default]
7979    Dark,
7980    /// Light mode.
7981    Light,
7982}
7983
7984/// Detect the current system theme.
7985///
7986/// Checks `CVKG_THEME` environment variable first:
7987/// - `"dark"` → `SystemTheme::Dark`
7988/// - `"light"` → `SystemTheme::Light`
7989/// - unset or any other value → `SystemTheme::Dark` (default)
7990///
7991/// Platform backends can call this and override with native detection
7992/// (e.g., `dark-light` crate on desktop, `prefers-color-scheme` on web).
7993pub fn detect_system_theme() -> SystemTheme {
7994    std::env::var("CVKG_THEME")
7995        .ok()
7996        .and_then(|v| match v.as_str() {
7997            "light" => Some(SystemTheme::Light),
7998            "dark" => Some(SystemTheme::Dark),
7999            _ => None,
8000        })
8001        .unwrap_or(SystemTheme::Dark)
8002}
8003
8004// =============================================================================
8005// AUDIO / HAPTIC -- Item 14: Spatial Audio / Haptic Feedback
8006// =============================================================================
8007// OS-agnostic: pure trait abstractions. Platform backends via cfg in renderer.
8008
8009pub mod audio_haptic;
8010pub use audio_haptic::{
8011    AudioEngine, HapticEngine, HapticIntensity, NullAudioEngine, NullHapticEngine, haptic_error,
8012    haptic_impact, haptic_selection, haptic_success, play_sound, set_audio_engine,
8013    set_haptic_engine, sounds,
8014};
8015
8016// =============================================================================
8017// PARALLAX -- Depth-based scroll offset system
8018// =============================================================================
8019
8020pub mod parallax;
8021pub use parallax::{DisplayEnvironment, ParallaxModifier, PerformanceContract, Tier3Fallback};
8022
8023// =============================================================================
8024// KVASIR IDENTITY -- Platform-wide unique identifier (crosscrate.md Finding #2)
8025// =============================================================================
8026
8027/// Platform-wide unique identifier used by every CVKG graph layer.
8028///
8029/// # Why this exists
8030/// The crosscrate audit (Finding #2) identified that each crate maintained its own
8031/// incompatible `NodeId(u64)` newtype, causing type-level friction whenever two
8032/// layers needed to reference the same object (e.g., VDOM ↔ Scene sync).
8033///
8034/// # Contract
8035/// - Every `KvasirId` produced by [`KvasirId::new`] is globally unique within
8036///   a single process lifetime (backed by a monotonic atomic counter).
8037/// - IDs are sequential and cache-friendly in `HashMap` / `BTreeMap` keys.
8038/// - `KvasirId(0)` is **reserved as the null/invalid sentinel** — never returned
8039///   by `new()`.
8040/// - `Serialize`/`Deserialize` round-trips through the inner `u64`.
8041#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8042pub struct KvasirId(pub u64);
8043
8044impl KvasirId {
8045    /// The null sentinel value. Never allocated by [`KvasirId::new`].
8046    pub const NULL: KvasirId = KvasirId(0);
8047
8048    /// Allocate a new process-unique `KvasirId`.
8049    ///
8050    /// Uses a relaxed atomic increment — order does not matter because IDs
8051    /// only need to be distinct, not sequentially ordered relative to other
8052    /// memory operations.
8053    pub fn new() -> Self {
8054        use std::sync::atomic::{AtomicU64, Ordering};
8055        static COUNTER: AtomicU64 = AtomicU64::new(1);
8056        KvasirId(COUNTER.fetch_add(1, Ordering::Relaxed))
8057    }
8058
8059    /// Returns `true` if this is the null sentinel value.
8060    pub fn is_null(self) -> bool {
8061        self.0 == 0
8062    }
8063}
8064
8065impl std::fmt::Display for KvasirId {
8066    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8067        write!(f, "KvasirId({})", self.0)
8068    }
8069}
8070
8071/// Lossless conversion from a raw `u64` into a `KvasirId`.
8072///
8073/// # Why this exists
8074/// The crosscrate audit (Phase 1 of the implementation plan) unifies identity
8075/// across `cvkg-scene`, `cvkg-vdom`, and `cvkg-flow` by making each crate's
8076/// `NodeId` a type alias for `KvasirId`. Existing call sites that constructed
8077/// `NodeId(some_u64)` need a way to migrate without touching every literal.
8078///
8079/// # Contract
8080/// - `u64` -> `KvasirId` is infallible (any `u64` is a valid id; 0 maps to NULL).
8081/// - `KvasirId` -> `u64` is infallible (trivially the inner value).
8082///
8083/// # Note
8084/// Allocating ids should still go through `KvasirId::new()` so that the
8085/// atomic counter is respected. `From<u64>` is for *existing* ids that came
8086/// from serialized data or stable test fixtures.
8087impl From<u64> for KvasirId {
8088    fn from(value: u64) -> Self {
8089        KvasirId(value)
8090    }
8091}
8092
8093impl From<KvasirId> for u64 {
8094    fn from(id: KvasirId) -> Self {
8095        id.0
8096    }
8097}
8098
8099// =============================================================================
8100// INVALIDATION MODEL -- Platform-wide dirty flag system (crosscrate.md Finding #3)
8101// =============================================================================
8102
8103/// Bitmask encoding which pipeline layers are dirty for a given object.
8104///
8105/// # Why this exists
8106/// The crosscrate audit (Finding #3) identified that each crate had its own
8107/// `is_dirty: bool` field with no shared semantic. Without a unified model,
8108/// updates propagate as full-tree redraws instead of targeted passes, leading
8109/// to performance collapse at scale.
8110///
8111/// # Layers (in pipeline order)
8112/// - `STATE`     — application-level data changed (triggers LAYOUT + PAINT + COMPOSITE)
8113/// - `LAYOUT`    — size or position changed (triggers PAINT + COMPOSITE)
8114/// - `PAINT`     — visual appearance changed (triggers COMPOSITE only)
8115/// - `COMPOSITE` — compositing properties changed (e.g. opacity, transform, blur)
8116///
8117/// # Contract
8118/// A crate that dirtifies a layer MUST also dirtify all downstream layers.
8119/// Use the helper constants [`DirtyFlags::from_state_change`] etc. rather
8120/// than setting bits manually to ensure the invariant is maintained.
8121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8122pub struct DirtyFlags(pub u8);
8123
8124impl DirtyFlags {
8125    /// No layers are dirty.
8126    pub const CLEAN: DirtyFlags = DirtyFlags(0b0000_0000);
8127    /// Application state changed — propagates to all downstream layers.
8128    pub const STATE: DirtyFlags = DirtyFlags(0b0000_1111);
8129    /// Layout (size/position) changed — propagates to paint + composite.
8130    pub const LAYOUT: DirtyFlags = DirtyFlags(0b0000_0111);
8131    /// Paint (visual) changed — propagates to composite.
8132    pub const PAINT: DirtyFlags = DirtyFlags(0b0000_0011);
8133    /// Compositing properties changed (opacity, clip, backdrop).
8134    pub const COMPOSITE: DirtyFlags = DirtyFlags(0b0000_0001);
8135    /// All layers dirty (equivalent to STATE).
8136    pub const ALL: DirtyFlags = DirtyFlags(0b0000_1111);
8137
8138    /// Returns `true` if any dirty bits are set.
8139    #[inline]
8140    pub fn is_dirty(self) -> bool {
8141        self.0 != 0
8142    }
8143
8144    /// Returns `true` if the composite layer needs reprocessing.
8145    #[inline]
8146    pub fn needs_composite(self) -> bool {
8147        self.0 & 0b0000_0001 != 0
8148    }
8149
8150    /// Returns `true` if the paint layer needs reprocessing.
8151    #[inline]
8152    pub fn needs_paint(self) -> bool {
8153        self.0 & 0b0000_0010 != 0
8154    }
8155
8156    /// Returns `true` if layout needs reprocessing.
8157    #[inline]
8158    pub fn needs_layout(self) -> bool {
8159        self.0 & 0b0000_0100 != 0
8160    }
8161
8162    /// Returns `true` if application state has changed.
8163    #[inline]
8164    pub fn needs_state(self) -> bool {
8165        self.0 & 0b0000_1000 != 0
8166    }
8167
8168    /// Merge another set of flags into this one (bitwise OR).
8169    #[inline]
8170    pub fn merge(self, other: DirtyFlags) -> DirtyFlags {
8171        DirtyFlags(self.0 | other.0)
8172    }
8173
8174    /// Clear all dirty flags, marking this object as clean.
8175    #[inline]
8176    pub fn clear(self) -> DirtyFlags {
8177        DirtyFlags::CLEAN
8178    }
8179}
8180
8181impl std::ops::BitOr for DirtyFlags {
8182    type Output = DirtyFlags;
8183    fn bitor(self, rhs: DirtyFlags) -> DirtyFlags {
8184        DirtyFlags(self.0 | rhs.0)
8185    }
8186}
8187
8188impl std::ops::BitOrAssign for DirtyFlags {
8189    fn bitor_assign(&mut self, rhs: DirtyFlags) {
8190        self.0 |= rhs.0;
8191    }
8192}
8193
8194impl std::ops::BitAnd for DirtyFlags {
8195    type Output = DirtyFlags;
8196    fn bitand(self, rhs: DirtyFlags) -> DirtyFlags {
8197        DirtyFlags(self.0 & rhs.0)
8198    }
8199}
8200
8201/// A single invalidation record associating a `KvasirId` with its dirty layers.
8202///
8203/// # Contract
8204/// Invalidation records are produced by any system that mutates state and
8205/// consumed by the scheduler to determine what work must be done next frame.
8206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8207pub struct InvalidationRecord {
8208    /// The object that was mutated.
8209    pub id: KvasirId,
8210    /// Which pipeline layers need reprocessing.
8211    pub flags: DirtyFlags,
8212}
8213
8214impl InvalidationRecord {
8215    /// Create a new invalidation record.
8216    pub fn new(id: KvasirId, flags: DirtyFlags) -> Self {
8217        Self { id, flags }
8218    }
8219
8220    /// Create a record indicating the object's full pipeline needs rebuilding.
8221    pub fn full(id: KvasirId) -> Self {
8222        Self { id, flags: DirtyFlags::ALL }
8223    }
8224}
8225
8226#[cfg(test)]
8227mod kvasir_identity_tests {
8228    use super::*;
8229
8230    #[test]
8231    fn kvasir_id_new_is_non_zero() {
8232        // Contract: KvasirId::new() must never return the null sentinel.
8233        let id = KvasirId::new();
8234        assert!(!id.is_null(), "KvasirId::new() returned null sentinel");
8235    }
8236
8237    #[test]
8238    fn kvasir_id_new_is_unique() {
8239        // Each call must produce a distinct ID.
8240        let a = KvasirId::new();
8241        let b = KvasirId::new();
8242        let c = KvasirId::new();
8243        assert_ne!(a, b);
8244        assert_ne!(b, c);
8245        assert_ne!(a, c);
8246    }
8247
8248    #[test]
8249    fn kvasir_id_null_sentinel() {
8250        assert!(KvasirId::NULL.is_null());
8251        assert!(!KvasirId::new().is_null());
8252    }
8253
8254    #[test]
8255    fn kvasir_id_serde_roundtrip() {
8256        let id = KvasirId(42);
8257        let json = serde_json::to_string(&id).unwrap();
8258        let decoded: KvasirId = serde_json::from_str(&json).unwrap();
8259        assert_eq!(id, decoded);
8260    }
8261
8262    #[test]
8263    fn dirty_flags_clean_is_not_dirty() {
8264        assert!(!DirtyFlags::CLEAN.is_dirty());
8265    }
8266
8267    #[test]
8268    fn dirty_flags_all_implies_all_layers() {
8269        let f = DirtyFlags::ALL;
8270        assert!(f.needs_state());
8271        assert!(f.needs_layout());
8272        assert!(f.needs_paint());
8273        assert!(f.needs_composite());
8274    }
8275
8276    #[test]
8277    fn dirty_flags_composite_only() {
8278        let f = DirtyFlags::COMPOSITE;
8279        assert!(!f.needs_state());
8280        assert!(!f.needs_layout());
8281        assert!(!f.needs_paint());
8282        assert!(f.needs_composite());
8283    }
8284
8285    #[test]
8286    fn dirty_flags_merge() {
8287        let a = DirtyFlags::COMPOSITE;
8288        let b = DirtyFlags::PAINT;
8289        let merged = a.merge(b);
8290        assert!(merged.needs_composite());
8291        assert!(merged.needs_paint());
8292        assert!(!merged.needs_layout());
8293    }
8294
8295    #[test]
8296    fn dirty_flags_bitor() {
8297        let combined = DirtyFlags::PAINT | DirtyFlags::COMPOSITE;
8298        assert!(combined.needs_paint());
8299        assert!(combined.needs_composite());
8300    }
8301
8302    #[test]
8303    fn dirty_flags_clear() {
8304        let dirty = DirtyFlags::ALL;
8305        let clean = dirty.clear();
8306        assert!(!clean.is_dirty());
8307    }
8308
8309    #[test]
8310    fn dirty_flags_serde_roundtrip() {
8311        let f = DirtyFlags::LAYOUT;
8312        let json = serde_json::to_string(&f).unwrap();
8313        let decoded: DirtyFlags = serde_json::from_str(&json).unwrap();
8314        assert_eq!(f, decoded);
8315    }
8316
8317    #[test]
8318    fn invalidation_record_full() {
8319        let id = KvasirId::new();
8320        let rec = InvalidationRecord::full(id);
8321        assert_eq!(rec.id, id);
8322        assert!(rec.flags.needs_state());
8323        assert!(rec.flags.needs_layout());
8324    }
8325}
8326
8327// =========================================================================
8328// P1-15: Subscriber List Mutex Poisoning
8329// =========================================================================
8330//
8331// Regression tests for the audit finding: a single panicking subscriber
8332// would poison the Mutex and break all future state updates forever.
8333// The fix wraps each callback in catch_unwind, so panics are isolated
8334// and logged without affecting other subscribers or future updates.
8335
8336#[cfg(test)]
8337mod subscriber_panic_isolation_tests {
8338    use super::*;
8339    use std::sync::atomic::{AtomicUsize, Ordering};
8340
8341    #[test]
8342    fn panicking_subscriber_does_not_poison_mutex() {
8343        let state = State::new(0i32);
8344        let fired = Arc::new(AtomicUsize::new(0));
8345
8346        // First subscriber: panics.
8347        let _ = state.subscribe(|_| -> () {
8348            panic!("subscriber 1 explodes");
8349        });
8350
8351        // Second subscriber: should still fire.
8352        let fired_clone = Arc::clone(&fired);
8353        let _ = state.subscribe(move |v| {
8354            fired_clone.store(*v as usize + 1, Ordering::SeqCst);
8355        });
8356
8357        // Trigger the state change. Subscriber 1 panics; subscriber 2 runs.
8358        state.set(42);
8359
8360        assert_eq!(
8361            fired.load(Ordering::SeqCst),
8362            43,
8363            "second subscriber must fire even though first panicked"
8364        );
8365
8366        // Critical: future state updates must still work.
8367        let fired2 = Arc::new(AtomicUsize::new(0));
8368        let fired2_clone = Arc::clone(&fired2);
8369        let _ = state.subscribe(move |v| {
8370            fired2_clone.store(*v as usize, Ordering::SeqCst);
8371        });
8372        state.set(100);
8373        assert_eq!(
8374            fired2.load(Ordering::SeqCst),
8375            100,
8376            "future updates must work after subscriber panic"
8377        );
8378    }
8379
8380    #[test]
8381    fn all_subscribers_fire_even_if_one_panics() {
8382        let state = State::new(0u32);
8383        let count = Arc::new(AtomicUsize::new(0));
8384
8385        // Mix of panicking and counting subscribers.
8386        let _ = state.subscribe(|_| panic!("boom 1"));
8387        let c1 = Arc::clone(&count);
8388        let _ = state.subscribe(move |_| {
8389            c1.fetch_add(1, Ordering::SeqCst);
8390        });
8391        let _ = state.subscribe(|_| panic!("boom 2"));
8392        let c2 = Arc::clone(&count);
8393        let _ = state.subscribe(move |_| {
8394            c2.fetch_add(1, Ordering::SeqCst);
8395        });
8396
8397        state.set(1);
8398
8399        // Both non-panicking subscribers must have fired.
8400        assert_eq!(
8401            count.load(Ordering::SeqCst),
8402            2,
8403            "both non-panicking subscribers should fire"
8404        );
8405    }
8406
8407    #[test]
8408    fn invoke_subscribers_safely_returns_count() {
8409        // Direct unit test of the helper function.
8410        use std::sync::Mutex;
8411        let subs: SubscriberList<u32> = Arc::new(Mutex::new(Vec::new()));
8412
8413        let count1 = Arc::new(AtomicUsize::new(0));
8414        let count1_clone = Arc::clone(&count1);
8415        subs.lock().unwrap().push(Box::new(move |v| {
8416            count1_clone.store(*v as usize, Ordering::SeqCst);
8417        }));
8418
8419        let count2 = Arc::new(AtomicUsize::new(0));
8420        let count2_clone = Arc::clone(&count2);
8421        subs.lock().unwrap().push(Box::new(move |v| {
8422            count2_clone.store(*v as usize + 100, Ordering::SeqCst);
8423        }));
8424
8425        let invoked = invoke_subscribers_safely(&subs, &7);
8426        assert_eq!(invoked, 2, "both subscribers should be invoked");
8427        assert_eq!(count1.load(Ordering::SeqCst), 7);
8428        assert_eq!(count2.load(Ordering::SeqCst), 107);
8429    }
8430}
8431
8432// =========================================================================
8433// P1-17: Suspense::new_async Shared Fallback Runtime
8434// =========================================================================
8435//
8436// Regression tests for the audit finding: when no ambient tokio
8437// runtime exists, new_async spawned a new OS thread + runtime per
8438// call. The fix introduces a process-wide shared fallback runtime.
8439
8440#[cfg(test)]
8441mod p1_17_shared_fallback_runtime_tests {
8442    use super::*;
8443    use std::time::Duration;
8444
8445    #[test]
8446    fn fallback_runtime_is_shared() {
8447        // Calling fallback_runtime() multiple times should return the
8448        // same Runtime instance (singleton via OnceLock). This is the
8449        // core invariant that bounds thread creation.
8450        let r1 = fallback_runtime();
8451        let r2 = fallback_runtime();
8452        assert!(
8453            std::ptr::eq(r1 as *const _, r2 as *const _),
8454            "fallback_runtime must return the same instance"
8455        );
8456    }
8457
8458    #[test]
8459    fn fallback_worker_count_is_bounded() {
8460        // The worker count must be >= 1 and <= 8 regardless of host
8461        // CPU count. This is what prevents the audit's "spawns
8462        // hundreds of OS threads" issue.
8463        let n = *FALLBACK_WORKER_COUNT.get_or_init(|| {
8464            let available = std::thread::available_parallelism()
8465                .map(|n| n.get())
8466                .unwrap_or(2);
8467            available.saturating_sub(1).clamp(1, 8)
8468        });
8469        assert!(n >= 1, "worker count must be at least 1, got {n}");
8470        assert!(n <= 8, "worker count must be at most 8, got {n}");
8471    }
8472
8473    #[test]
8474    fn many_suspense_calls_share_runtime() {
8475        // P1-17 regression: 20 new_async calls in quick succession
8476        // should not hang or OOM. They all share the single
8477        // fallback runtime, so we never create more than ~8 OS
8478        // threads regardless of call count.
8479        //
8480        // We use a counter SharedState to confirm all 20 futures
8481        // actually run to completion.
8482        let counter = State::new(0u32);
8483        let mut handles = Vec::new();
8484        for _ in 0..20 {
8485            let s = Suspense::new_async(async { Ok::<u32, String>(1) });
8486            // Each suspense ready()s after the future resolves.
8487            // We don't block on ready (would deadlock without
8488            // explicit tokio context), but the spawn is enough to
8489            // exercise the path.
8490            let _ = s; // suppress unused warning
8491            handles.push(s);
8492        }
8493        // Force the counter to tick so the test observably runs.
8494        counter.set(20);
8495        assert_eq!(counter.get(), 20);
8496        // If we got here, the test did not hang or panic, which is
8497        // the main thing we want to verify for P1-17.
8498    }
8499
8500    // ==========================================
8501    // P1-14: State<T> redundant storage documentation
8502    // ==========================================
8503
8504    #[test]
8505    fn p1_14_state_storage_mechanisms() {
8506        // P1-14 documentation test: State<T> has 4 storage
8507        // mechanisms (swap, metadata_swap, tvar, metadata_tvar).
8508        // The audit flagged this as redundant. The fix is to
8509        // document the trade-off (arc_swap for reads, TVar for
8510        // atomic compound transactions) and add a set_direct()
8511        // method for callers who don't need compound transactions.
8512        use std::mem::size_of;
8513        let state = State::new(42u32);
8514        // State contains 4 storage mechanisms + subscribers +
8515        // version + resolution.
8516        // This test documents the size and the trade-off.
8517        let size = size_of_val(&state);
8518        // Size should be at least the size of 4 Arcs (4*8=32 on
8519        // 64-bit) plus subscribers (1 Arc) plus version (1 Arc)
8520        // plus ConflictResolution (1 byte tag).
8521        assert!(
8522            size >= 4 * std::mem::size_of::<usize>(),
8523            "State<T> should be at least 4 Arcs in size"
8524        );
8525    }
8526
8527    #[test]
8528    fn p1_14_set_direct_updates_value() {
8529        // P1-14: set_direct() bypasses TVar for simple updates.
8530        // The swap is the authoritative read source.
8531        let state = State::new(0u32);
8532        state.set_direct(42);
8533        assert_eq!(state.get(), 42);
8534    }
8535
8536    #[test]
8537    fn p1_14_set_direct_notifies_subscribers() {
8538        // P1-14: set_direct() must notify subscribers just like
8539        // set().
8540        let state = State::new(0u32);
8541        let received = Arc::new(Mutex::new(Vec::new()));
8542        let received_clone = Arc::clone(&received);
8543        state.subscribe(move |v| {
8544            received_clone.lock().unwrap().push(*v);
8545        });
8546        state.set_direct(1);
8547        state.set_direct(2);
8548        state.set_direct(3);
8549        // Allow the subscriber invocations to complete.
8550        std::thread::sleep(std::time::Duration::from_millis(10));
8551        let log = received.lock().unwrap();
8552        // Should have at least the last 3 values, but the order
8553        // and count depend on how many subscribers were invoked
8554        // (subscribers can be invoked synchronously or batched).
8555        assert!(
8556            log.contains(&1) && log.contains(&2) && log.contains(&3),
8557            "set_direct must notify subscribers of all values"
8558        );
8559    }
8560}
8561
8562// =========================================================================
8563// P1-39: DirtyRegionManager -- tracks changed rectangles
8564// =========================================================================
8565//
8566// The P1-39 audit found that the scene graph lacks dirty region
8567// tracking. Large UIs may redraw excessively when only a small
8568// region changes. This struct provides the foundation for
8569// future dirty-region optimizations.
8570//
8571// Currently it just stores a list of dirty rectangles. Future
8572// work would add:
8573//  - Coalescing adjacent dirty regions into larger rects
8574//  - Tree-based hierarchical dirty tracking
8575//  - Integration with the renderer's scissor/clip
8576//
8577// This is a passive container -- callers add dirty regions
8578// when they change something, and the renderer can clear
8579// them after a frame.
8580
8581/// P1-39: a list of regions that have changed and need to be
8582/// re-rendered. Coalesces overlapping rectangles on add to
8583/// avoid unbounded growth.
8584#[derive(Debug, Clone, Default)]
8585pub struct DirtyRegionManager {
8586    /// The dirty rectangles, in screen-space coordinates.
8587    regions: Vec<Rect>,
8588    /// Counter incremented on each clear, useful for detecting
8589    /// "stale" dirty regions after multiple frames.
8590    generation: u64,
8591}
8592
8593impl DirtyRegionManager {
8594    /// Create a new empty dirty region manager.
8595    pub fn new() -> Self {
8596        Self::default()
8597    }
8598
8599    /// Mark a region as dirty. The region is in screen-space
8600    /// coordinates (typically the same coordinate system as the
8601    /// rest of the rendering).
8602    ///
8603    /// If `region` overlaps with an existing dirty region, the
8604    /// two are coalesced into a single larger rectangle. This
8605    /// prevents the dirty list from growing unbounded for
8606    /// large UIs with many small changes.
8607    pub fn mark_dirty(&mut self, region: Rect) {
8608        // Try to merge with an existing overlapping region.
8609        for existing in self.regions.iter_mut() {
8610            if Self::rects_overlap(*existing, region) {
8611                *existing = Self::union_rect(*existing, region);
8612                return;
8613            }
8614        }
8615        // No overlap -- add as new region.
8616        self.regions.push(region);
8617    }
8618
8619    /// Get the current dirty regions. The renderer can use
8620    /// this list to clip drawing to only the changed areas.
8621    pub fn regions(&self) -> &[Rect] {
8622        &self.regions
8623    }
8624
8625    /// Check if any region is dirty. Useful for skipping a
8626    /// frame when nothing has changed.
8627    pub fn is_dirty(&self) -> bool {
8628        !self.regions.is_empty()
8629    }
8630
8631    /// Clear all dirty regions. Called by the renderer after
8632    /// processing a frame.
8633    ///
8634    /// Increments the generation counter so callers can detect
8635    /// when a clear has happened.
8636    pub fn clear(&mut self) {
8637        self.regions.clear();
8638        self.generation = self.generation.wrapping_add(1);
8639    }
8640
8641    /// Get the current generation counter. Increases on every
8642    /// clear(). Callers can cache this to detect when the
8643    /// dirty state has been reset.
8644    pub fn generation(&self) -> u64 {
8645        self.generation
8646    }
8647
8648    /// Number of dirty regions currently tracked. After
8649    /// coalescing, this should be much smaller than the number
8650    /// of mark_dirty() calls.
8651    pub fn len(&self) -> usize {
8652        self.regions.len()
8653    }
8654
8655    /// Check whether the dirty region list is empty.
8656    pub fn is_empty(&self) -> bool {
8657        self.regions.is_empty()
8658    }
8659
8660    /// Check if two rectangles overlap.
8661    fn rects_overlap(a: Rect, b: Rect) -> bool {
8662        a.x < b.x + b.width
8663            && a.x + a.width > b.x
8664            && a.y < b.y + b.height
8665            && a.y + a.height > b.y
8666    }
8667
8668    /// Compute the union of two rectangles (the smallest
8669    /// rectangle that contains both).
8670    fn union_rect(a: Rect, b: Rect) -> Rect {
8671        let min_x = a.x.min(b.x);
8672        let min_y = a.y.min(b.y);
8673        let max_x = (a.x + a.width).max(b.x + b.width);
8674        let max_y = (a.y + a.height).max(b.y + b.height);
8675        Rect {
8676            x: min_x,
8677            y: min_y,
8678            width: max_x - min_x,
8679            height: max_y - min_y,
8680        }
8681    }
8682}
8683
8684#[cfg(test)]
8685mod p1_39_dirty_region_tests {
8686    use super::{DirtyRegionManager, Rect};
8687
8688    #[test]
8689    fn new_manager_is_empty() {
8690        let m = DirtyRegionManager::new();
8691        assert!(!m.is_dirty());
8692        assert!(m.is_empty());
8693        assert_eq!(m.len(), 0);
8694    }
8695
8696    #[test]
8697    fn mark_dirty_adds_region() {
8698        let mut m = DirtyRegionManager::new();
8699        m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8700        assert!(m.is_dirty());
8701        assert_eq!(m.len(), 1);
8702    }
8703
8704    #[test]
8705    fn overlapping_regions_coalesce() {
8706        let mut m = DirtyRegionManager::new();
8707        m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8708        m.mark_dirty(Rect { x: 5.0, y: 5.0, width: 10.0, height: 10.0 });
8709        // Should be coalesced into a single region.
8710        assert_eq!(m.len(), 1);
8711        let r = &m.regions()[0];
8712        assert_eq!(r.x, 0.0);
8713        assert_eq!(r.y, 0.0);
8714        assert_eq!(r.width, 15.0);
8715        assert_eq!(r.height, 15.0);
8716    }
8717
8718    #[test]
8719    fn non_overlapping_regions_dont_coalesce() {
8720        let mut m = DirtyRegionManager::new();
8721        m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8722        m.mark_dirty(Rect { x: 100.0, y: 100.0, width: 10.0, height: 10.0 });
8723        // Should remain as 2 separate regions.
8724        assert_eq!(m.len(), 2);
8725    }
8726
8727    #[test]
8728    fn clear_resets_regions_and_increments_generation() {
8729        let mut m = DirtyRegionManager::new();
8730        m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8731        let g1 = m.generation();
8732        m.clear();
8733        assert!(!m.is_dirty());
8734        assert_eq!(m.len(), 0);
8735        assert_eq!(m.generation(), g1 + 1);
8736    }
8737
8738    #[test]
8739    fn many_overlapping_marks_coalesce_to_one() {
8740        let mut m = DirtyRegionManager::new();
8741        // Mark 100 overlapping small regions.
8742        for i in 0..100 {
8743            m.mark_dirty(Rect {
8744                x: i as f32,
8745                y: i as f32,
8746                width: 10.0,
8747                height: 10.0,
8748            });
8749        }
8750        // All should coalesce to a single region.
8751        assert_eq!(m.len(), 1);
8752    }
8753}
8754
8755// =========================================================================
8756// P1-43: FrameBudget -- global frame budget contract
8757// =========================================================================
8758//
8759// The P1-43 audit found that no global frame budget contract
8760// exists. Individual subsystems may exceed their time allocation
8761// without coordination. P0-2 already handles per-frame
8762// degradation (skipping non-essential passes when over budget)
8763// but doesn't coordinate allocation across subsystems.
8764//
8765// This struct provides the foundation for future frame budget
8766// coordination. It tracks wall-clock time per frame and per
8767// subsystem, and allows callers to check whether a subsystem
8768// is within its allocated time slice.
8769//
8770// Currently a passive observer. Future work would add:
8771//  - Per-subsystem time allocation
8772//  - Automatic QualityLevel adjustment when over budget
8773//  - Integration with the renderer's frame loop
8774
8775// =============================================================================
8776// P1-41: LIST / TREE VIRTUALIZATION
8777// =============================================================================
8778
8779/// Outcome of a `VirtualListWindow::compute` call — describes which rows
8780/// should be rendered and what scroll offset to apply to position them.
8781///
8782/// P1-41: IDE and visualization workloads with tens-of-thousands of rows must
8783/// only render the rows visible in the current viewport.  `VirtualListWindow`
8784/// computes the correct row range without building the full row list.
8785#[derive(Debug, Clone, PartialEq)]
8786pub struct VirtualWindow {
8787    /// Index of the first row that should be rendered (inclusive).
8788    pub first_visible: usize,
8789    /// Index one past the last row that should be rendered (exclusive).
8790    pub last_visible: usize,
8791    /// Total estimated height of all rows above `first_visible`, in logical
8792    /// pixels.  Use this as the scroll-offset padding above the rendered rows.
8793    pub offset_before: f32,
8794    /// Total estimated height of all rows below `last_visible`, in logical
8795    /// pixels.  Use this as the placeholder height below the rendered rows.
8796    pub offset_after: f32,
8797}
8798
8799/// Computes the visible slice of a uniform-height virtual list.
8800///
8801/// Contract:
8802/// - `total_rows`: total number of items in the list (can be enormous).
8803/// - `row_height`: uniform logical height of every row in pixels.
8804/// - `viewport_y`: scroll offset of the viewport top edge.
8805/// - `viewport_height`: height of the visible window in pixels.
8806/// - `overscan`: number of extra rows to render above/below the viewport for
8807///   smooth scrolling.  Typically 2–5.
8808///
8809/// Returns a `VirtualWindow` describing the rendered slice and offset padding.
8810/// If `row_height` is ≤ 0 or `total_rows` is 0, returns a zero window.
8811pub fn compute_virtual_list_window(
8812    total_rows: usize,
8813    row_height: f32,
8814    viewport_y: f32,
8815    viewport_height: f32,
8816    overscan: usize,
8817) -> VirtualWindow {
8818    if total_rows == 0 || row_height <= 0.0 {
8819        return VirtualWindow {
8820            first_visible: 0,
8821            last_visible: 0,
8822            offset_before: 0.0,
8823            offset_after: 0.0,
8824        };
8825    }
8826
8827    // How many rows fit in the viewport (rounded up for partial rows).
8828    let visible_rows = (viewport_height / row_height).ceil() as usize;
8829
8830    // First row whose bottom edge is below the viewport top.
8831    let first = (viewport_y / row_height).floor() as isize - overscan as isize;
8832    let first = first.max(0) as usize;
8833
8834    // Last row whose top edge is above the viewport bottom.
8835    let last = first + visible_rows + 2 * overscan;
8836    let last = last.min(total_rows);
8837
8838    VirtualWindow {
8839        first_visible: first,
8840        last_visible: last,
8841        offset_before: first as f32 * row_height,
8842        offset_after: (total_rows - last) as f32 * row_height,
8843    }
8844}
8845
8846/// Computes the visible slice of a variable-height virtual list using
8847/// a precomputed prefix-sum of row heights.
8848///
8849/// Contract:
8850/// - `prefix_heights[i]` is the cumulative height of all rows 0..i (not
8851///   including row i).  `prefix_heights.len()` must equal `total_rows + 1`
8852///   where `prefix_heights[0] == 0` and `prefix_heights[total_rows]` is the
8853///   total list height.
8854/// - `viewport_y` and `viewport_height` are in the same logical pixel units.
8855/// - `overscan` works the same as in `compute_virtual_list_window`.
8856///
8857/// This is O(log N) via binary search on the prefix-sum array.
8858pub fn compute_virtual_list_window_variable(
8859    prefix_heights: &[f32],
8860    viewport_y: f32,
8861    viewport_height: f32,
8862    overscan: usize,
8863) -> VirtualWindow {
8864    let total_rows = prefix_heights.len().saturating_sub(1);
8865    if total_rows == 0 {
8866        return VirtualWindow {
8867            first_visible: 0,
8868            last_visible: 0,
8869            offset_before: 0.0,
8870            offset_after: 0.0,
8871        };
8872    }
8873
8874    // Binary search for the first row whose cumulative top is >= viewport_y.
8875    let first_idx = prefix_heights
8876        .partition_point(|&h| h < viewport_y)
8877        .saturating_sub(1);
8878    let first = first_idx.saturating_sub(overscan);
8879
8880    // Binary search for the last row whose top < viewport_y + viewport_height.
8881    let viewport_bottom = viewport_y + viewport_height;
8882    let last_idx = prefix_heights.partition_point(|&h| h < viewport_bottom);
8883    let last = (last_idx + overscan).min(total_rows);
8884
8885    VirtualWindow {
8886        first_visible: first,
8887        last_visible: last,
8888        offset_before: prefix_heights[first],
8889        offset_after: prefix_heights[total_rows] - prefix_heights[last],
8890    }
8891}
8892
8893// =============================================================================
8894// P1-42: DEPENDENCY-TRACKED STATE INVALIDATION
8895// =============================================================================
8896
8897/// Tracks fine-grained dependencies between components (subscribers) and the
8898/// state keys they depend on.
8899///
8900/// P1-42: A single `State<T>` mutation currently fans out to every subscriber,
8901/// even those that do not depend on the changed value.  `DependencyGraph`
8902/// enables callers to register which state keys a component depends on, then
8903/// query only the components that are actually affected by a given change.
8904///
8905/// This is a lightweight directed graph: state_key → Set<component_id>.
8906/// The graph is append-only; removal happens by re-registration.
8907pub struct DependencyGraph {
8908    /// Maps a state key (e.g. a hashed type id + version) to the set of
8909    /// component IDs that depend on it.
8910    deps: std::collections::HashMap<u64, std::collections::HashSet<u64>>,
8911    /// Reverse map: component_id → set of state keys it depends on.
8912    /// Used to efficiently re-register (clear then re-add) a component.
8913    reverse: std::collections::HashMap<u64, Vec<u64>>,
8914}
8915
8916impl DependencyGraph {
8917    /// Create an empty dependency graph.
8918    pub fn new() -> Self {
8919        Self {
8920            deps: std::collections::HashMap::new(),
8921            reverse: std::collections::HashMap::new(),
8922        }
8923    }
8924
8925    /// Register that `component_id` depends on `state_key`.
8926    ///
8927    /// Idempotent — calling this twice with the same arguments has no effect.
8928    /// To replace a component's full dependency set, call `unregister` first.
8929    pub fn register(&mut self, component_id: u64, state_key: u64) {
8930        self.deps
8931            .entry(state_key)
8932            .or_default()
8933            .insert(component_id);
8934        self.reverse
8935            .entry(component_id)
8936            .or_default()
8937            .push(state_key);
8938    }
8939
8940    /// Remove all dependencies for `component_id`.  Call before re-registering
8941    /// a component after its dependency set changes.
8942    pub fn unregister(&mut self, component_id: u64) {
8943        if let Some(keys) = self.reverse.remove(&component_id) {
8944            for key in keys {
8945                if let Some(set) = self.deps.get_mut(&key) {
8946                    set.remove(&component_id);
8947                }
8948            }
8949        }
8950    }
8951
8952    /// Return the set of component IDs that depend on `state_key`.
8953    ///
8954    /// Returns an empty slice when no component depends on this key.
8955    /// Callers iterate the result and schedule re-render only for those
8956    /// components instead of broadcasting to all subscribers.
8957    pub fn affected_components(&self, state_key: u64) -> impl Iterator<Item = u64> + '_ {
8958        self.deps
8959            .get(&state_key)
8960            .into_iter()
8961            .flat_map(|set| set.iter().copied())
8962    }
8963
8964    /// Return true if any component depends on `state_key`.
8965    pub fn has_dependents(&self, state_key: u64) -> bool {
8966        self.deps
8967            .get(&state_key)
8968            .map_or(false, |set| !set.is_empty())
8969    }
8970
8971    /// Total number of registered dependency edges.
8972    pub fn edge_count(&self) -> usize {
8973        self.deps.values().map(|s| s.len()).sum()
8974    }
8975}
8976
8977impl Default for DependencyGraph {
8978    fn default() -> Self {
8979        Self::new()
8980    }
8981}
8982
8983use std::time::{Duration, Instant};
8984
8985/// P1-43: per-subsystem budget allocation. A frame's total
8986/// time is split across animation, layout, and render
8987/// subsystems. Each gets a fraction of the total budget.
8988#[derive(Debug, Clone, Copy, PartialEq)]
8989pub struct SubsystemBudget {
8990    /// Time slice allocated to this subsystem, in seconds.
8991    pub time_slice: Duration,
8992    /// Whether this subsystem is allowed to be skipped if
8993    /// over budget (non-essential) or must always complete
8994    /// (essential).
8995    pub skippable: bool,
8996    /// Name of the subsystem, for logging.
8997    pub name: &'static str,
8998}
8999
9000/// P1-43: global frame budget tracker. Holds the total
9001/// budget for a frame, per-subsystem allocations, and
9002/// wall-clock measurements.
9003///
9004/// Named FrameBudgetTracker to avoid confusion with the
9005/// existing FrameBudget config struct (which holds the
9006/// target_ms and allow_degradation settings).
9007#[derive(Debug)]
9008pub struct FrameBudgetTracker {
9009    /// Total budget for the frame (typically 1/60s = 16.67ms
9010    /// for 60fps).
9011    total: Duration,
9012    /// Per-subsystem allocations. Sum should not exceed
9013    /// total.
9014    allocations: Vec<SubsystemBudget>,
9015    /// Frame start time, captured on new_frame().
9016    start: Option<Instant>,
9017    /// Per-subsystem elapsed time, updated on subsystem_finish().
9018    elapsed: Vec<Duration>,
9019}
9020
9021impl FrameBudgetTracker {
9022    /// Standard 60fps frame budget: 16.67ms total, with
9023    /// default allocations across animation (4ms), layout
9024    /// (4ms), and render (8ms). Subsystems are skippable
9025    /// when over budget except for render, which is essential.
9026    pub fn default_60fps() -> Self {
9027        Self {
9028            total: Duration::from_micros(16_666), // ~16.67ms
9029            allocations: vec![
9030                SubsystemBudget {
9031                    time_slice: Duration::from_micros(4_000),
9032                    skippable: true,
9033                    name: "animation",
9034                },
9035                SubsystemBudget {
9036                    time_slice: Duration::from_micros(4_000),
9037                    skippable: true,
9038                    name: "layout",
9039                },
9040                SubsystemBudget {
9041                    time_slice: Duration::from_micros(8_000),
9042                    skippable: false, // render must always run
9043                    name: "render",
9044                },
9045            ],
9046            start: None,
9047            elapsed: vec![
9048                Duration::ZERO,
9049                Duration::ZERO,
9050                Duration::ZERO,
9051            ],
9052        }
9053    }
9054
9055    /// Standard 120fps frame budget: 8.33ms total, with
9056    /// allocations across animation (2ms), layout (2ms), and render (4ms).
9057    /// Used for high-refresh-rate targets like the Berserker demo.
9058    pub fn default_120fps() -> Self {
9059        Self {
9060            total: Duration::from_micros(8_333), // ~8.33ms
9061            allocations: vec![
9062                SubsystemBudget {
9063                    time_slice: Duration::from_micros(2_000),
9064                    skippable: true,
9065                    name: "animation",
9066                },
9067                SubsystemBudget {
9068                    time_slice: Duration::from_micros(2_000),
9069                    skippable: true,
9070                    name: "layout",
9071                },
9072                SubsystemBudget {
9073                    time_slice: Duration::from_micros(4_000),
9074                    skippable: false, // render must always run
9075                    name: "render",
9076                },
9077            ],
9078            start: None,
9079            elapsed: vec![
9080                Duration::ZERO,
9081                Duration::ZERO,
9082                Duration::ZERO,
9083            ],
9084        }
9085    }
9086
9087    /// Get the total frame budget.
9088    pub fn total(&self) -> Duration {
9089        self.total
9090    }
9091
9092    /// Get the per-subsystem allocations.
9093    pub fn allocations(&self) -> &[SubsystemBudget] {
9094        &self.allocations
9095    }
9096
9097    /// Mark the start of a new frame. Call this at the
9098    /// beginning of the render loop.
9099    pub fn new_frame(&mut self) {
9100        self.start = Some(Instant::now());
9101        for e in self.elapsed.iter_mut() {
9102            *e = Duration::ZERO;
9103        }
9104    }
9105
9106    /// Mark a subsystem as finishing. Updates the elapsed
9107    /// time for that subsystem.
9108    pub fn subsystem_finish(&mut self, index: usize) {
9109        if let Some(start) = self.start {
9110            if index < self.elapsed.len() {
9111                let now = Instant::now();
9112                self.elapsed[index] = now.duration_since(start);
9113            }
9114        }
9115    }
9116
9117    /// Check if a subsystem is within its time allocation.
9118    /// Returns true if the subsystem has used less time than
9119    /// its allocated slice.
9120    pub fn is_within_budget(&self, index: usize) -> bool {
9121        if index >= self.allocations.len() {
9122            return false;
9123        }
9124        if index >= self.elapsed.len() {
9125            return false;
9126        }
9127        self.elapsed[index] <= self.allocations[index].time_slice
9128    }
9129
9130    /// Check if the entire frame is within the total budget.
9131    /// Returns true if all subsystems have completed within
9132    /// their allocations.
9133    pub fn frame_within_budget(&self) -> bool {
9134        for (i, alloc) in self.allocations.iter().enumerate() {
9135            if i < self.elapsed.len()
9136                && self.elapsed[i] > alloc.time_slice
9137                && !alloc.skippable
9138            {
9139                return false;
9140            }
9141        }
9142        true
9143    }
9144
9145    /// Get the elapsed time for a subsystem.
9146    pub fn elapsed(&self, index: usize) -> Duration {
9147        if index < self.elapsed.len() {
9148            self.elapsed[index]
9149        } else {
9150            Duration::ZERO
9151        }
9152    }
9153
9154    /// Get the total time elapsed since new_frame().
9155    pub fn total_elapsed(&self) -> Duration {
9156        match self.start {
9157            Some(start) => start.elapsed(),
9158            None => Duration::ZERO,
9159        }
9160    }
9161}
9162
9163#[cfg(test)]
9164mod p1_43_frame_budget_tests {
9165    use super::FrameBudgetTracker;
9166
9167    #[test]
9168    fn default_60fps_has_16ms_total() {
9169        let fb = FrameBudgetTracker::default_60fps();
9170        // 16.67ms is the target for 60fps.
9171        assert!(fb.total().as_micros() >= 16_000);
9172        assert!(fb.total().as_micros() <= 17_000);
9173    }
9174
9175    #[test]
9176    fn default_allocations_sum_to_roughly_total() {
9177        let fb = FrameBudgetTracker::default_60fps();
9178        let sum: u128 = fb.allocations().iter()
9179            .map(|a| a.time_slice.as_micros())
9180            .sum();
9181        // The 3 allocations (4+4+8 = 16ms) should sum to
9182        // approximately the total budget.
9183        let total = fb.total().as_micros();
9184        assert!(sum <= total);
9185        assert!(sum >= total - 1_000); // within 1ms
9186    }
9187
9188    #[test]
9189    fn render_is_essential_layout_is_skippable() {
9190        let fb = FrameBudgetTracker::default_60fps();
9191        let render = fb.allocations().iter().find(|a| a.name == "render").unwrap();
9192        let layout = fb.allocations().iter().find(|a| a.name == "layout").unwrap();
9193        assert!(!render.skippable, "render must always run");
9194        assert!(layout.skippable, "layout can be skipped if over budget");
9195    }
9196
9197    #[test]
9198    fn new_frame_resets_state() {
9199        let mut fb = FrameBudgetTracker::default_60fps();
9200        fb.new_frame();
9201        // All subsystems should start with zero elapsed.
9202        for i in 0..fb.allocations().len() {
9203            assert_eq!(fb.elapsed(i).as_nanos(), 0);
9204        }
9205    }
9206
9207    #[test]
9208    fn is_within_budget_initially_true() {
9209        let mut fb = FrameBudgetTracker::default_60fps();
9210        fb.new_frame();
9211        // Right after new_frame, no time has been used, so
9212        // all subsystems should be within budget.
9213        for i in 0..fb.allocations().len() {
9214            assert!(fb.is_within_budget(i));
9215        }
9216    }
9217
9218    #[test]
9219    fn frame_within_budget_initially_true() {
9220        let mut fb = FrameBudgetTracker::default_60fps();
9221        fb.new_frame();
9222        assert!(fb.frame_within_budget());
9223    }
9224}
9225
9226/// P2-36: input latency telemetry. Tracks end-to-end latency
9227/// between input event receipt and frame rendering completion.
9228///
9229/// Keeps a sliding window of latency measurements to compute
9230/// percentile metrics (e.g. P50, P95, P99).
9231#[derive(Debug, Clone)]
9232pub struct InputLatencyTracker {
9233    /// Maximum number of samples to retain in the sliding window.
9234    window_size: usize,
9235    /// Sliding window of (event_time, render_time) pairs.
9236    samples: std::collections::VecDeque<(Instant, Instant)>,
9237}
9238
9239impl InputLatencyTracker {
9240    /// Creates a new `InputLatencyTracker` with the specified maximum sliding window size.
9241    ///
9242    /// # Arguments
9243    /// * `window_size` - The maximum number of samples to keep in memory.
9244    pub fn new(window_size: usize) -> Self {
9245        Self {
9246            window_size,
9247            samples: std::collections::VecDeque::with_capacity(window_size),
9248        }
9249    }
9250
9251    /// Records an input event's latency sample.
9252    ///
9253    /// # Arguments
9254    /// * `event_time` - The timestamp when the input event was received.
9255    /// * `render_time` - The timestamp when the rendering for the corresponding frame completed.
9256    ///
9257    /// # Contract
9258    /// If the window size is zero, this call is a no-op.
9259    /// If the sliding window is full, the oldest recorded sample will be discarded.
9260    pub fn record_frame(&mut self, event_time: Instant, render_time: Instant) {
9261        if self.window_size == 0 {
9262            return;
9263        }
9264        if self.samples.len() >= self.window_size {
9265            self.samples.pop_front();
9266        }
9267        self.samples.push_back((event_time, render_time));
9268    }
9269
9270    /// Computes the latency value corresponding to the requested percentile.
9271    ///
9272    /// # Arguments
9273    /// * `p` - The target percentile (0.0 to 100.0 inclusive).
9274    ///
9275    /// # Returns
9276    /// The computed latency duration for the target percentile. If there are no samples,
9277    /// or if the percentile parameter is out of bounds, returns `Duration::ZERO`.
9278    ///
9279    /// # Contract
9280    /// If the input percentile is less than 0.0 or greater than 100.0, this function
9281    /// returns `Duration::ZERO`. The percentile is computed using the nearest-rank method.
9282    pub fn percentile(&self, p: f64) -> Duration {
9283        if self.samples.is_empty() || p < 0.0 || p > 100.0 {
9284            return Duration::ZERO;
9285        }
9286        let mut latencies: Vec<Duration> = self.samples
9287            .iter()
9288            .map(|&(e, r)| {
9289                if r > e {
9290                    r.duration_since(e)
9291                } else {
9292                    Duration::ZERO
9293                }
9294            })
9295            .collect();
9296        latencies.sort();
9297        let len = latencies.len();
9298        let rank = p / 100.0;
9299        let index = ((len as f64 * rank).ceil() as usize).saturating_sub(1);
9300        let index = index.min(len - 1);
9301        latencies[index]
9302    }
9303
9304    /// Clears all recorded samples from the tracker.
9305    ///
9306    /// # Contract
9307    /// Resets the tracker to an empty state with zero samples, retaining the window size.
9308    pub fn clear(&mut self) {
9309        self.samples.clear();
9310    }
9311
9312    /// Returns the configured sliding window size.
9313    pub fn window_size(&self) -> usize {
9314        self.window_size
9315    }
9316
9317    /// Updates the configured sliding window size.
9318    ///
9319    /// # Arguments
9320    /// * `size` - The new maximum number of samples to retain.
9321    ///
9322    /// # Contract
9323    /// If the new size is smaller than the current number of samples, the oldest
9324    /// extra samples will be discarded to match the new size.
9325    pub fn set_window_size(&mut self, size: usize) {
9326        self.window_size = size;
9327        while self.samples.len() > self.window_size {
9328            self.samples.pop_front();
9329        }
9330    }
9331
9332    /// Returns the number of samples currently stored in the sliding window.
9333    pub fn len(&self) -> usize {
9334        self.samples.len()
9335    }
9336
9337    /// Returns whether the tracker currently contains no samples.
9338    pub fn is_empty(&self) -> bool {
9339        self.samples.is_empty()
9340    }
9341}
9342
9343#[cfg(test)]
9344mod p2_36_input_latency_tests {
9345    use super::InputLatencyTracker;
9346    use std::time::{Duration, Instant};
9347
9348    #[test]
9349    fn test_empty_tracker() {
9350        let tracker = InputLatencyTracker::new(10);
9351        assert!(tracker.is_empty());
9352        assert_eq!(tracker.len(), 0);
9353        assert_eq!(tracker.percentile(50.0), Duration::ZERO);
9354    }
9355
9356    #[test]
9357    fn test_record_and_sliding_window() {
9358        let mut tracker = InputLatencyTracker::new(3);
9359        let now = Instant::now();
9360        tracker.record_frame(now, now + Duration::from_millis(10));
9361        tracker.record_frame(now, now + Duration::from_millis(20));
9362        tracker.record_frame(now, now + Duration::from_millis(30));
9363        assert_eq!(tracker.len(), 3);
9364        
9365        // This should evict the 10ms sample
9366        tracker.record_frame(now, now + Duration::from_millis(40));
9367        assert_eq!(tracker.len(), 3);
9368        
9369        // Percentiles should be of [20ms, 30ms, 40ms]
9370        assert_eq!(tracker.percentile(50.0), Duration::from_millis(30));
9371        assert_eq!(tracker.percentile(0.0), Duration::from_millis(20));
9372        assert_eq!(tracker.percentile(100.0), Duration::from_millis(40));
9373    }
9374
9375    #[test]
9376    fn test_resize_and_clear() {
9377        let mut tracker = InputLatencyTracker::new(5);
9378        let now = Instant::now();
9379        for i in 1..=5 {
9380            tracker.record_frame(now, now + Duration::from_millis(i * 10));
9381        }
9382        assert_eq!(tracker.len(), 5);
9383        
9384        tracker.set_window_size(3);
9385        assert_eq!(tracker.window_size(), 3);
9386        assert_eq!(tracker.len(), 3);
9387        // Retained samples should be [30ms, 40ms, 50ms]
9388        assert_eq!(tracker.percentile(0.0), Duration::from_millis(30));
9389        assert_eq!(tracker.percentile(100.0), Duration::from_millis(50));
9390        
9391        tracker.clear();
9392        assert!(tracker.is_empty());
9393        assert_eq!(tracker.percentile(50.0), Duration::ZERO);
9394    }
9395
9396    #[test]
9397    fn test_invalid_percentiles() {
9398        let mut tracker = InputLatencyTracker::new(2);
9399        let now = Instant::now();
9400        tracker.record_frame(now, now + Duration::from_millis(10));
9401        assert_eq!(tracker.percentile(-1.0), Duration::ZERO);
9402        assert_eq!(tracker.percentile(101.0), Duration::ZERO);
9403    }
9404}
9405
9406// =========================================================================
9407// P1-40: EventPhase tests
9408// =========================================================================
9409
9410#[cfg(test)]
9411mod p1_40_event_phase_tests {
9412    use super::EventPhase;
9413
9414    #[test]
9415    fn all_phases_in_capture_order() {
9416        // P1-40: propagation order is Capture -> Target -> Bubble.
9417        let phases = EventPhase::ALL;
9418        assert_eq!(phases[0], EventPhase::Capture);
9419        assert_eq!(phases[1], EventPhase::Target);
9420        assert_eq!(phases[2], EventPhase::Bubble);
9421        assert_eq!(phases.len(), 3);
9422    }
9423
9424    #[test]
9425    fn phase_distinct() {
9426        // P1-40: each phase should be a distinct value.
9427        let capture = EventPhase::Capture;
9428        let target = EventPhase::Target;
9429        let bubble = EventPhase::Bubble;
9430        assert_ne!(capture, target);
9431        assert_ne!(target, bubble);
9432        assert_ne!(capture, bubble);
9433    }
9434
9435    #[test]
9436    fn phase_clone_preserves_value() {
9437        // P1-40: EventPhase should be Copy/Clone.
9438        let phase = EventPhase::Bubble;
9439        let copied = phase;
9440        assert_eq!(phase, copied);
9441    }
9442}
9443
9444// =========================================================================
9445// P1-41: VirtualListWindow tests
9446// =========================================================================
9447#[cfg(test)]
9448mod p1_41_virtual_list_tests {
9449    use super::{compute_virtual_list_window, compute_virtual_list_window_variable};
9450
9451    #[test]
9452    fn uniform_list_at_top_shows_first_rows() {
9453        // 1000 rows of 20px each, viewport at y=0 height=200, overscan=2.
9454        let w = compute_virtual_list_window(1000, 20.0, 0.0, 200.0, 2);
9455        // Visible rows 0..10, overscan adds 2 above (clamped to 0) and 2 below.
9456        assert_eq!(w.first_visible, 0, "first_visible should be clamped to 0");
9457        // 10 visible + 2*overscan below = 14, but overscan above is 0 (clamped)
9458        // so total = 10 visible + 2 below overscan = 12 rows (0..12)
9459        assert!(w.last_visible <= 14, "last_visible should not exceed 14 at top");
9460        assert_eq!(w.offset_before, 0.0);
9461        assert!(w.offset_after > 0.0, "tail placeholder must be > 0");
9462    }
9463
9464    #[test]
9465    fn uniform_list_mid_scroll_correct_window() {
9466        // Scrolled to row 50 (y=1000) in a 1000-row, 20px list.
9467        let w = compute_virtual_list_window(1000, 20.0, 1000.0, 200.0, 2);
9468        assert!(w.first_visible <= 48, "must include overscan rows above 50");
9469        assert!(w.last_visible > 60, "must include rows below visible");
9470        // offset_before = first * 20px
9471        let expected_before = w.first_visible as f32 * 20.0;
9472        assert!((w.offset_before - expected_before).abs() < 1.0);
9473    }
9474
9475    #[test]
9476    fn uniform_list_near_end_does_not_exceed_total() {
9477        let w = compute_virtual_list_window(10, 20.0, 150.0, 100.0, 5);
9478        assert!(w.last_visible <= 10, "must not exceed total rows");
9479        assert!(w.offset_after >= 0.0);
9480    }
9481
9482    #[test]
9483    fn variable_list_basic() {
9484        // 5 rows with heights [10, 20, 30, 40, 50] => prefix = [0,10,30,60,100,150]
9485        let prefix: Vec<f32> = vec![0.0, 10.0, 30.0, 60.0, 100.0, 150.0];
9486        // Viewport from y=0 to y=40 should include rows 0,1,2 (tops at 0,10,30).
9487        let w = compute_virtual_list_window_variable(&prefix, 0.0, 40.0, 0);
9488        assert!(w.first_visible == 0);
9489        assert!(w.last_visible >= 3 && w.last_visible <= 5);
9490    }
9491
9492    #[test]
9493    fn empty_list_returns_zero_window() {
9494        let w = compute_virtual_list_window(0, 20.0, 0.0, 200.0, 2);
9495        assert_eq!(w.first_visible, 0);
9496        assert_eq!(w.last_visible, 0);
9497        assert_eq!(w.offset_before, 0.0);
9498        assert_eq!(w.offset_after, 0.0);
9499    }
9500}
9501
9502// =========================================================================
9503// P1-42: DependencyGraph tests
9504// =========================================================================
9505#[cfg(test)]
9506mod p1_42_dependency_graph_tests {
9507    use super::DependencyGraph;
9508
9509    #[test]
9510    fn register_and_query_single_dep() {
9511        let mut g = DependencyGraph::new();
9512        g.register(42, 100); // component 42 depends on state key 100
9513        let affected: Vec<u64> = g.affected_components(100).collect();
9514        assert_eq!(affected, vec![42]);
9515    }
9516
9517    #[test]
9518    fn unregister_removes_component() {
9519        let mut g = DependencyGraph::new();
9520        g.register(1, 10);
9521        g.register(2, 10);
9522        g.unregister(1);
9523        let affected: Vec<u64> = g.affected_components(10).collect();
9524        assert!(!affected.contains(&1), "component 1 must be gone after unregister");
9525        assert!(affected.contains(&2), "component 2 must still be present");
9526    }
9527
9528    #[test]
9529    fn no_deps_returns_empty() {
9530        let g = DependencyGraph::new();
9531        let affected: Vec<u64> = g.affected_components(999).collect();
9532        assert!(affected.is_empty());
9533        assert!(!g.has_dependents(999));
9534    }
9535
9536    #[test]
9537    fn edge_count_reflects_registrations() {
9538        let mut g = DependencyGraph::new();
9539        assert_eq!(g.edge_count(), 0);
9540        g.register(1, 10);
9541        g.register(2, 10);
9542        g.register(1, 20); // component 1 now depends on two keys
9543        assert_eq!(g.edge_count(), 3);
9544    }
9545
9546    #[test]
9547    fn re_register_after_unregister_works() {
9548        let mut g = DependencyGraph::new();
9549        g.register(5, 50);
9550        g.unregister(5);
9551        // Re-register with different key.
9552        g.register(5, 60);
9553        assert!(!g.has_dependents(50), "old key must be gone");
9554        assert!(g.has_dependents(60), "new key must be present");
9555    }
9556}