Skip to main content

cvkg_render_native/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.3)
2//!
3//! All AI agents contributing to this crate MUST follow ALL eight 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–8) ────────────────────────────────────────
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//! 8. HARDWARE VERIFIED — NEVER declare success based on mock data/rendering for native crates.
21//!                      Any change to input, rendering, or lifecycle MUST be verified via physical
22//!                      loopback (e.g., cargo run -p berserker) and signal path tracing.
23//!
24//! Sources:
25//! Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
26//! CVKG Extended: Section 14 of the CVKG Design Specification (v1.3)
27#![allow(
28    unused_imports,
29    clippy::single_component_path_imports,
30    dead_code,
31    clippy::items_after_test_module,
32    clippy::field_reassign_with_default,
33    clippy::collapsible_if,
34    clippy::unnecessary_map_or
35)]
36
37//! Platform-native widget delegation using winit and AccessKit
38//!
39//! This crate provides platform-specific rendering backends for native desktop targets
40//  using winit for window/event handling and AccessKit for accessibility tree integration.
41
42use cvkg_core::{FrameRenderer, Renderer};
43use image;
44// FIX #10: Wayland import gated to Linux only — was unconditional, broke macOS/Windows builds.
45use std::sync::Arc;
46use winit::{
47    application::ApplicationHandler,
48    event::{DeviceEvent, DeviceId, WindowEvent},
49    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
50    window::{Window, WindowId},
51};
52
53/// Represents the current state of a window.
54///
55/// Used by [`WindowStateDetector`] to track lifecycle transitions and drive
56/// rendering decisions (e.g., skip frames when occluded or minimized).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum WindowState {
59    /// Window is visible and active.
60    Normal,
61    /// Window is minimized to the Dock or taskbar.
62    Minimized,
63    /// Window is in fullscreen mode.
64    Fullscreen,
65    /// Window is in Split View (side-by-side with another window).
66    SplitView,
67    /// Window is occluded by another window.
68    Occluded,
69    /// Window is hidden (ordered out).
70    Hidden,
71}
72
73/// Tracks the current [`WindowState`] based on incoming winit [`WindowEvent`]s.
74///
75/// The detector maps raw winit events to high-level window states and exposes
76/// helpers for render-loop decisions ([`should_render`], [`control_flow`]).
77///
78/// # Usage
79///
80/// ```no_run
81/// use cvkg_render_native::{WindowStateDetector, WindowState};
82/// let mut detector = WindowStateDetector::new();
83/// // In your event loop:
84/// // if let Some(new_state) = detector.update_from_event(&event) { ... }
85/// ```
86pub struct WindowStateDetector {
87    state: WindowState,
88    is_key: bool,
89    is_main: bool,
90}
91
92impl WindowStateDetector {
93    /// Creates a new detector initialized to [`WindowState::Normal`].
94    pub fn new() -> Self {
95        Self {
96            state: WindowState::Normal,
97            is_key: false,
98            is_main: false,
99        }
100    }
101
102    /// Returns the current window state.
103    pub fn state(&self) -> WindowState {
104        self.state
105    }
106
107    /// Returns whether the window is the key (first responder) window.
108    pub fn is_key(&self) -> bool {
109        self.is_key
110    }
111
112    /// Returns whether the window is the main window.
113    pub fn is_main(&self) -> bool {
114        self.is_main
115    }
116
117    /// Updates the internal state based on a winit [`WindowEvent`].
118    ///
119    /// Returns `Some(WindowState)` if the state changed, `None` otherwise.
120    ///
121    /// # State mapping
122    ///
123    /// | winit event | resulting state |
124    /// |---|---|
125    /// | `Occluded(true)` | `Occluded` |
126    /// | `Focused(true)` | updates `is_key`; checks fullscreen |
127    /// | `Focused(false)` | updates `is_key` |
128    /// | Default | `Normal` |
129    ///
130    /// Note: `Minimized` and `Fullscreen` detection requires querying the
131    /// winit `Window` directly (see [`update_from_window`]).
132    pub fn update_from_event(&mut self, event: &WindowEvent) -> Option<WindowState> {
133        let old_state = self.state;
134        match event {
135            WindowEvent::Occluded(true) => {
136                self.state = WindowState::Occluded;
137            }
138            WindowEvent::Focused(focused) => {
139                self.is_key = *focused;
140                if !focused && self.state != WindowState::Minimized {
141                    self.state = WindowState::Normal;
142                }
143            }
144            _ => {}
145        };
146        if self.state != old_state {
147            Some(self.state)
148        } else {
149            None
150        }
151    }
152
153    /// Updates the state by querying the winit `Window` directly.
154    ///
155    /// This should be called once per frame to detect states that winit
156    /// does not emit as events (minimized, fullscreen).
157    ///
158    /// Returns `Some(WindowState)` if the state changed, `None` otherwise.
159    pub fn update_from_window(&mut self, window: &winit::window::Window) -> Option<WindowState> {
160        let old_state = self.state;
161        if window.is_minimized().unwrap_or(false) {
162            self.state = WindowState::Minimized;
163        } else if window.fullscreen().is_some() {
164            self.state = WindowState::Fullscreen;
165        } else if self.state == WindowState::Minimized || self.state == WindowState::Fullscreen {
166            // Transition back to Normal when no longer minimized/fullscreen
167            self.state = WindowState::Normal;
168        }
169        if self.state != old_state {
170            Some(self.state)
171        } else {
172            None
173        }
174    }
175
176    /// Returns `true` if the window should render a frame in the current state.
177    ///
178    /// Returns `false` for [`WindowState::Occluded`], [`WindowState::Minimized`],
179    /// and [`WindowState::Hidden`].
180    pub fn should_render(&self) -> bool {
181        !matches!(
182            self.state,
183            WindowState::Occluded | WindowState::Minimized | WindowState::Hidden
184        )
185    }
186
187    /// Returns the appropriate [`ControlFlow`] for the current state.
188    ///
189    /// Non-rendering states get `ControlFlow::Wait` (save CPU cycles);
190    /// rendering states get `ControlFlow::Poll` for maximum responsiveness.
191    pub fn control_flow(&self) -> ControlFlow {
192        if self.should_render() {
193            ControlFlow::Poll
194        } else {
195            ControlFlow::Wait
196        }
197    }
198}
199
200impl Default for WindowStateDetector {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206/// Hit-test helper for resize handles on windows with rounded corners.
207///
208/// macOS Tahoe uses a 26pt corner radius, which means the visual corner arc
209/// does not cover the full 19×19 resize hotspot. This struct expands the
210/// clickable area 8px beyond the visual corner edge so users can still grab
211/// the resize handle reliably.
212pub struct ResizeHitTest {
213    /// The size of the window in physical pixels.
214    window_size: winit::dpi::PhysicalSize<u32>,
215    /// The corner radius in points (logical pixels).
216    corner_radius: f32,
217    /// Extra expansion in pixels beyond the visual corner edge.
218    expansion: f32,
219}
220
221impl ResizeHitTest {
222    /// Creates a new hit-test helper.
223    ///
224    /// # Arguments
225    ///
226    /// * `window_size` — the current window size in physical pixels.
227    /// * `corner_radius` — the corner radius in points (e.g., 26.0 for Tahoe).
228    /// * `expansion` — extra pixels to expand beyond the visual edge (e.g., 8.0).
229    pub fn new(
230        window_size: winit::dpi::PhysicalSize<u32>,
231        corner_radius: f32,
232        expansion: f32,
233    ) -> Self {
234        Self {
235            window_size,
236            corner_radius,
237            expansion,
238        }
239    }
240
241    /// Tests whether `pos` (a point relative to the window's top-left corner)
242    /// falls within the expanded resize-hit region for any corner.
243    ///
244    /// The hit region for each corner is a square of side `corner_radius + expansion`,
245    /// anchored at the corner. A point is considered a hit if it falls within
246    /// any of the four corner squares.
247    pub fn hit_test(&self, pos: winit::dpi::PhysicalSize<u32>, corner_radius: f32) -> bool {
248        let r = corner_radius + self.expansion;
249        let w = self.window_size.width as f32;
250        let h = self.window_size.height as f32;
251        let px = pos.width as f32;
252        let py = pos.height as f32;
253
254        // Top-left corner: square [0, r) x [0, r)
255        if px <= r && py <= r {
256            return true;
257        }
258
259        // Top-right corner: square [w-r, w) x [0, r)
260        if px >= w - r && py <= r {
261            return true;
262        }
263
264        // Bottom-left corner: square [0, r) x [h-r, h)
265        if px <= r && py >= h - r {
266            return true;
267        }
268
269        // Bottom-right corner: square [w-r, w) x [h-r, h)
270        if px >= w - r && py >= h - r {
271            return true;
272        }
273
274        false
275    }
276}
277
278/// Platform safe area insets (menu bar, notch, etc.).
279///
280/// Values are in logical points.
281#[derive(Debug, Clone, Copy, PartialEq)]
282pub struct SafeAreaInsets {
283    /// Top inset (e.g., menu bar on macOS).
284    pub top: f32,
285    /// Bottom inset (e.g., Dock when at bottom).
286    pub bottom: f32,
287    /// Left inset.
288    pub left: f32,
289    /// Right inset.
290    pub right: f32,
291}
292
293impl SafeAreaInsets {
294    /// Returns zero insets on all sides.
295    pub fn zero() -> Self {
296        Self {
297            top: 0.0,
298            bottom: 0.0,
299            left: 0.0,
300            right: 0.0,
301        }
302    }
303
304    /// Returns appropriate safe-area insets for a given [`WindowState`].
305    ///
306    /// # Platform behavior
307    ///
308    /// * **Fullscreen** — zero insets (window owns the entire screen).
309    /// * **Normal** — 24pt top on macOS for the menu bar, 0 on other platforms.
310    /// * **All other states** — same as Normal.
311    pub fn for_window_state(state: WindowState) -> Self {
312        if state == WindowState::Fullscreen {
313            return Self::zero();
314        }
315        #[cfg(target_os = "macos")]
316        let top = 24.0;
317        #[cfg(not(target_os = "macos"))]
318        let top = 0.0;
319        Self {
320            top,
321            bottom: 0.0,
322            left: 0.0,
323            right: 0.0,
324        }
325    }
326}
327
328/// Native renderer backend implementing the Renderer trait.
329/// It wraps a shared SurtrRenderer for high-performance GPU drawing.
330pub struct NativeRenderer {
331    gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
332    delta_time: f32,
333    elapsed_time: f32,
334    berserker_mode: cvkg_core::BerserkerMode,
335    rage: f32,
336    window: Arc<Window>,
337}
338
339/// Custom events for the native application event loop, handling accessibility
340/// callbacks and routing window lifecycle control events from background threads.
341#[derive(Debug)]
342pub enum AppEvent {
343    /// Action request from the accessibility subsystem.
344    AccessibilityAction(accesskit::ActionRequest),
345    /// Request to close a specific window.
346    CloseWindow(winit::window::WindowId),
347    /// Request to set the title bar string of a window.
348    SetTitle(winit::window::WindowId, String),
349    /// Request to resize a window.
350    SetSize(winit::window::WindowId, f32, f32),
351    /// Request to change visibility of a window.
352    SetVisible(winit::window::WindowId, bool),
353    /// Request to bring a window to the front and focus it.
354    BringToFront(winit::window::WindowId),
355}
356
357impl NativeRenderer {
358    /// Create a new NativeRenderer (internal use by App)
359    fn new(
360        window: Arc<Window>,
361        gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
362        delta_time: f32,
363        elapsed_time: f32,
364        berserker_mode: cvkg_core::BerserkerMode,
365        rage: f32,
366    ) -> Self {
367        Self {
368            gpu,
369            delta_time,
370            elapsed_time,
371            berserker_mode,
372            rage,
373            window,
374        }
375    }
376
377    /// Start the CVKG native application with the given view.
378    /// This is the main entry point for desktop applications.
379    pub fn run<V: cvkg_core::View + 'static>(view: V) {
380        let event_loop = EventLoop::<AppEvent>::with_user_event()
381            .build()
382            .expect("Failed to create event loop");
383        event_loop.set_control_flow(ControlFlow::Wait);
384
385        let mut app = App {
386            view,
387            window_manager: WindowManager::new(),
388            gpu: None,
389            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
390            proxy: event_loop.create_proxy(),
391            start_time: std::time::Instant::now(),
392            last_frame_time: std::time::Instant::now(),
393            berserker_mode: cvkg_core::BerserkerMode::Normal,
394            rage: 0.0,
395            state_detector: WindowStateDetector::new(),
396            modifiers: winit::keyboard::ModifiersState::default(),
397            audio_engine: None,
398            haptic_engine: Arc::new(VisualHapticEngine::new()),
399        };
400
401        event_loop.run_app(&mut app).expect("Event loop error");
402    }
403}
404
405/// Native implementation of the cvkg_core::Window trait.
406/// Communicates state updates back to the winit event loop thread using an EventLoopProxy.
407struct NativeWindowWrapper {
408    winit_id: winit::window::WindowId,
409    window: Arc<winit::window::Window>,
410    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
411    is_key: Arc<std::sync::atomic::AtomicBool>,
412    is_main: bool,
413}
414
415impl cvkg_core::Window for NativeWindowWrapper {
416    /// Request that this window be closed.
417    fn close(&self) {
418        let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
419    }
420
421    /// Change the title bar text of this window.
422    fn set_title(&self, title: &str) {
423        let _ = self
424            .proxy
425            .send_event(AppEvent::SetTitle(self.winit_id, title.to_string()));
426    }
427
428    /// Request updating this window's dimensions.
429    fn set_size(&self, width: f32, height: f32) {
430        let _ = self
431            .proxy
432            .send_event(AppEvent::SetSize(self.winit_id, width, height));
433    }
434
435    /// Return true if this window has key focus.
436    fn is_key(&self) -> bool {
437        self.is_key.load(std::sync::atomic::Ordering::SeqCst)
438    }
439
440    /// Return true if this is the primary application window.
441    fn is_main(&self) -> bool {
442        self.is_main
443    }
444
445    /// Return true if this window is visible.
446    fn is_visible(&self) -> bool {
447        self.window.is_visible().unwrap_or(false)
448    }
449
450    /// Show or hide this window.
451    fn set_visible(&self, visible: bool) {
452        let _ = self
453            .proxy
454            .send_event(AppEvent::SetVisible(self.winit_id, visible));
455    }
456
457    /// Focus and bring this window to the foreground.
458    fn bring_to_front(&self) {
459        let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
460    }
461}
462
463/// Dynamic manager for all active native windows and their rendering contexts.
464pub struct WindowManager {
465    /// Mapping from native winit WindowId to internal WindowData.
466    pub windows: std::collections::HashMap<winit::window::WindowId, WindowData>,
467    /// Stack of windows ordered from back to front (end of vector is top-most).
468    pub window_stack: Vec<winit::window::WindowId>,
469    /// Mapping of winit window IDs to core IDs.
470    pub winit_to_core: std::collections::HashMap<winit::window::WindowId, cvkg_core::WindowId>,
471    /// Mapping of core window IDs to winit IDs.
472    pub core_to_winit: std::collections::HashMap<cvkg_core::WindowId, winit::window::WindowId>,
473    /// Monotonic counter to allocate unique core window IDs.
474    pub next_core_id: u64,
475}
476
477impl Default for WindowManager {
478    fn default() -> Self {
479        Self::new()
480    }
481}
482
483impl WindowManager {
484    /// Create an empty WindowManager.
485    pub fn new() -> Self {
486        Self {
487            windows: std::collections::HashMap::new(),
488            window_stack: Vec::new(),
489            winit_to_core: std::collections::HashMap::new(),
490            core_to_winit: std::collections::HashMap::new(),
491            next_core_id: 1,
492        }
493    }
494
495    /// Create and register a new native window.
496    pub fn create_window(
497        &mut self,
498        event_loop: &ActiveEventLoop,
499        gpu: &Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
500        proxy: winit::event_loop::EventLoopProxy<AppEvent>,
501        config: cvkg_core::WindowConfig,
502        is_main: bool,
503        view: &impl cvkg_core::View,
504    ) -> cvkg_core::WindowHandle {
505        let mut window_attrs = Window::default_attributes()
506            .with_title(&config.title)
507            .with_visible(true)
508            .with_transparent(config.transparent)
509            .with_decorations(config.decorations)
510            .with_inner_size(winit::dpi::LogicalSize::new(config.size.0, config.size.1));
511
512        if let Some(min) = config.min_size {
513            window_attrs =
514                window_attrs.with_min_inner_size(winit::dpi::LogicalSize::new(min.0, min.1));
515        }
516        if let Some(max) = config.max_size {
517            window_attrs =
518                window_attrs.with_max_inner_size(winit::dpi::LogicalSize::new(max.0, max.1));
519        }
520
521        let winit_level = match config.level {
522            cvkg_core::WindowLevel::Normal => winit::window::WindowLevel::Normal,
523            cvkg_core::WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
524            cvkg_core::WindowLevel::PopUpMenu => winit::window::WindowLevel::AlwaysOnTop,
525        };
526        window_attrs = window_attrs.with_window_level(winit_level);
527
528        let window = Arc::new(
529            event_loop
530                .create_window(window_attrs)
531                .expect("Failed to create window"),
532        );
533
534        let winit_id = window.id();
535        let core_id = cvkg_core::WindowId(self.next_core_id);
536        self.next_core_id += 1;
537
538        let is_key_focused = Arc::new(std::sync::atomic::AtomicBool::new(true));
539
540        let wrapper = Arc::new(NativeWindowWrapper {
541            winit_id,
542            window: window.clone(),
543            proxy: proxy.clone(),
544            is_key: is_key_focused.clone(),
545            is_main,
546        });
547
548        let handle = cvkg_core::WindowHandle::new(core_id, wrapper);
549
550        let vdom = cvkg_vdom::VDom::build(
551            view,
552            cvkg_core::Rect::new(0.0, 0.0, config.size.0, config.size.1),
553        );
554
555        let data = WindowData {
556            window: window.clone(),
557            accesskit_adapter: None,
558            vdom: Some(vdom),
559            cursor_pos: [0.0, 0.0],
560            last_redraw_start: std::time::Instant::now(),
561            frame_history: std::collections::VecDeque::with_capacity(60),
562            frame_count: 0,
563            last_pos: None,
564            is_dragging: false,
565            drag_start_pos: [0.0, 0.0],
566            drag_button: 0,
567            drag_threshold: 5.0,
568            is_key_focused,
569            is_main,
570            core_id,
571            window_handle: handle.clone(),
572        };
573
574        self.windows.insert(winit_id, data);
575        self.window_stack.push(winit_id);
576        self.winit_to_core.insert(winit_id, core_id);
577        self.core_to_winit.insert(core_id, winit_id);
578
579        if let Some(gpu_mutex) = gpu {
580            gpu_mutex.lock().unwrap().register_window(window.clone());
581        }
582
583        handle
584    }
585
586    /// Close and unregister a native window.
587    pub fn close_window(&mut self, winit_id: winit::window::WindowId) {
588        self.windows.remove(&winit_id);
589        self.window_stack.retain(|id| *id != winit_id);
590        if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
591            self.core_to_winit.remove(&core_id);
592        }
593    }
594
595    /// Bring a native window to the foreground and focus it.
596    pub fn bring_to_front(&mut self, winit_id: winit::window::WindowId) {
597        self.window_stack.retain(|id| *id != winit_id);
598        self.window_stack.push(winit_id);
599        if let Some(data) = self.windows.get(&winit_id) {
600            data.window.focus_window();
601        }
602    }
603
604    /// Get a reference to a window's data.
605    pub fn window(&self, winit_id: winit::window::WindowId) -> Option<&WindowData> {
606        self.windows.get(&winit_id)
607    }
608
609    /// Get a mutable reference to a window's data.
610    pub fn window_mut(&mut self, winit_id: winit::window::WindowId) -> Option<&mut WindowData> {
611        self.windows.get_mut(&winit_id)
612    }
613
614    /// Return the list of window IDs in current Z-order stack.
615    pub fn window_order(&self) -> &[winit::window::WindowId] {
616        &self.window_stack
617    }
618}
619
620pub struct WindowData {
621    window: Arc<Window>,
622    accesskit_adapter: Option<accesskit_winit::Adapter>,
623    vdom: Option<cvkg_vdom::VDom>,
624    cursor_pos: [f32; 2],
625    /// The instant the last redraw finished, used for measuring inter-frame gap timing.
626    last_redraw_start: std::time::Instant,
627    /// Sliding window of frame times for tail latency (P99) calculation.
628    frame_history: std::collections::VecDeque<f32>,
629    /// Total frames rendered on this window.
630    frame_count: u64,
631    /// Last window position for shake detection.
632    last_pos: Option<[i32; 2]>,
633    // ── Drag tracking ──────────────────────────────────────────────────────
634    /// Whether a drag is currently in progress.
635    is_dragging: bool,
636    /// The position where the drag started.
637    drag_start_pos: [f32; 2],
638    /// The button that initiated the drag.
639    drag_button: u32,
640    /// Drag threshold in logical pixels (pointer must move this far to start drag).
641    drag_threshold: f32,
642
643    // ── Multi-window tracking ──────────────────────────────────────────────
644    is_key_focused: Arc<std::sync::atomic::AtomicBool>,
645    is_main: bool,
646    core_id: cvkg_core::WindowId,
647    window_handle: cvkg_core::WindowHandle,
648}
649
650struct App<V: cvkg_core::View> {
651    view: V,
652    window_manager: WindowManager,
653    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
654    #[allow(dead_code)]
655    asset_manager: std::sync::Arc<NativeAssetManager>,
656    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
657    start_time: std::time::Instant,
658    last_frame_time: std::time::Instant,
659    berserker_mode: cvkg_core::BerserkerMode,
660    rage: f32,
661    /// Tracks the current window state for render-loop decisions.
662    state_detector: WindowStateDetector,
663    /// Tracks active modifier key states (Ctrl, Shift, Command, etc.).
664    modifiers: winit::keyboard::ModifiersState,
665    /// Cross-platform audio engine for spatialized sound cues.
666    audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
667    /// Visual haptic engine for micro-feedback animations.
668    haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
669}
670
671impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
672    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
673        if self.gpu.is_none() {
674            // Detect and apply system accessibility preferences at startup
675            let a11y_prefs = cvkg_core::AccessibilityPreferences::detect_from_system();
676            cvkg_core::set_accessibility_preferences(a11y_prefs);
677            if a11y_prefs.reduce_motion
678                || a11y_prefs.reduce_transparency
679                || a11y_prefs.increase_contrast
680            {
681                log::info!(
682                    "[Native] Accessibility prefs: motion={} transparency={} contrast={}",
683                    a11y_prefs.reduce_motion,
684                    a11y_prefs.reduce_transparency,
685                    a11y_prefs.increase_contrast
686                );
687            }
688
689            // Detect and apply system theme (dark/light)
690            let system_theme = cvkg_core::detect_system_theme();
691            log::info!("[Native] System theme detected: {:?}", system_theme);
692
693            // Initialize cross-platform audio engine
694            self.audio_engine =
695                RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
696
697            // Initialize visual haptic engine for micro-feedback
698            self.haptic_engine = Arc::new(VisualHapticEngine::new());
699
700            log::info!("[Native] App instance (resumed): {:p}", self);
701
702            let config = cvkg_core::WindowConfig {
703                title: "CVKG Berserker".to_string(),
704                size: (1280.0, 720.0),
705                min_size: None,
706                max_size: None,
707                resizable: true,
708                transparent: false,
709                decorations: true,
710                level: cvkg_core::WindowLevel::Normal,
711            };
712
713            let handle = self.window_manager.create_window(
714                event_loop,
715                &self.gpu,
716                self.proxy.clone(),
717                config,
718                true, // is_main
719                &self.view,
720            );
721
722            let winit_id = self
723                .window_manager
724                .core_to_winit
725                .get(&handle.id)
726                .copied()
727                .expect("Failed to get winit_id");
728            let window = self
729                .window_manager
730                .windows
731                .get(&winit_id)
732                .unwrap()
733                .window
734                .clone();
735
736            // Immediately set self.gpu to prevent re-entry
737            let gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
738            self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
739
740            // Register the window surface with the newly forged GPU renderer
741            if let Some(gpu_mutex) = &self.gpu {
742                gpu_mutex
743                    .lock()
744                    .expect("Failed to lock GPU mutex")
745                    .register_window(window.clone());
746            }
747
748            log::info!("[Native] Initialization complete.");
749            window.request_redraw();
750        }
751    }
752
753    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
754        if matches!(cause, winit::event::StartCause::Poll) {
755            // Too noisy
756        } else {
757            log::debug!("[Native] Event Loop Wake: {:?}", cause);
758        }
759    }
760
761    fn device_event(
762        &mut self,
763        _event_loop: &ActiveEventLoop,
764        _device_id: winit::event::DeviceId,
765        event: winit::event::DeviceEvent,
766    ) {
767        if matches!(event, winit::event::DeviceEvent::MouseMotion { .. }) {
768            // log::trace!("[Native] Raw Mouse Motion");
769        } else {
770            log::info!("[Native] DEVICE EVENT: {:?}", event);
771        }
772    }
773
774    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
775        if !matches!(event, WindowEvent::RedrawRequested)
776            && !matches!(event, WindowEvent::CursorMoved { .. })
777        {
778            log::info!(
779                "[Native] App instance: {:p} | WINDOW EVENT: {:?}",
780                self,
781                event
782            );
783        }
784
785        let gpu_arc = if let Some(g) = &self.gpu {
786            g.clone()
787        } else {
788            log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
789            return;
790        };
791
792        let mut close_window = false;
793        let mut bring_to_front = false;
794        let mut create_new_window = false;
795        // Cmd+Q was pressed — close all windows after the state block ends.
796        let mut quit_all = false;
797
798        {
799            let state = if let Some(s) = self.window_manager.windows.get_mut(&id) {
800                s
801            } else {
802                return;
803            };
804
805            match event {
806                WindowEvent::Moved(pos) => {
807                    let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
808                    let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
809                    let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
810
811                    if speed > 0.1 {
812                        // Significant kinetic injection
813                        self.rage = (self.rage + 0.2).min(1.0);
814                        log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
815                    }
816
817                    state.last_pos = Some([pos.x, pos.y]);
818                    state.window.request_redraw();
819                }
820                WindowEvent::DroppedFile(path) => {
821                    if let Some(vdom) = &state.vdom {
822                        vdom.dispatch_event(cvkg_core::Event::FileDrop {
823                            path: path.to_string_lossy().into_owned(),
824                        });
825                    }
826                }
827                WindowEvent::CloseRequested => {
828                    let close_action = cvkg_core::WindowCloseAction::Allow;
829                    match close_action {
830                        cvkg_core::WindowCloseAction::Allow
831                        | cvkg_core::WindowCloseAction::Confirm => {
832                            close_window = true;
833                        }
834                        cvkg_core::WindowCloseAction::Deny => {
835                            log::info!("[Native] Close request denied for window {:?}", id);
836                        }
837                    }
838                }
839                WindowEvent::Resized(physical_size) => {
840                    gpu_arc
841                        .lock()
842                        .expect("GPU mutex poisoned during resize")
843                        .resize(
844                            id,
845                            physical_size.width,
846                            physical_size.height,
847                            state.window.scale_factor() as f32,
848                        );
849                    state.window.request_redraw();
850                }
851                WindowEvent::Focused(focused) => {
852                    log::info!("[Native] Window focus changed: {}", focused);
853                    state
854                        .is_key_focused
855                        .store(focused, std::sync::atomic::Ordering::SeqCst);
856                    if focused {
857                        bring_to_front = true;
858                    }
859                }
860                WindowEvent::RedrawRequested => {
861                    if state.frame_count % 60 == 0 {
862                        log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
863                    }
864                    let size = state.window.inner_size();
865                    let scale = state.window.scale_factor();
866                    let logical_size = size.to_logical::<f32>(scale);
867
868                    let rect = cvkg_core::Rect {
869                        x: 0.0,
870                        y: 0.0,
871                        width: logical_size.width,
872                        height: logical_size.height,
873                    };
874
875                    // Record the start of this redraw and snapshot the previous frame's
876                    // start time before overwriting it, so inter-frame gap is measurable.
877                    let redraw_start = std::time::Instant::now();
878                    let last_redraw_start = state.last_redraw_start;
879                    // Update last_redraw_start immediately so the next frame measures correctly
880                    // even if this frame returns early.
881                    state.last_redraw_start = redraw_start;
882
883                    // Build new vdom and diff (layout pass)
884                    let layout_start = std::time::Instant::now();
885                    let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
886                    let layout_end = std::time::Instant::now();
887
888                    // Apply patches to the accessibility tree and the previous VDOM
889                    let state_flush_start = std::time::Instant::now();
890                    if let Some(prev_vdom) = &mut state.vdom {
891                        let patches = prev_vdom.diff(&new_vdom);
892                        let mut nodes = Vec::new();
893                        for patch in &patches {
894                            if let cvkg_vdom::VDomPatch::Create(node)
895                            | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
896                            {
897                                nodes
898                                    .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
899                            } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
900                                && let Some(node) = new_vdom.nodes.get(id)
901                            {
902                                nodes
903                                    .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
904                            }
905                        }
906                        if !nodes.is_empty() {
907                            if let Some(adapter) = &mut state.accesskit_adapter {
908                                adapter.update_if_active(|| accesskit::TreeUpdate {
909                                    nodes,
910                                    tree: None,
911                                    focus: accesskit::NodeId(1),
912                                });
913                            }
914                        }
915                        prev_vdom.apply_patches(patches);
916                    } else {
917                        state.vdom = Some(new_vdom);
918                    }
919                    let state_flush_end = std::time::Instant::now();
920
921                    // GPU rendering
922                    let draw_start = std::time::Instant::now();
923                    let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
924                    let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
925                    let mut gpu = gpu_arc
926                        .lock()
927                        .expect("GPU mutex poisoned during frame begin");
928                    let encoder = gpu.begin_frame(id);
929                    let mut renderer = NativeRenderer::new(
930                        state.window.clone(),
931                        gpu_arc.clone(),
932                        delta_time,
933                        elapsed_time,
934                        self.berserker_mode,
935                        self.rage,
936                    );
937                    // Release the gpu lock before calling render — the render methods each
938                    // re-acquire it per-call, allowing the view tree to interleave with other
939                    // work without holding one giant critical section across the whole draw.
940                    drop(gpu);
941                    self.view.render(&mut renderer, rect);
942                    let draw_end = std::time::Instant::now();
943
944                    // Re-acquire to submit the frame
945                    let gpu_submit_start = std::time::Instant::now();
946                    let mut gpu = gpu_arc
947                        .lock()
948                        .expect("GPU mutex poisoned during frame submit");
949                    gpu.render_frame();
950                    gpu.end_frame(encoder);
951                    let gpu_submit_end = std::time::Instant::now();
952
953                    // Build telemetry from this frame's timing measurements.
954                    // NOTE: input_time_ms measures the inter-frame gap (time from end of last frame
955                    // to start of this one), not input dispatch latency. The field name is defined
956                    // in cvkg_core::TelemetryData and kept as-is to match that struct.
957                    let mut telemetry = cvkg_core::TelemetryData::default();
958                    telemetry.input_time_ms =
959                        redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
960                    telemetry.layout_time_ms =
961                        layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
962                    telemetry.state_flush_time_ms = state_flush_end
963                        .duration_since(state_flush_start)
964                        .as_secs_f32()
965                        * 1000.0;
966                    telemetry.draw_time_ms =
967                        draw_end.duration_since(draw_start).as_secs_f32() * 1000.0;
968                    telemetry.gpu_submit_time_ms = gpu_submit_end
969                        .duration_since(gpu_submit_start)
970                        .as_secs_f32()
971                        * 1000.0;
972
973                    // Total frame time from redraw request to GPU submission complete
974                    let frame_time_ms =
975                        gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
976                    telemetry.frame_time_ms = frame_time_ms;
977
978                    // Tail Latency Tracking (P99 and Jitter) over a 100-frame sliding window.
979                    state.frame_history.push_back(frame_time_ms);
980                    if state.frame_history.len() > 100 {
981                        state.frame_history.pop_front();
982                    }
983
984                    let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
985                    sorted_frames
986                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
987
988                    if !sorted_frames.is_empty() {
989                        let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
990                        telemetry.p99_frame_time_ms =
991                            sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
992
993                        // Jitter: standard deviation of frame times over the sliding window.
994                        let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
995                        let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
996                            / sorted_frames.len() as f32;
997                        telemetry.frame_jitter_ms = variance.sqrt();
998                    }
999
1000                    // FIX #8: hardware_stall_detected is now reset each frame based on current
1001                    // jitter rather than being set once and never cleared. A single jittery frame
1002                    // no longer permanently flags the session. Jitter > 20ms is a heuristic for
1003                    // scheduling disruption (GC, OS preemption, slow layout) — not a confirmed
1004                    // hardware stall, but the field name is defined in cvkg_core::TelemetryData.
1005                    telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
1006
1007                    state.frame_count += 1;
1008
1009                    telemetry.berserker_rage = self.rage;
1010                    gpu.telemetry = telemetry;
1011                }
1012                WindowEvent::CursorEntered { .. } => {
1013                    log::info!("[Native] Cursor ENTERED window");
1014                    if let Some(vdom) = &state.vdom {
1015                        vdom.dispatch_event(cvkg_core::Event::PointerEnter);
1016                    }
1017                    state.window.request_redraw();
1018                }
1019                WindowEvent::CursorLeft { .. } => {
1020                    log::info!("[Native] Cursor LEFT window");
1021                    if let Some(vdom) = &state.vdom {
1022                        vdom.dispatch_event(cvkg_core::Event::PointerLeave);
1023                    }
1024                    state.window.request_redraw();
1025                }
1026                WindowEvent::CursorMoved { position, .. } => {
1027                    let scale = state.window.scale_factor();
1028                    let logical = position.to_logical::<f32>(scale);
1029                    log::info!(
1030                        "[Native] Cursor Moved: Physical={:?} Logical={:?} Scale={}",
1031                        position,
1032                        logical,
1033                        scale
1034                    );
1035                    state.cursor_pos = [logical.x, logical.y];
1036                    if let Some(vdom) = &state.vdom {
1037                        vdom.dispatch_event(cvkg_core::Event::PointerMove {
1038                            x: state.cursor_pos[0],
1039                            y: state.cursor_pos[1],
1040                            proximity_field: 0.0,
1041                            tilt: None,
1042                            azimuth: None,
1043                            pressure: Some(1.0),
1044                            barrel_rotation: None,
1045                        });
1046                    }
1047                    state.window.request_redraw();
1048                }
1049                WindowEvent::MouseInput {
1050                    state: mouse_state,
1051                    button,
1052                    ..
1053                } => {
1054                    log::info!(
1055                        "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
1056                        mouse_state,
1057                        button,
1058                        state.cursor_pos
1059                    );
1060                    if let Some(vdom) = &state.vdom {
1061                        let btn_id = match button {
1062                            winit::event::MouseButton::Left => 0,
1063                            winit::event::MouseButton::Right => 2,
1064                            winit::event::MouseButton::Middle => 1,
1065                            winit::event::MouseButton::Back => 3,
1066                            winit::event::MouseButton::Forward => 4,
1067                            winit::event::MouseButton::Other(id) => id as u32,
1068                        };
1069
1070                        match mouse_state {
1071                            winit::event::ElementState::Pressed => {
1072                                log::info!("[Native] Dispatching PointerDown to VDOM");
1073                                vdom.dispatch_event(cvkg_core::Event::PointerDown {
1074                                    x: state.cursor_pos[0],
1075                                    y: state.cursor_pos[1],
1076                                    button: btn_id,
1077                                    proximity_field: 0.0,
1078                                    tilt: None,
1079                                    azimuth: None,
1080                                    pressure: Some(1.0),
1081                                    barrel_rotation: None,
1082                                });
1083                            }
1084                            winit::event::ElementState::Released => {
1085                                log::info!("[Native] Dispatching PointerUp to VDOM");
1086                                vdom.dispatch_event(cvkg_core::Event::PointerUp {
1087                                    x: state.cursor_pos[0],
1088                                    y: state.cursor_pos[1],
1089                                    button: btn_id,
1090                                    tilt: None,
1091                                    azimuth: None,
1092                                    pressure: Some(0.0),
1093                                    barrel_rotation: None,
1094                                });
1095                            }
1096                        }
1097                        state.window.request_redraw();
1098                    } else {
1099                        log::warn!("[Native] Mouse input received but state.vdom is None!");
1100                    }
1101                }
1102                WindowEvent::MouseWheel { delta, .. } => {
1103                    if let Some(vdom) = &state.vdom {
1104                        let (dx, dy) = match delta {
1105                            winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
1106                            winit::event::MouseScrollDelta::PixelDelta(pos) => {
1107                                (pos.x as f32, pos.y as f32)
1108                            }
1109                        };
1110                        vdom.dispatch_event(cvkg_core::Event::PointerWheel {
1111                            x: state.cursor_pos[0],
1112                            y: state.cursor_pos[1],
1113                            delta_x: dx,
1114                            delta_y: dy,
1115                        });
1116                        state.window.request_redraw();
1117                    }
1118                }
1119                // ── Trackpad gestures (pinch-to-zoom, swipe) ──────────────────────
1120                // OS-agnostic: winit provides these on macOS trackpad, Windows precision
1121                // touchpads, and Linux (where supported). Falls back gracefully.
1122                WindowEvent::PinchGesture { delta, .. } => {
1123                    if let Some(vdom) = &state.vdom {
1124                        let scale = 1.0 + delta as f32;
1125                        let velocity = delta as f32;
1126                        vdom.dispatch_event(cvkg_core::Event::GesturePinch {
1127                            center: state.cursor_pos,
1128                            scale,
1129                            velocity,
1130                            phase: cvkg_core::TouchPhase::Moved,
1131                        });
1132                    }
1133                    // Provide micro-feedback on pinch
1134                    if let Some(audio) = &self.audio_engine {
1135                        audio.play_sound("nav_tick", 0.3);
1136                    }
1137                    self.haptic_engine
1138                        .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
1139                    state.window.request_redraw();
1140                }
1141                WindowEvent::RotationGesture { delta, .. } => {
1142                    if let Some(vdom) = &state.vdom {
1143                        let angle = delta;
1144                        vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
1145                            direction: [angle.cos(), angle.sin()],
1146                            velocity: delta.abs(),
1147                            phase: cvkg_core::TouchPhase::Moved,
1148                        });
1149                    }
1150                    state.window.request_redraw();
1151                }
1152                WindowEvent::KeyboardInput { event, .. } => {
1153                    if event.state == winit::event::ElementState::Pressed {
1154                        if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
1155                            // Cross-platform "command" key: ⌘ on macOS, Ctrl on all other OSes.
1156                            // This ensures keyboard shortcuts work identically on every platform
1157                            // without separate branches in every handler.
1158                            let is_cmd = if cfg!(target_os = "macos") {
1159                                self.modifiers.super_key()
1160                            } else {
1161                                self.modifiers.control_key()
1162                            };
1163                            let is_shift = self.modifiers.shift_key();
1164
1165                            if is_cmd {
1166                                match code {
1167                                    // ── Undo / Redo ───────────────────────────────
1168                                    winit::keyboard::KeyCode::KeyZ => {
1169                                        if is_shift {
1170                                            log::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
1171                                            let mut redo_action = None;
1172                                            cvkg_core::update_system_state(|s| {
1173                                                let mut s = s.clone();
1174                                                redo_action = s.undo_manager.redo();
1175                                                s
1176                                            });
1177                                            if let Some(action) = redo_action {
1178                                                action();
1179                                            }
1180                                            state.window.request_redraw();
1181                                        } else {
1182                                            log::info!("[Native] Shortcut: Undo (Cmd+Z)");
1183                                            let mut undo_action = None;
1184                                            cvkg_core::update_system_state(|s| {
1185                                                let mut s = s.clone();
1186                                                undo_action = s.undo_manager.undo();
1187                                                s
1188                                            });
1189                                            if let Some(action) = undo_action {
1190                                                action();
1191                                            }
1192                                            state.window.request_redraw();
1193                                        }
1194                                    }
1195                                    // Ctrl+Y as alternative Redo on non-macOS
1196                                    winit::keyboard::KeyCode::KeyY
1197                                        if !cfg!(target_os = "macos") =>
1198                                    {
1199                                        log::info!("[Native] Shortcut: Redo (Ctrl+Y)");
1200                                        let mut redo_action = None;
1201                                        cvkg_core::update_system_state(|s| {
1202                                            let mut s = s.clone();
1203                                            redo_action = s.undo_manager.redo();
1204                                            s
1205                                        });
1206                                        if let Some(action) = redo_action {
1207                                            action();
1208                                        }
1209                                        state.window.request_redraw();
1210                                    }
1211                                    // ── File operations ───────────────────────────
1212                                    winit::keyboard::KeyCode::KeyN => {
1213                                        log::info!("[Native] Shortcut: New Window (Cmd+N)");
1214                                        create_new_window = true;
1215                                    }
1216                                    winit::keyboard::KeyCode::KeyO => {
1217                                        log::info!("[Native] Shortcut: Open File (Cmd+O)");
1218                                        if let Some(vdom) = &state.vdom {
1219                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1220                                                key: "cmd+o".to_string(),
1221                                            });
1222                                        }
1223                                        state.window.request_redraw();
1224                                    }
1225                                    winit::keyboard::KeyCode::KeyS => {
1226                                        log::info!("[Native] Shortcut: Save (Cmd+S)");
1227                                        if let Some(vdom) = &state.vdom {
1228                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1229                                                key: "cmd+s".to_string(),
1230                                            });
1231                                        }
1232                                        state.window.request_redraw();
1233                                    }
1234                                    winit::keyboard::KeyCode::KeyW => {
1235                                        log::info!("[Native] Shortcut: Close Window (Cmd+W)");
1236                                        close_window = true;
1237                                    }
1238                                    winit::keyboard::KeyCode::KeyQ => {
1239                                        log::info!("[Native] Shortcut: Quit (Cmd+Q)");
1240                                        // Defer closing all windows until after the state borrow ends.
1241                                        quit_all = true;
1242                                    }
1243                                    // ── Clipboard ────────────────────────────────
1244                                    winit::keyboard::KeyCode::KeyC => {
1245                                        log::info!("[Native] Shortcut: Copy (Cmd+C)");
1246                                        if let Some(vdom) = &state.vdom {
1247                                            vdom.dispatch_event(cvkg_core::Event::Copy);
1248                                        }
1249                                        state.window.request_redraw();
1250                                    }
1251                                    winit::keyboard::KeyCode::KeyV => {
1252                                        log::info!("[Native] Shortcut: Paste (Cmd+V)");
1253                                        // Read the system clipboard. Fall back to empty string on
1254                                        // error so the Paste event is always delivered to the VDOM.
1255                                        let text = arboard::Clipboard::new()
1256                                            .ok()
1257                                            .and_then(|mut cb| cb.get_text().ok())
1258                                            .unwrap_or_default();
1259                                        if let Some(vdom) = &state.vdom {
1260                                            vdom.dispatch_event(cvkg_core::Event::Paste(text));
1261                                        }
1262                                        state.window.request_redraw();
1263                                    }
1264                                    winit::keyboard::KeyCode::KeyX => {
1265                                        log::info!("[Native] Shortcut: Cut (Cmd+X)");
1266                                        if let Some(vdom) = &state.vdom {
1267                                            vdom.dispatch_event(cvkg_core::Event::Cut);
1268                                        }
1269                                        state.window.request_redraw();
1270                                    }
1271                                    // ── Selection / search ────────────────────────
1272                                    winit::keyboard::KeyCode::KeyA => {
1273                                        log::info!("[Native] Shortcut: Select All (Cmd+A)");
1274                                        if let Some(vdom) = &state.vdom {
1275                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1276                                                key: "cmd+a".to_string(),
1277                                            });
1278                                        }
1279                                        state.window.request_redraw();
1280                                    }
1281                                    winit::keyboard::KeyCode::KeyF => {
1282                                        log::info!("[Native] Shortcut: Find (Cmd+F)");
1283                                        if let Some(vdom) = &state.vdom {
1284                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1285                                                key: "cmd+f".to_string(),
1286                                            });
1287                                        }
1288                                        state.window.request_redraw();
1289                                    }
1290                                    _ => {}
1291                                }
1292                            }
1293                        }
1294                    }
1295
1296                    if let Some(vdom) = &state.vdom
1297                        && let Some(cvkg_event) = convert_keyboard_event(event)
1298                    {
1299                        vdom.dispatch_event(cvkg_event);
1300                        state.window.request_redraw();
1301                    }
1302                }
1303
1304                WindowEvent::Ime(ime_event) => {
1305                    if let Some(vdom) = &state.vdom
1306                        && let Some(cvkg_event) = convert_ime_event(ime_event)
1307                    {
1308                        vdom.dispatch_event(cvkg_event);
1309                        state.window.request_redraw();
1310                    }
1311                }
1312                WindowEvent::ModifiersChanged(new_modifiers) => {
1313                    self.modifiers = new_modifiers.state();
1314                    let shift = self.modifiers.shift_key();
1315                    let ctrl = self.modifiers.control_key();
1316                    let alt = self.modifiers.alt_key();
1317                    let logo = self.modifiers.super_key();
1318                    cvkg_core::update_system_state(|st| {
1319                        let mut new_st = st.clone();
1320                        new_st.modifiers_shift = shift;
1321                        new_st.modifiers_ctrl = ctrl;
1322                        new_st.modifiers_alt = alt;
1323                        new_st.modifiers_logo = logo;
1324                        new_st
1325                    });
1326                }
1327                _ => {}
1328            }
1329        } // end of state block
1330
1331        if close_window {
1332            self.window_manager.close_window(id);
1333        }
1334        if quit_all {
1335            // Drain all windows; the is_empty check below will exit the event loop.
1336            for wid in self.window_manager.window_order().to_vec() {
1337                self.window_manager.close_window(wid);
1338            }
1339        }
1340        // Exit the event loop when all windows are closed (Cmd+W on last window, or Cmd+Q).
1341        if self.window_manager.windows.is_empty() {
1342            event_loop.exit();
1343        }
1344        if bring_to_front {
1345            self.window_manager.bring_to_front(id);
1346        }
1347        if create_new_window {
1348            self.window_manager.create_window(
1349                event_loop,
1350                &self.gpu,
1351                self.proxy.clone(),
1352                cvkg_core::WindowConfig {
1353                    title: "New CVKG Window".to_string(),
1354                    size: (800.0, 600.0),
1355                    ..Default::default()
1356                },
1357                false, // is_main
1358                &self.view,
1359            );
1360        }
1361    }
1362
1363    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1364        match event {
1365            AppEvent::AccessibilityAction(request) => {
1366                let node_id = cvkg_vdom::NodeId(request.target.0);
1367                let target_state = self.window_manager.windows.values_mut().find(|s| {
1368                    s.vdom
1369                        .as_ref()
1370                        .map_or(false, |v| v.nodes.contains_key(&node_id))
1371                });
1372
1373                if let Some(state) = target_state
1374                    && let Some(vdom) = &state.vdom
1375                    && let Some(node) = vdom.nodes.get(&node_id)
1376                    && request.action == accesskit::Action::Click
1377                {
1378                    let event = cvkg_core::Event::PointerClick {
1379                        x: node.layout.x + node.layout.width / 2.0,
1380                        y: node.layout.y + node.layout.height / 2.0,
1381                        button: 0, // Assume left click for accessibility actions
1382                        tilt: None,
1383                        azimuth: None,
1384                        pressure: Some(1.0),
1385                        barrel_rotation: None,
1386                    };
1387                    vdom.dispatch_event(event);
1388                }
1389            }
1390            AppEvent::CloseWindow(winit_id) => {
1391                self.window_manager.close_window(winit_id);
1392                if self.window_manager.windows.is_empty() {
1393                    event_loop.exit();
1394                }
1395            }
1396            AppEvent::SetTitle(winit_id, title) => {
1397                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1398                    data.window.set_title(&title);
1399                }
1400            }
1401            AppEvent::SetSize(winit_id, width, height) => {
1402                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1403                    let _ = data
1404                        .window
1405                        .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1406                }
1407            }
1408            AppEvent::SetVisible(winit_id, visible) => {
1409                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1410                    data.window.set_visible(visible);
1411                }
1412            }
1413            AppEvent::BringToFront(winit_id) => {
1414                self.window_manager.bring_to_front(winit_id);
1415            }
1416        }
1417    }
1418
1419    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1420        // Apply Rage Decay: rage naturally settles to 0 over time.
1421        self.rage = (self.rage - 0.02).max(0.0);
1422
1423        // Frame Throttling: 60FPS target (16.6ms)
1424        let now = std::time::Instant::now();
1425        let target_interval = std::time::Duration::from_millis(16);
1426
1427        if now.duration_since(self.last_frame_time) >= target_interval {
1428            if self.rage > 0.01 {
1429                // Only log heartbeat when there is kinetic activity
1430                log::debug!("[Native] Heartbeat ticking (rage: {})", self.rage);
1431            }
1432            self.last_frame_time = now;
1433            for window_state in self.window_manager.windows.values() {
1434                window_state.window.request_redraw();
1435            }
1436            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1437                now + target_interval,
1438            ));
1439        } else {
1440            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1441                self.last_frame_time + target_interval,
1442            ));
1443        }
1444    }
1445}
1446
1447impl cvkg_core::ElapsedTime for NativeRenderer {
1448    fn delta_time(&self) -> f32 {
1449        self.delta_time
1450    }
1451
1452    fn elapsed_time(&self) -> f32 {
1453        self.elapsed_time
1454    }
1455}
1456
1457impl cvkg_core::Renderer for NativeRenderer {
1458    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1459        self.gpu
1460            .lock()
1461            .expect("GPU mutex poisoned: fill_rect")
1462            .fill_rect(rect, color);
1463    }
1464    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
1465        self.gpu
1466            .lock()
1467            .expect("GPU mutex poisoned: fill_rounded_rect")
1468            .fill_rounded_rect(rect, radius, color);
1469    }
1470    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1471        self.gpu
1472            .lock()
1473            .expect("GPU mutex poisoned: fill_ellipse")
1474            .fill_ellipse(rect, color);
1475    }
1476    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1477        self.gpu
1478            .lock()
1479            .expect("GPU mutex poisoned: stroke_rect")
1480            .stroke_rect(rect, color, stroke_width);
1481    }
1482    fn stroke_rounded_rect(
1483        &mut self,
1484        rect: cvkg_core::Rect,
1485        radius: f32,
1486        color: [f32; 4],
1487        stroke_width: f32,
1488    ) {
1489        self.gpu
1490            .lock()
1491            .expect("GPU mutex poisoned: stroke_rounded_rect")
1492            .stroke_rounded_rect(rect, radius, color, stroke_width);
1493    }
1494    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1495        self.gpu
1496            .lock()
1497            .expect("GPU mutex poisoned: stroke_ellipse")
1498            .stroke_ellipse(rect, color, stroke_width);
1499    }
1500    fn draw_line(
1501        &mut self,
1502        x1: f32,
1503        y1: f32,
1504        x2: f32,
1505        y2: f32,
1506        color: [f32; 4],
1507        stroke_width: f32,
1508    ) {
1509        self.gpu
1510            .lock()
1511            .expect("GPU mutex poisoned: draw_line")
1512            .draw_line(x1, y1, x2, y2, color, stroke_width);
1513    }
1514    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1515        self.gpu
1516            .lock()
1517            .expect("GPU mutex poisoned: draw_text")
1518            .draw_text(text, x, y, size, color);
1519    }
1520    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1521        self.gpu
1522            .lock()
1523            .expect("GPU mutex poisoned: measure_text")
1524            .measure_text(text, size)
1525    }
1526    fn draw_linear_gradient(
1527        &mut self,
1528        rect: cvkg_core::Rect,
1529        start_color: [f32; 4],
1530        end_color: [f32; 4],
1531        angle: f32,
1532    ) {
1533        self.gpu
1534            .lock()
1535            .expect("GPU mutex poisoned: draw_linear_gradient")
1536            .draw_linear_gradient(rect, start_color, end_color, angle);
1537    }
1538    fn draw_radial_gradient(
1539        &mut self,
1540        rect: cvkg_core::Rect,
1541        inner_color: [f32; 4],
1542        outer_color: [f32; 4],
1543    ) {
1544        self.gpu
1545            .lock()
1546            .expect("GPU mutex poisoned: draw_radial_gradient")
1547            .draw_radial_gradient(rect, inner_color, outer_color);
1548    }
1549    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
1550        self.gpu
1551            .lock()
1552            .expect("GPU mutex poisoned: draw_texture")
1553            .draw_texture(texture_id, rect);
1554    }
1555    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
1556        self.gpu
1557            .lock()
1558            .expect("GPU mutex poisoned: draw_image")
1559            .draw_image(image_name, rect);
1560    }
1561    fn load_image(&mut self, name: &str, data: &[u8]) {
1562        self.gpu
1563            .lock()
1564            .expect("GPU mutex poisoned: load_image")
1565            .load_image(name, data);
1566    }
1567    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
1568        self.gpu
1569            .lock()
1570            .expect("GPU mutex poisoned: push_clip_rect")
1571            .push_clip_rect(rect);
1572    }
1573    fn pop_clip_rect(&mut self) {
1574        self.gpu
1575            .lock()
1576            .expect("GPU mutex poisoned: pop_clip_rect")
1577            .pop_clip_rect();
1578    }
1579    fn push_opacity(&mut self, opacity: f32) {
1580        self.gpu
1581            .lock()
1582            .expect("GPU mutex poisoned: push_opacity")
1583            .push_opacity(opacity);
1584    }
1585    fn draw_3d_cube(&mut self, rect: cvkg_core::Rect, color: [f32; 4], rotation: [f32; 3]) {
1586        self.gpu
1587            .lock()
1588            .expect("GPU mutex poisoned: draw_3d_cube")
1589            .draw_3d_cube(rect, color, rotation);
1590    }
1591    fn pop_opacity(&mut self) {
1592        self.gpu
1593            .lock()
1594            .expect("GPU mutex poisoned: pop_opacity")
1595            .pop_opacity();
1596    }
1597    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
1598        self.gpu
1599            .lock()
1600            .expect("GPU mutex poisoned: bifrost")
1601            .bifrost(rect, blur, saturation, opacity);
1602    }
1603    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
1604        self.gpu
1605            .lock()
1606            .expect("GPU mutex poisoned: push_mjolnir_slice")
1607            .push_mjolnir_slice(angle, offset);
1608    }
1609    fn pop_mjolnir_slice(&mut self) {
1610        self.gpu
1611            .lock()
1612            .expect("GPU mutex poisoned: pop_mjolnir_slice")
1613            .pop_mjolnir_slice();
1614    }
1615    fn mjolnir_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
1616        self.gpu
1617            .lock()
1618            .expect("GPU mutex poisoned: mjolnir_shatter")
1619            .mjolnir_shatter(rect, pieces, force, color);
1620    }
1621    fn mjolnir_fluid_shatter(
1622        &mut self,
1623        rect: cvkg_core::Rect,
1624        pieces: u32,
1625        force: f32,
1626        color: [f32; 4],
1627    ) {
1628        self.gpu
1629            .lock()
1630            .expect("GPU mutex poisoned: mjolnir_fluid_shatter")
1631            .mjolnir_fluid_shatter(rect, pieces, force, color);
1632    }
1633    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1634        self.gpu
1635            .lock()
1636            .expect("GPU mutex poisoned: draw_mjolnir_bolt")
1637            .draw_mjolnir_bolt(from, to, color);
1638    }
1639    fn gungnir(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32, intensity: f32) {
1640        self.gpu
1641            .lock()
1642            .expect("GPU mutex poisoned: gungnir")
1643            .gungnir(rect, color, radius, intensity);
1644    }
1645    fn mani_glow(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32) {
1646        self.gpu
1647            .lock()
1648            .expect("GPU mutex poisoned: mani_glow")
1649            .mani_glow(rect, color, radius);
1650    }
1651    fn register_handler(
1652        &mut self,
1653        event_type: &str,
1654        handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1655    ) {
1656        self.gpu
1657            .lock()
1658            .expect("GPU mutex poisoned: register_handler")
1659            .register_handler(event_type, handler);
1660    }
1661    fn push_vnode(&mut self, rect: cvkg_core::Rect, name: &'static str) {
1662        self.gpu
1663            .lock()
1664            .expect("GPU mutex poisoned: push_vnode")
1665            .push_vnode(rect, name);
1666    }
1667    fn pop_vnode(&mut self) {
1668        self.gpu
1669            .lock()
1670            .expect("GPU mutex poisoned: pop_vnode")
1671            .pop_vnode();
1672    }
1673    // FIX #1: Removed duplicate definitions of set_z_index and get_z_index.
1674    // They appeared twice in this impl block (after pop_vnode and after register_shared_element),
1675    // which is a hard compiler error. Exactly one definition of each is kept here.
1676    fn set_z_index(&mut self, z: f32) {
1677        self.gpu
1678            .lock()
1679            .expect("GPU mutex poisoned: set_z_index")
1680            .set_z_index(z);
1681    }
1682    fn get_z_index(&self) -> f32 {
1683        self.gpu
1684            .lock()
1685            .expect("GPU mutex poisoned: get_z_index")
1686            .get_z_index()
1687    }
1688    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
1689        self.gpu
1690            .lock()
1691            .expect("GPU mutex poisoned: register_shared_element")
1692            .register_shared_element(id, rect);
1693    }
1694    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1695        self.gpu
1696            .lock()
1697            .expect("GPU mutex poisoned: load_svg")
1698            .load_svg(name, svg_data);
1699    }
1700    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
1701        self.gpu
1702            .lock()
1703            .expect("GPU mutex poisoned: draw_svg")
1704            .draw_svg(name, rect, None, 0);
1705    }
1706    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1707        self.gpu
1708            .lock()
1709            .expect("GPU mutex poisoned: get_telemetry")
1710            .telemetry
1711            .clone()
1712    }
1713    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
1714        self.gpu
1715            .lock()
1716            .expect("GPU mutex poisoned: prewarm_vram")
1717            .prewarm_vram(assets);
1718    }
1719    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
1720        self.gpu
1721            .lock()
1722            .expect("GPU mutex poisoned: push_transform")
1723            .push_transform(translation, scale, rotation);
1724    }
1725    fn pop_transform(&mut self) {
1726        self.gpu
1727            .lock()
1728            .expect("GPU mutex poisoned: pop_transform")
1729            .pop_transform();
1730    }
1731
1732    fn set_berserker_mode(&mut self, state: cvkg_core::BerserkerMode) {
1733        self.berserker_mode = state;
1734
1735        // Berserker Determinism: Apply OS-level scheduler priority hints for GodMode.
1736        // SAFETY: setpriority is a POSIX syscall. We pass PRIO_PROCESS with pid=0 (self).
1737        // Failure is silently ignored via let _ because insufficient permissions are expected
1738        // in unprivileged environments and must not crash the render loop.
1739        if state == cvkg_core::BerserkerMode::GodMode {
1740            log::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
1741            #[cfg(target_os = "linux")]
1742            unsafe {
1743                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
1744            }
1745        } else {
1746            #[cfg(target_os = "linux")]
1747            unsafe {
1748                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
1749            }
1750        }
1751
1752        self.gpu
1753            .lock()
1754            .expect("GPU mutex poisoned: set_berserker_mode")
1755            .set_berserker_mode(state);
1756    }
1757
1758    fn set_rage(&mut self, rage: f32) {
1759        self.rage = rage;
1760        self.gpu
1761            .lock()
1762            .expect("GPU mutex poisoned: set_rage")
1763            .set_rage(rage);
1764    }
1765
1766    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
1767        self.gpu
1768            .lock()
1769            .expect("GPU mutex poisoned: memoize")
1770            .memoize(id, data_hash, render_fn);
1771    }
1772    fn request_redraw(&mut self) {
1773        self.window.request_redraw();
1774    }
1775
1776    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
1777    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
1778    ///
1779    /// FIX #4: capture_frame() returns a Future that borrows the SurtrRenderer, so the
1780    /// MutexGuard must remain alive until block_on completes — the guard cannot be dropped
1781    /// before the future is driven to completion. The lock is held for the duration of the
1782    /// GPU readback. This is acceptable because capture_png is an infrequent, explicit
1783    /// user-triggered operation (not called on the hot render path), so blocking other
1784    /// render calls for the readback duration is not a practical concern.
1785    fn capture_png(&mut self) -> Vec<u8> {
1786        log::info!("CAPTURING_FRAME: Initiating GPU readback...");
1787        // INVARIANT: The MutexGuard `gpu` must outlive the future returned by capture_frame()
1788        // because the future borrows from the SurtrRenderer. We therefore lock, block_on the
1789        // future (driving it to completion), and only then allow the guard to drop.
1790        let gpu = self.gpu.lock().expect("GPU mutex poisoned: capture_png");
1791        pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
1792            log::error!("GPU frame capture failed: {}", e);
1793            Vec::new() // Return empty buffer on failure — do not panic the render loop
1794        })
1795    }
1796
1797    fn print(&mut self) {
1798        log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
1799        // In a production environment, this would interface with CUPS/GDI/AirPrint.
1800        // For the Ulfhednar prototype, we simulate the handshake.
1801        println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
1802    }
1803}
1804
1805// ── Native Menu Bar Builder ───────────────────────────────────────────
1806
1807// ── Event Conversion Helpers ───────────────────────────────────────────
1808
1809fn convert_keyboard_event(event: winit::event::KeyEvent) -> Option<cvkg_core::Event> {
1810    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
1811        let key_str = format!("{:?}", code);
1812        if event.state == winit::event::ElementState::Pressed {
1813            Some(cvkg_core::Event::KeyDown { key: key_str })
1814        } else {
1815            Some(cvkg_core::Event::KeyUp { key: key_str })
1816        }
1817    } else {
1818        None
1819    }
1820}
1821
1822fn convert_ime_event(event: winit::event::Ime) -> Option<cvkg_core::Event> {
1823    if let winit::event::Ime::Commit(string) = event {
1824        Some(cvkg_core::Event::Ime(string))
1825    } else {
1826        None
1827    }
1828}
1829
1830fn convert_mouse_event(
1831    state: winit::event::ElementState,
1832    position: [f32; 2],
1833    button: u32,
1834) -> cvkg_core::Event {
1835    match state {
1836        winit::event::ElementState::Pressed => cvkg_core::Event::PointerDown {
1837            x: position[0],
1838            y: position[1],
1839            button,
1840            proximity_field: 0.0,
1841            tilt: None,
1842            azimuth: None,
1843            pressure: Some(1.0),
1844            barrel_rotation: None,
1845        },
1846        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
1847            x: position[0],
1848            y: position[1],
1849            button,
1850            tilt: None,
1851            azimuth: None,
1852            pressure: Some(0.0),
1853            barrel_rotation: None,
1854        },
1855    }
1856}
1857
1858// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
1859
1860struct ShieldWall {
1861    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
1862}
1863
1864impl accesskit::ActionHandler for ShieldWall {
1865    fn do_action(&mut self, request: accesskit::ActionRequest) {
1866        let _ = self
1867            .proxy
1868            .send_event(AppEvent::AccessibilityAction(request));
1869    }
1870}
1871
1872impl accesskit::ActivationHandler for ShieldWall {
1873    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
1874        let mut root = accesskit::Node::new(accesskit::Role::Window);
1875        root.set_label("CVKG Application");
1876
1877        let root_id = accesskit::NodeId(1);
1878        Some(accesskit::TreeUpdate {
1879            nodes: vec![(root_id, root)],
1880            tree: Some(accesskit::Tree::new(root_id)),
1881            focus: root_id,
1882        })
1883    }
1884}
1885
1886impl accesskit::DeactivationHandler for ShieldWall {
1887    fn deactivate_accessibility(&mut self) {}
1888}
1889
1890type AssetCacheMap =
1891    std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
1892
1893/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
1894///
1895/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
1896/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
1897/// without blocking concurrent render-loop readers.
1898pub struct NativeAssetManager {
1899    cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
1900}
1901
1902impl Default for NativeAssetManager {
1903    fn default() -> Self {
1904        Self::new()
1905    }
1906}
1907
1908impl NativeAssetManager {
1909    /// Create a new, empty NativeAssetManager.
1910    pub fn new() -> Self {
1911        Self {
1912            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
1913                std::collections::HashMap::new(),
1914            )),
1915        }
1916    }
1917}
1918
1919impl cvkg_core::AssetManager for NativeAssetManager {
1920    /// Return the cached asset state for `url`.
1921    ///
1922    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
1923    /// Slow path (cache miss): atomically insert a Loading sentinel via `rcu()`,
1924    /// then spawn a background thread for I/O. The `rcu()` closure may execute
1925    /// more than once under contention, so `already_tracked` is determined by
1926    /// whether the closure actually inserted the Loading entry (detected by checking
1927    /// the returned map). This prevents duplicate I/O threads for the same URL.
1928    ///
1929    /// FIX #5: The previous implementation set `already_tracked` inside the `rcu`
1930    /// closure body, which is incorrect because `rcu` retries the closure on
1931    /// contention — the bool would reflect only the last execution. The fix uses
1932    /// the fast-path check result plus the atomic `rcu` insertion to determine
1933    /// whether a thread must be spawned, making the logic correct under concurrency.
1934    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
1935        // Fast path: lock-free read from the current cache snapshot.
1936        if let Some(state) = self.cache.load().get(url) {
1937            return state.clone();
1938        }
1939
1940        let cache = self.cache.clone();
1941        let key = url.to_string();
1942
1943        // Slow path: atomically insert Loading if the key is absent.
1944        // `rcu` returns the final committed map; we inspect it to determine
1945        // whether *this* call was the one that inserted Loading (and thus
1946        // should spawn the I/O thread) versus a concurrent call that beat us.
1947        let mut we_inserted = false;
1948        self.cache.rcu(|map| {
1949            if map.contains_key(&key) {
1950                // Another caller already claimed this URL — do not insert.
1951                (**map).clone()
1952            } else {
1953                we_inserted = true;
1954                let mut m = (**map).clone();
1955                m.insert(key.clone(), cvkg_core::AssetState::Loading);
1956                m
1957            }
1958        });
1959
1960        // Only the caller that performed the insertion spawns the I/O thread,
1961        // preventing duplicate concurrent reads for the same asset URL.
1962        if we_inserted {
1963            let cache_inner = cache.clone();
1964            let key_inner = key.clone();
1965
1966            std::thread::spawn(move || {
1967                log::debug!("[Native] Asynchronously loading asset: {}", key_inner);
1968                let result = match std::fs::read(&key_inner) {
1969                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
1970                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
1971                };
1972
1973                cache_inner.rcu(move |map| {
1974                    let mut m = (**map).clone();
1975                    m.insert(key_inner.clone(), result.clone());
1976                    m
1977                });
1978            });
1979        }
1980
1981        cvkg_core::AssetState::Loading
1982    }
1983
1984    /// Trigger a background load of `url` without waiting for the result.
1985    ///
1986    /// FIX #6: The previous implementation had a bare fast-path check followed
1987    /// by an unconditional thread spawn, allowing two concurrent calls for the
1988    /// same URL to both spawn I/O threads. Now uses the same rcu-based insertion
1989    /// guard as `load_image` to ensure exactly one thread is spawned per URL.
1990    fn preload_image(&self, url: &str) {
1991        // Fast path: if already in cache (any state), no work to do.
1992        if self.cache.load().contains_key(url) {
1993            return;
1994        }
1995
1996        let cache = self.cache.clone();
1997        let key = url.to_string();
1998
1999        let mut we_inserted = false;
2000        self.cache.rcu(|map| {
2001            if map.contains_key(&key) {
2002                (**map).clone()
2003            } else {
2004                we_inserted = true;
2005                let mut m = (**map).clone();
2006                m.insert(key.clone(), cvkg_core::AssetState::Loading);
2007                m
2008            }
2009        });
2010
2011        if we_inserted {
2012            std::thread::spawn(move || {
2013                log::debug!("[Native] Preloading asset: {}", key);
2014                let result = match std::fs::read(&key) {
2015                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2016                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2017                };
2018
2019                cache.rcu(move |map| {
2020                    let mut m = (**map).clone();
2021                    m.insert(key.clone(), result.clone());
2022                    m
2023                });
2024            });
2025        }
2026    }
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031    use super::*;
2032    use cvkg_core::AssetManager;
2033    use std::io::Write;
2034
2035    /// FIX #12: Replaced hardcoded relative path "test_asset.png" with a temp-dir path
2036    /// constructed from a unique per-test name. The previous path was written to the
2037    /// process working directory, which varies by invocation and causes collisions when
2038    /// tests run in parallel or when a prior run panics before cleanup.
2039    #[test]
2040    fn test_native_asset_manager_loading() {
2041        let manager = NativeAssetManager::new();
2042        let temp_path = std::env::temp_dir().join("cvkg_test_asset_loading.png");
2043        let temp_file_path = temp_path.to_str().expect("temp path must be valid UTF-8");
2044        let test_data = b"fake-image-data";
2045
2046        // Create a temporary file in the OS temp directory
2047        let mut file = std::fs::File::create(temp_file_path).unwrap();
2048        file.write_all(test_data).unwrap();
2049        drop(file);
2050
2051        // First call returns Loading and spawns the background I/O thread
2052        let mut state = manager.load_image(temp_file_path);
2053
2054        // Poll until Ready or timeout
2055        let start = std::time::Instant::now();
2056        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2057            std::thread::sleep(std::time::Duration::from_millis(10));
2058            state = manager.load_image(temp_file_path);
2059        }
2060
2061        if let cvkg_core::AssetState::Ready(data) = state {
2062            assert_eq!(&*data, test_data);
2063        } else {
2064            let _ = std::fs::remove_file(temp_file_path);
2065            panic!("Expected Ready state, got {:?}", state);
2066        }
2067
2068        // Verify fast path returns Ready immediately from cache
2069        let state2 = manager.load_image(temp_file_path);
2070        if let cvkg_core::AssetState::Ready(data) = state2 {
2071            assert_eq!(&*data, test_data);
2072        } else {
2073            let _ = std::fs::remove_file(temp_file_path);
2074            panic!("Expected Ready state (cached), got {:?}", state2);
2075        }
2076
2077        let _ = std::fs::remove_file(temp_file_path);
2078    }
2079
2080    #[test]
2081    fn test_native_asset_manager_error() {
2082        let manager = NativeAssetManager::new();
2083        let path = "non_existent_file_cvkg_test.png";
2084        let mut state = manager.load_image(path);
2085
2086        let start = std::time::Instant::now();
2087        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2088            std::thread::sleep(std::time::Duration::from_millis(10));
2089            state = manager.load_image(path);
2090        }
2091
2092        if let cvkg_core::AssetState::Error(_) = state {
2093            // Expected — non-existent file must produce an Error state
2094        } else {
2095            panic!("Expected Error state, got {:?}", state);
2096        }
2097    }
2098
2099    #[test]
2100    fn test_event_conversion() {
2101        // Mouse press event
2102        let event = convert_mouse_event(winit::event::ElementState::Pressed, [10.0, 20.0], 0);
2103        if let cvkg_core::Event::PointerDown { x, y, button, .. } = event {
2104            assert_eq!(x, 10.0);
2105            assert_eq!(y, 20.0);
2106            assert_eq!(button, 0);
2107        } else {
2108            panic!("Expected PointerDown");
2109        }
2110
2111        // IME commit event
2112        let event = convert_ime_event(winit::event::Ime::Commit("hello".to_string()));
2113        if let Some(cvkg_core::Event::Ime(s)) = event {
2114            assert_eq!(s, "hello");
2115        } else {
2116            panic!("Expected Ime event");
2117        }
2118    }
2119}
2120
2121/// load_icon — Searches known asset directories for 'icon.png'.
2122/// Returns a winit Icon if found and decodable, None otherwise.
2123/// All failures are logged at warn level — missing icons are non-fatal.
2124fn load_icon() -> Option<winit::window::Icon> {
2125    // FIX #13: Replaced unwrap_or_default() with unwrap_or_else that logs the failure.
2126    // unwrap_or_default() produced an empty PathBuf silently, making all subsequent
2127    // icon path lookups silently fail with no diagnostic output.
2128    let base = std::env::current_dir().unwrap_or_else(|e| {
2129        log::warn!(
2130            "[Native] Failed to get current directory for icon search: {}",
2131            e
2132        );
2133        std::path::PathBuf::new()
2134    });
2135
2136    let mut candidates = vec![
2137        base.join("icon.png"),
2138        base.join("crates/ulfhednar/icons/icon.png"),
2139        base.join("ulfhednar/icons/icon.png"),
2140        base.join("crates/ulfhednar/assets/icon.png"),
2141        base.join("ulfhednar/assets/icon.png"),
2142        base.join("assets/icon.png"),
2143    ];
2144
2145    // Also search relative to the executable directory
2146    if let Ok(exe_path) = std::env::current_exe()
2147        && let Some(exe_dir) = exe_path.parent()
2148    {
2149        candidates.push(exe_dir.join("icons/icon.png"));
2150        candidates.push(exe_dir.join("assets/icon.png"));
2151        candidates.push(exe_dir.join("icon.png"));
2152        if let Some(parent) = exe_dir.parent() {
2153            candidates.push(parent.join("icons/icon.png"));
2154            candidates.push(parent.join("assets/icon.png"));
2155            candidates.push(parent.join("icon.png"));
2156        }
2157    }
2158
2159    for path in candidates {
2160        if !path.exists() {
2161            log::debug!("[Native] Icon candidate not found: {:?}", path);
2162            continue;
2163        }
2164
2165        match image::open(&path) {
2166            Ok(img) => {
2167                let rgba = img.to_rgba8();
2168                let (width, height) = rgba.dimensions();
2169                match winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
2170                    Ok(icon) => {
2171                        log::info!("[Native] Successfully loaded app icon from: {:?}", path);
2172                        return Some(icon);
2173                    }
2174                    Err(e) => {
2175                        log::warn!("[Native] Icon format error at {:?}: {}", path, e);
2176                    }
2177                }
2178            }
2179            Err(e) => {
2180                log::warn!("[Native] Failed to open icon image at {:?}: {}", path, e);
2181            }
2182        }
2183    }
2184
2185    log::warn!(
2186        "[Native] Failed to find icon.png in any search path (CWD: {:?})",
2187        base
2188    );
2189    None
2190}
2191
2192// =============================================================================
2193// AUDIO / HAPTIC ENGINES — Cross-platform micro-feedback
2194// =============================================================================
2195
2196/// Cross-platform audio engine using rodio for spatialized sound cues.
2197/// Uses rodio 0.21 API: OutputStreamBuilder::open_default_stream() returns
2198/// OutputStream directly. Playback via Sink::try_new(&stream.mixer()) + append.
2199pub struct RodioAudioEngine {
2200    _stream: rodio::OutputStream,
2201}
2202
2203// OutputStream is not Send+Sync on macOS due to CoreAudio, but we only use it
2204// from the main thread. The AudioEngine trait requires Send+Sync for use in
2205// App struct fields, which is safe here because we never move it across threads.
2206unsafe impl Send for RodioAudioEngine {}
2207unsafe impl Sync for RodioAudioEngine {}
2208
2209impl RodioAudioEngine {
2210    /// Create a new audio engine. Falls back to None if audio init fails.
2211    pub fn new() -> Option<Self> {
2212        match rodio::OutputStreamBuilder::open_default_stream() {
2213            Ok(stream) => {
2214                log::info!("[Native] Audio engine initialized (rodio)");
2215                Some(Self { _stream: stream })
2216            }
2217            Err(e) => {
2218                log::warn!("[Native] Audio init failed (no sound): {}", e);
2219                None
2220            }
2221        }
2222    }
2223}
2224
2225impl cvkg_core::AudioEngine for RodioAudioEngine {
2226    fn play_sound(&self, name: &str, volume: f32) {
2227        let data: &[u8] = match name {
2228            "nav_tick" => cvkg_core::sounds::NAVIGATION_TICK,
2229            "success_chime" => cvkg_core::sounds::SUCCESS_CHIME,
2230            "warning_tone" => cvkg_core::sounds::WARNING_TONE,
2231            _ => {
2232                log::warn!("[Native] Unknown sound: {}", name);
2233                return;
2234            }
2235        };
2236        self.play_buffer(data, volume);
2237    }
2238
2239    fn play_buffer(&self, data: &[u8], _volume: f32) {
2240        use std::io::Cursor;
2241        let cursor = Cursor::new(data.to_vec());
2242        let mixer = self._stream.mixer();
2243        match rodio::play(mixer, cursor) {
2244            Ok(_sink) => {}
2245            Err(e) => log::warn!("[Native] Audio play failed: {}", e),
2246        }
2247    }
2248
2249    fn play_spatial(&self, name: &str, _position: [f32; 3], volume: f32) {
2250        // Spatial audio: play sound without positional attenuation (OS-agnostic fallback)
2251        self.play_sound(name, volume);
2252    }
2253}
2254
2255/// Visual haptic engine that translates haptic requests into visual micro-animations.
2256/// Used as a cross-platform fallback where native haptics are unavailable.
2257pub struct VisualHapticEngine {
2258    last_impact: std::sync::Mutex<std::time::Instant>,
2259}
2260
2261impl Default for VisualHapticEngine {
2262    fn default() -> Self {
2263        Self::new()
2264    }
2265}
2266
2267impl VisualHapticEngine {
2268    pub fn new() -> Self {
2269        Self {
2270            last_impact: std::sync::Mutex::new(std::time::Instant::now()),
2271        }
2272    }
2273}
2274
2275impl cvkg_core::HapticEngine for VisualHapticEngine {
2276    fn impact(&self, intensity: cvkg_core::HapticIntensity) {
2277        let _ = intensity;
2278        *self.last_impact.lock().unwrap() = std::time::Instant::now();
2279    }
2280    fn selection(&self) {
2281        self.impact(cvkg_core::HapticIntensity::Light);
2282    }
2283    fn success(&self) {
2284        self.impact(cvkg_core::HapticIntensity::Medium);
2285    }
2286    fn warning(&self) {
2287        self.impact(cvkg_core::HapticIntensity::Medium);
2288    }
2289    fn error(&self) {
2290        self.impact(cvkg_core::HapticIntensity::Heavy);
2291    }
2292    fn visual_tick(&self, _intensity: f32) {
2293        *self.last_impact.lock().unwrap() = std::time::Instant::now();
2294    }
2295}