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    /// Initial accessibility tree requested by screen reader.
356    AccessibilityInitialTreeRequested(winit::window::WindowId),
357}
358
359impl From<accesskit_winit::Event> for AppEvent {
360    fn from(event: accesskit_winit::Event) -> Self {
361        match event.window_event {
362            accesskit_winit::WindowEvent::ActionRequested(req) => {
363                AppEvent::AccessibilityAction(req)
364            }
365            accesskit_winit::WindowEvent::InitialTreeRequested => {
366                AppEvent::AccessibilityInitialTreeRequested(event.window_id)
367            }
368            _ => AppEvent::AccessibilityAction(accesskit::ActionRequest {
369                action: accesskit::Action::Focus,
370                target_node: accesskit::NodeId(0),
371                target_tree: accesskit::TreeId::ROOT,
372                data: None,
373            }),
374        }
375    }
376}
377
378impl NativeRenderer {
379    /// Create a new NativeRenderer (internal use by App)
380    fn new(
381        window: Arc<Window>,
382        gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
383        delta_time: f32,
384        elapsed_time: f32,
385        berserker_mode: cvkg_core::BerserkerMode,
386        rage: f32,
387    ) -> Self {
388        Self {
389            gpu,
390            delta_time,
391            elapsed_time,
392            berserker_mode,
393            rage,
394            window,
395        }
396    }
397
398    /// Start the CVKG native application with the given view.
399    /// This is the main entry point for desktop applications.
400    pub fn run<V: cvkg_core::View + 'static>(view: V) {
401        let event_loop = EventLoop::<AppEvent>::with_user_event()
402            .build()
403            .expect("Failed to create event loop");
404        event_loop.set_control_flow(ControlFlow::Wait);
405
406        let mut app = App {
407            view,
408            window_manager: WindowManager::new(),
409            gpu: None,
410            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
411            proxy: event_loop.create_proxy(),
412            start_time: std::time::Instant::now(),
413            last_frame_time: std::time::Instant::now(),
414            berserker_mode: cvkg_core::BerserkerMode::Normal,
415            rage: 0.0,
416            state_detector: WindowStateDetector::new(),
417            modifiers: winit::keyboard::ModifiersState::default(),
418            audio_engine: None,
419            haptic_engine: Arc::new(VisualHapticEngine::new()),
420        };
421
422        event_loop.run_app(&mut app).expect("Event loop error");
423    }
424}
425
426/// Native implementation of the cvkg_core::Window trait.
427/// Communicates state updates back to the winit event loop thread using an EventLoopProxy.
428struct NativeWindowWrapper {
429    winit_id: winit::window::WindowId,
430    window: Arc<winit::window::Window>,
431    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
432    is_key: Arc<std::sync::atomic::AtomicBool>,
433    is_main: bool,
434}
435
436impl cvkg_core::Window for NativeWindowWrapper {
437    /// Request that this window be closed.
438    fn close(&self) {
439        let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
440    }
441
442    /// Change the title bar text of this window.
443    fn set_title(&self, title: &str) {
444        let _ = self
445            .proxy
446            .send_event(AppEvent::SetTitle(self.winit_id, title.to_string()));
447    }
448
449    /// Request updating this window's dimensions.
450    fn set_size(&self, width: f32, height: f32) {
451        let _ = self
452            .proxy
453            .send_event(AppEvent::SetSize(self.winit_id, width, height));
454    }
455
456    /// Return true if this window has key focus.
457    fn is_key(&self) -> bool {
458        self.is_key.load(std::sync::atomic::Ordering::SeqCst)
459    }
460
461    /// Return true if this is the primary application window.
462    fn is_main(&self) -> bool {
463        self.is_main
464    }
465
466    /// Return true if this window is visible.
467    fn is_visible(&self) -> bool {
468        self.window.is_visible().unwrap_or(false)
469    }
470
471    /// Show or hide this window.
472    fn set_visible(&self, visible: bool) {
473        let _ = self
474            .proxy
475            .send_event(AppEvent::SetVisible(self.winit_id, visible));
476    }
477
478    /// Focus and bring this window to the foreground.
479    fn bring_to_front(&self) {
480        let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
481    }
482}
483
484/// Dynamic manager for all active native windows and their rendering contexts.
485pub struct WindowManager {
486    /// Mapping from native winit WindowId to internal WindowData.
487    pub windows: std::collections::HashMap<winit::window::WindowId, WindowData>,
488    /// Stack of windows ordered from back to front (end of vector is top-most).
489    pub window_stack: Vec<winit::window::WindowId>,
490    /// Mapping of winit window IDs to core IDs.
491    pub winit_to_core: std::collections::HashMap<winit::window::WindowId, cvkg_core::WindowId>,
492    /// Mapping of core window IDs to winit IDs.
493    pub core_to_winit: std::collections::HashMap<cvkg_core::WindowId, winit::window::WindowId>,
494    /// Monotonic counter to allocate unique core window IDs.
495    pub next_core_id: u64,
496}
497
498impl Default for WindowManager {
499    fn default() -> Self {
500        Self::new()
501    }
502}
503
504impl WindowManager {
505    /// Create an empty WindowManager.
506    pub fn new() -> Self {
507        Self {
508            windows: std::collections::HashMap::new(),
509            window_stack: Vec::new(),
510            winit_to_core: std::collections::HashMap::new(),
511            core_to_winit: std::collections::HashMap::new(),
512            next_core_id: 1,
513        }
514    }
515
516    /// Create and register a new native window.
517    pub fn create_window(
518        &mut self,
519        event_loop: &ActiveEventLoop,
520        gpu: &Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
521        proxy: winit::event_loop::EventLoopProxy<AppEvent>,
522        config: cvkg_core::WindowConfig,
523        is_main: bool,
524        view: &impl cvkg_core::View,
525    ) -> cvkg_core::WindowHandle {
526        let mut window_attrs = Window::default_attributes()
527            .with_title(&config.title)
528            .with_visible(true)
529            .with_transparent(config.transparent)
530            .with_decorations(config.decorations)
531            .with_inner_size(winit::dpi::LogicalSize::new(config.size.0, config.size.1));
532
533        if let Some(min) = config.min_size {
534            window_attrs =
535                window_attrs.with_min_inner_size(winit::dpi::LogicalSize::new(min.0, min.1));
536        }
537        if let Some(max) = config.max_size {
538            window_attrs =
539                window_attrs.with_max_inner_size(winit::dpi::LogicalSize::new(max.0, max.1));
540        }
541
542        let winit_level = match config.level {
543            cvkg_core::WindowLevel::Normal => winit::window::WindowLevel::Normal,
544            cvkg_core::WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
545            cvkg_core::WindowLevel::PopUpMenu => winit::window::WindowLevel::AlwaysOnTop,
546        };
547        window_attrs = window_attrs.with_window_level(winit_level);
548
549        #[cfg(target_os = "macos")]
550        {
551            use winit::platform::macos::WindowAttributesExtMacOS;
552            window_attrs = window_attrs
553                .with_titlebar_transparent(true)
554                .with_title_hidden(true)
555                .with_fullsize_content_view(true)
556                .with_has_shadow(true);
557        }
558
559        let window = Arc::new(
560            event_loop
561                .create_window(window_attrs)
562                .expect("Failed to create window"),
563        );
564
565        let winit_id = window.id();
566        let core_id = cvkg_core::WindowId(self.next_core_id);
567        self.next_core_id += 1;
568
569        let is_key_focused = Arc::new(std::sync::atomic::AtomicBool::new(true));
570
571        let wrapper = Arc::new(NativeWindowWrapper {
572            winit_id,
573            window: window.clone(),
574            proxy: proxy.clone(),
575            is_key: is_key_focused.clone(),
576            is_main,
577        });
578
579        let handle = cvkg_core::WindowHandle::new(core_id, wrapper);
580
581        let vdom = cvkg_vdom::VDom::build(
582            view,
583            cvkg_core::Rect::new(0.0, 0.0, config.size.0, config.size.1),
584        );
585
586        let accesskit_adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
587            event_loop,
588            &window,
589            proxy.clone(),
590        ));
591
592        let data = WindowData {
593            window: window.clone(),
594            accesskit_adapter,
595            vdom: Some(vdom),
596            cursor_pos: [0.0, 0.0],
597            cursor_velocity: [0.0, 0.0],
598            last_redraw_start: std::time::Instant::now(),
599            frame_history: std::collections::VecDeque::with_capacity(60),
600            frame_count: 0,
601            last_pos: None,
602            needs_cursor_update: false,
603            is_dragging: false,
604            drag_start_pos: [0.0, 0.0],
605            drag_button: 0,
606            drag_threshold: 5.0,
607            is_key_focused,
608            is_main,
609            core_id,
610            window_handle: handle.clone(),
611        };
612
613        self.windows.insert(winit_id, data);
614        self.window_stack.push(winit_id);
615        self.winit_to_core.insert(winit_id, core_id);
616        self.core_to_winit.insert(core_id, winit_id);
617
618        if let Some(gpu_mutex) = gpu {
619            gpu_mutex.lock().unwrap().register_window(window.clone());
620        }
621
622        handle
623    }
624
625    /// Close and unregister a native window.
626    pub fn close_window(&mut self, winit_id: winit::window::WindowId) {
627        self.windows.remove(&winit_id);
628        self.window_stack.retain(|id| *id != winit_id);
629        if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
630            self.core_to_winit.remove(&core_id);
631        }
632    }
633
634    /// Bring a native window to the foreground and focus it.
635    pub fn bring_to_front(&mut self, winit_id: winit::window::WindowId) {
636        self.window_stack.retain(|id| *id != winit_id);
637        self.window_stack.push(winit_id);
638        if let Some(data) = self.windows.get(&winit_id) {
639            data.window.focus_window();
640        }
641    }
642
643    /// Get a reference to a window's data.
644    pub fn window(&self, winit_id: winit::window::WindowId) -> Option<&WindowData> {
645        self.windows.get(&winit_id)
646    }
647
648    /// Get a mutable reference to a window's data.
649    pub fn window_mut(&mut self, winit_id: winit::window::WindowId) -> Option<&mut WindowData> {
650        self.windows.get_mut(&winit_id)
651    }
652
653    /// Return the list of window IDs in current Z-order stack.
654    pub fn window_order(&self) -> &[winit::window::WindowId] {
655        &self.window_stack
656    }
657}
658
659pub struct WindowData {
660    window: Arc<Window>,
661    accesskit_adapter: Option<accesskit_winit::Adapter>,
662    vdom: Option<cvkg_vdom::VDom>,
663    cursor_pos: [f32; 2],
664    cursor_velocity: [f32; 2],
665    /// The instant the last redraw finished, used for measuring inter-frame gap timing.
666    last_redraw_start: std::time::Instant,
667    /// Sliding window of frame times for tail latency (P99) calculation.
668    frame_history: std::collections::VecDeque<f32>,
669    /// Total frames rendered on this window.
670    frame_count: u64,
671    /// Last window position for shake detection.
672    last_pos: Option<[i32; 2]>,
673    /// Set when mouse moves; cleared when redraw processes. Prevents redundant
674    /// VDOM rebuilds when cursor moves faster than the display refresh rate.
675    needs_cursor_update: bool,
676    // ── Drag tracking ──────────────────────────────────────────────────────
677    /// Whether a drag is currently in progress.
678    is_dragging: bool,
679    /// The position where the drag started.
680    drag_start_pos: [f32; 2],
681    /// The button that initiated the drag.
682    drag_button: u32,
683    /// Drag threshold in logical pixels (pointer must move this far to start drag).
684    drag_threshold: f32,
685
686    // ── Multi-window tracking ──────────────────────────────────────────────
687    is_key_focused: Arc<std::sync::atomic::AtomicBool>,
688    is_main: bool,
689    core_id: cvkg_core::WindowId,
690    window_handle: cvkg_core::WindowHandle,
691}
692
693struct App<V: cvkg_core::View> {
694    view: V,
695    window_manager: WindowManager,
696    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
697    #[allow(dead_code)]
698    asset_manager: std::sync::Arc<NativeAssetManager>,
699    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
700    start_time: std::time::Instant,
701    last_frame_time: std::time::Instant,
702    berserker_mode: cvkg_core::BerserkerMode,
703    rage: f32,
704    /// Tracks the current window state for render-loop decisions.
705    state_detector: WindowStateDetector,
706    /// Tracks active modifier key states (Ctrl, Shift, Command, etc.).
707    modifiers: winit::keyboard::ModifiersState,
708    /// Cross-platform audio engine for spatialized sound cues.
709    audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
710    /// Visual haptic engine for micro-feedback animations.
711    haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
712}
713
714impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
715    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
716        if self.gpu.is_none() {
717            // Detect and apply system accessibility preferences at startup
718            let a11y_prefs = cvkg_core::AccessibilityPreferences::detect_from_system();
719            cvkg_core::set_accessibility_preferences(a11y_prefs);
720            if a11y_prefs.reduce_motion
721                || a11y_prefs.reduce_transparency
722                || a11y_prefs.increase_contrast
723            {
724                log::info!(
725                    "[Native] Accessibility prefs: motion={} transparency={} contrast={}",
726                    a11y_prefs.reduce_motion,
727                    a11y_prefs.reduce_transparency,
728                    a11y_prefs.increase_contrast
729                );
730            }
731
732            // Detect and apply system theme (dark/light)
733            let system_theme = cvkg_core::detect_system_theme();
734            log::info!("[Native] System theme detected: {:?}", system_theme);
735
736            // Initialize cross-platform audio engine
737            self.audio_engine =
738                RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
739
740            // Initialize visual haptic engine for micro-feedback
741            self.haptic_engine = Arc::new(VisualHapticEngine::new());
742
743            log::info!("[Native] App instance (resumed): {:p}", self);
744
745            let config = cvkg_core::WindowConfig {
746                title: "CVKG Berserker".to_string(),
747                size: (1280.0, 720.0),
748                min_size: None,
749                max_size: None,
750                resizable: true,
751                transparent: true,
752                decorations: true,
753                level: cvkg_core::WindowLevel::Normal,
754            };
755
756            let handle = self.window_manager.create_window(
757                event_loop,
758                &self.gpu,
759                self.proxy.clone(),
760                config,
761                true, // is_main
762                &self.view,
763            );
764
765            let winit_id = self
766                .window_manager
767                .core_to_winit
768                .get(&handle.id)
769                .copied()
770                .expect("Failed to get winit_id");
771            let window = self
772                .window_manager
773                .windows
774                .get(&winit_id)
775                .unwrap()
776                .window
777                .clone();
778
779            // Immediately set self.gpu to prevent re-entry
780            let gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
781            self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
782
783            log::info!("[Native] Initialization complete.");
784            window.request_redraw();
785        }
786    }
787
788    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
789        if matches!(cause, winit::event::StartCause::Poll) {
790            // Too noisy
791        } else {
792            // Lowered to trace to prevent logs flooding under standard debug levels
793            log::trace!("[Native] Event Loop Wake: {:?}", cause);
794        }
795    }
796
797    fn device_event(
798        &mut self,
799        _event_loop: &ActiveEventLoop,
800        _device_id: winit::event::DeviceId,
801        event: winit::event::DeviceEvent,
802    ) {
803        if matches!(event, winit::event::DeviceEvent::MouseMotion { .. }) {
804            // log::trace!("[Native] Raw Mouse Motion");
805        } else {
806            // Log device raw events at trace level to prevent I/O blocking performance issues
807            // under high mouse-polling rates on systems with direct input mapping.
808            log::trace!("[Native] DEVICE EVENT: {:?}", event);
809        }
810    }
811
812    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
813        if !matches!(event, WindowEvent::RedrawRequested)
814            && !matches!(event, WindowEvent::CursorMoved { .. })
815        {
816            log::info!(
817                "[Native] App instance: {:p} | WINDOW EVENT: {:?}",
818                self,
819                event
820            );
821        }
822
823        let gpu_arc = if let Some(g) = &self.gpu {
824            g.clone()
825        } else {
826            log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
827            return;
828        };
829
830        let mut close_window = false;
831        let mut bring_to_front = false;
832        let mut create_new_window = false;
833        // Cmd+Q was pressed — close all windows after the state block ends.
834        let mut quit_all = false;
835
836        {
837            let state = if let Some(s) = self.window_manager.windows.get_mut(&id) {
838                s
839            } else {
840                return;
841            };
842
843            match event {
844                WindowEvent::Moved(pos) => {
845                    let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
846                    let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
847                    let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
848
849                    if speed > 0.1 {
850                        // Significant kinetic injection
851                        self.rage = (self.rage + 0.2).min(1.0);
852                        log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
853                    }
854
855                    state.last_pos = Some([pos.x, pos.y]);
856                    state.window.request_redraw();
857                }
858                WindowEvent::DroppedFile(path) => {
859                    if let Some(vdom) = &state.vdom {
860                        vdom.dispatch_event(cvkg_core::Event::FileDrop {
861                            x: state.cursor_pos[0],
862                            y: state.cursor_pos[1],
863                            path: path.to_string_lossy().into_owned(),
864                        });
865                    }
866                }
867                WindowEvent::CloseRequested => {
868                    let close_action = cvkg_core::WindowCloseAction::Allow;
869                    match close_action {
870                        cvkg_core::WindowCloseAction::Allow
871                        | cvkg_core::WindowCloseAction::Confirm => {
872                            close_window = true;
873                        }
874                        cvkg_core::WindowCloseAction::Deny => {
875                            log::info!("[Native] Close request denied for window {:?}", id);
876                        }
877                    }
878                }
879                WindowEvent::Resized(physical_size) => {
880                    gpu_arc
881                        .lock()
882                        .expect("GPU mutex poisoned during resize")
883                        .resize(
884                            id,
885                            physical_size.width,
886                            physical_size.height,
887                            state.window.scale_factor() as f32,
888                        );
889                    state.window.request_redraw();
890                }
891                WindowEvent::Focused(focused) => {
892                    log::info!("[Native] Window focus changed: {}", focused);
893                    state
894                        .is_key_focused
895                        .store(focused, std::sync::atomic::Ordering::SeqCst);
896                    if focused {
897                        bring_to_front = true;
898                    }
899                }
900                WindowEvent::RedrawRequested => {
901                    if state.frame_count % 60 == 0 {
902                        log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
903                    }
904                    let size = state.window.inner_size();
905                    let scale = state.window.scale_factor();
906                    let logical_size = size.to_logical::<f32>(scale);
907
908                    let rect = cvkg_core::Rect {
909                        x: 0.0,
910                        y: 0.0,
911                        width: logical_size.width,
912                        height: logical_size.height,
913                    };
914
915                    // Record the start of this redraw and snapshot the previous frame's
916                    // start time before overwriting it, so inter-frame gap is measurable.
917                    let redraw_start = std::time::Instant::now();
918                    let last_redraw_start = state.last_redraw_start;
919                    // Update last_redraw_start immediately so the next frame measures correctly
920                    // even if this frame returns early.
921                    state.last_redraw_start = redraw_start;
922
923                    // Build new vdom and diff (layout pass)
924                    let layout_start = std::time::Instant::now();
925                    let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
926
927                    // Dispatch cursor events if the mouse moved since last frame
928                    if state.needs_cursor_update {
929                        if let Some(vdom) = &state.vdom {
930                            vdom.dispatch_event(cvkg_core::Event::PointerMove {
931                                x: state.cursor_pos[0],
932                                y: state.cursor_pos[1],
933                                proximity_field: 0.0,
934                                tilt: None,
935                                azimuth: None,
936                                pressure: Some(1.0),
937                                barrel_rotation: None,
938                                pointer_precision: 0.0,
939                            });
940                        }
941                        state.needs_cursor_update = false;
942                    }
943                    let layout_end = std::time::Instant::now();
944
945                    // Apply patches to the accessibility tree and the previous VDOM
946                    let state_flush_start = std::time::Instant::now();
947                    if let Some(prev_vdom) = &mut state.vdom {
948                        let patches = prev_vdom.diff(&new_vdom);
949                        let mut nodes = Vec::new();
950                        for patch in &patches {
951                            if let cvkg_vdom::VDomPatch::Create(node)
952                            | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
953                            {
954                                nodes
955                                    .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
956                            } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
957                                && let Some(node) = new_vdom.nodes.get(id)
958                            {
959                                nodes
960                                    .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
961                            }
962                        }
963                        if !nodes.is_empty() {
964                            if let Some(adapter) = &mut state.accesskit_adapter {
965                                adapter.update_if_active(|| accesskit::TreeUpdate {
966                                    nodes,
967                                    tree: None,
968                                    focus: accesskit::NodeId(1),
969                                    tree_id: accesskit::TreeId::ROOT,
970                                });
971                            }
972                        }
973                        prev_vdom.apply_patches(patches);
974                    } else {
975                        state.vdom = Some(new_vdom);
976                    }
977                    let state_flush_end = std::time::Instant::now();
978
979                    // GPU rendering
980                    let draw_start = std::time::Instant::now();
981                    let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
982                    let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
983                    let mut gpu = gpu_arc
984                        .lock()
985                        .expect("GPU mutex poisoned during frame begin");
986                    gpu.update_mouse(state.cursor_pos, state.cursor_velocity);
987                    let encoder = gpu.begin_frame(id);
988                    let mut renderer = NativeRenderer::new(
989                        state.window.clone(),
990                        gpu_arc.clone(),
991                        delta_time,
992                        elapsed_time,
993                        self.berserker_mode,
994                        self.rage,
995                    );
996                    // Release the gpu lock before calling render — the render methods each
997                    // re-acquire it per-call, allowing the view tree to interleave with other
998                    // work without holding one giant critical section across the whole draw.
999                    drop(gpu);
1000                    self.view.render(&mut renderer, rect);
1001                    let draw_end = std::time::Instant::now();
1002
1003                    // Re-acquire to submit the frame
1004                    let gpu_submit_start = std::time::Instant::now();
1005                    let mut gpu = gpu_arc
1006                        .lock()
1007                        .expect("GPU mutex poisoned during frame submit");
1008                    gpu.render_frame();
1009                    gpu.end_frame(encoder);
1010                    let gpu_submit_end = std::time::Instant::now();
1011
1012                    // Build telemetry from this frame's timing measurements.
1013                    // NOTE: input_time_ms measures the inter-frame gap (time from end of last frame
1014                    // to start of this one), not input dispatch latency. The field name is defined
1015                    // in cvkg_core::TelemetryData and kept as-is to match that struct.
1016                    let mut telemetry = cvkg_core::TelemetryData::default();
1017                    telemetry.input_time_ms =
1018                        redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
1019                    telemetry.layout_time_ms =
1020                        layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
1021                    telemetry.state_flush_time_ms = state_flush_end
1022                        .duration_since(state_flush_start)
1023                        .as_secs_f32()
1024                        * 1000.0;
1025                    telemetry.draw_time_ms =
1026                        draw_end.duration_since(draw_start).as_secs_f32() * 1000.0;
1027                    telemetry.gpu_submit_time_ms = gpu_submit_end
1028                        .duration_since(gpu_submit_start)
1029                        .as_secs_f32()
1030                        * 1000.0;
1031
1032                    // Total frame time from redraw request to GPU submission complete
1033                    let frame_time_ms =
1034                        gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
1035                    telemetry.frame_time_ms = frame_time_ms;
1036
1037                    // Log detailed frame time breakdown for performance diagnostics
1038                    log::info!(
1039                        "[Native] Frame timings: layout={:.2}ms state={:.2}ms draw={:.2}ms submit={:.2}ms total={:.2}ms",
1040                        telemetry.layout_time_ms,
1041                        telemetry.state_flush_time_ms,
1042                        telemetry.draw_time_ms,
1043                        telemetry.gpu_submit_time_ms,
1044                        telemetry.frame_time_ms
1045                    );
1046
1047                    // Tail Latency Tracking (P99 and Jitter) over a 100-frame sliding window.
1048                    state.frame_history.push_back(frame_time_ms);
1049                    if state.frame_history.len() > 100 {
1050                        state.frame_history.pop_front();
1051                    }
1052
1053                    let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
1054                    sorted_frames
1055                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1056
1057                    if !sorted_frames.is_empty() {
1058                        let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
1059                        telemetry.p99_frame_time_ms =
1060                            sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
1061
1062                        // Jitter: standard deviation of frame times over the sliding window.
1063                        let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
1064                        let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
1065                            / sorted_frames.len() as f32;
1066                        telemetry.frame_jitter_ms = variance.sqrt();
1067                    }
1068
1069                    // FIX #8: hardware_stall_detected is now reset each frame based on current
1070                    // jitter rather than being set once and never cleared. A single jittery frame
1071                    // no longer permanently flags the session. Jitter > 20ms is a heuristic for
1072                    // scheduling disruption (GC, OS preemption, slow layout) — not a confirmed
1073                    // hardware stall, but the field name is defined in cvkg_core::TelemetryData.
1074                    telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
1075
1076                    state.frame_count += 1;
1077
1078                    telemetry.berserker_rage = self.rage;
1079                    gpu.telemetry = telemetry;
1080                }
1081                WindowEvent::CursorEntered { .. } => {
1082                    log::info!("[Native] Cursor ENTERED window");
1083                    if let Some(vdom) = &state.vdom {
1084                        vdom.dispatch_event(cvkg_core::Event::PointerEnter);
1085                    }
1086                    state.window.request_redraw();
1087                }
1088                WindowEvent::CursorLeft { .. } => {
1089                    log::info!("[Native] Cursor LEFT window");
1090                    if let Some(vdom) = &state.vdom {
1091                        vdom.dispatch_event(cvkg_core::Event::PointerLeave);
1092                    }
1093                    state.window.request_redraw();
1094                }
1095                WindowEvent::CursorMoved { position, .. } => {
1096                    let scale = state.window.scale_factor();
1097                    let logical = position.to_logical::<f32>(scale);
1098                    let elapsed = state.last_redraw_start.elapsed().as_secs_f32().max(0.001);
1099                    let dx = logical.x - state.cursor_pos[0];
1100                    let dy = logical.y - state.cursor_pos[1];
1101                    state.cursor_velocity = [dx / elapsed, dy / elapsed];
1102                    state.cursor_pos = [logical.x, logical.y];
1103                    state.needs_cursor_update = true;
1104                    // Don't request_redraw here — the redraw will process the cursor update.
1105                    // Only request a redraw if we're not already in a redraw cycle.
1106                    if state.frame_count == 0 {
1107                        state.window.request_redraw();
1108                    }
1109                }
1110                WindowEvent::MouseInput {
1111                    state: mouse_state,
1112                    button,
1113                    ..
1114                } => {
1115                    log::info!(
1116                        "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
1117                        mouse_state,
1118                        button,
1119                        state.cursor_pos
1120                    );
1121                    if let Some(vdom) = &state.vdom {
1122                        let btn_id = match button {
1123                            winit::event::MouseButton::Left => 0,
1124                            winit::event::MouseButton::Right => 2,
1125                            winit::event::MouseButton::Middle => 1,
1126                            winit::event::MouseButton::Back => 3,
1127                            winit::event::MouseButton::Forward => 4,
1128                            winit::event::MouseButton::Other(id) => id as u32,
1129                        };
1130
1131                        match mouse_state {
1132                            winit::event::ElementState::Pressed => {
1133                                log::info!("[Native] Dispatching PointerDown to VDOM");
1134                                vdom.dispatch_event(cvkg_core::Event::PointerDown {
1135                                    x: state.cursor_pos[0],
1136                                    y: state.cursor_pos[1],
1137                                    button: btn_id,
1138                                    proximity_field: 0.0,
1139                                    tilt: None,
1140                                    azimuth: None,
1141                                    pressure: Some(1.0),
1142                                    barrel_rotation: None,
1143                                    pointer_precision: 0.0,
1144                                });
1145                            }
1146                            winit::event::ElementState::Released => {
1147                                log::info!("[Native] Dispatching PointerUp to VDOM");
1148                                vdom.dispatch_event(cvkg_core::Event::PointerUp {
1149                                    x: state.cursor_pos[0],
1150                                    y: state.cursor_pos[1],
1151                                    button: btn_id,
1152                                    tilt: None,
1153                                    azimuth: None,
1154                                    pressure: Some(0.0),
1155                                    barrel_rotation: None,
1156                                    pointer_precision: 0.0,
1157                                });
1158                            }
1159                        }
1160                        state.window.request_redraw();
1161                    } else {
1162                        log::warn!("[Native] Mouse input received but state.vdom is None!");
1163                    }
1164                }
1165                WindowEvent::MouseWheel { delta, .. } => {
1166                    if let Some(vdom) = &state.vdom {
1167                        let (dx, dy) = match delta {
1168                            winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
1169                            winit::event::MouseScrollDelta::PixelDelta(pos) => {
1170                                (pos.x as f32, pos.y as f32)
1171                            }
1172                        };
1173                        vdom.dispatch_event(cvkg_core::Event::PointerWheel {
1174                            x: state.cursor_pos[0],
1175                            y: state.cursor_pos[1],
1176                            delta_x: dx,
1177                            delta_y: dy,
1178                            pointer_precision: 0.0,
1179                        });
1180                        state.window.request_redraw();
1181                    }
1182                }
1183                // ── Touch screen inputs ──────────────────────────────────────────
1184                // Map native winit touchscreen events to VDOM Pointer events using
1185                // low-precision fat-finger bounding expansion (150px proximity field).
1186                WindowEvent::Touch(touch) => {
1187                    if let Some(vdom) = &state.vdom {
1188                        let scale = state.window.scale_factor();
1189                        let logical = touch.location.to_logical::<f32>(scale);
1190                        let x = logical.x;
1191                        let y = logical.y;
1192                        let touch_btn = 0; // Touch maps to primary/left button
1193                        match touch.phase {
1194                            winit::event::TouchPhase::Started => {
1195                                log::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
1196                                vdom.dispatch_event(cvkg_core::Event::PointerDown {
1197                                    x,
1198                                    y,
1199                                    button: touch_btn,
1200                                    proximity_field: 0.0,
1201                                    tilt: None,
1202                                    azimuth: None,
1203                                    pressure: Some(
1204                                        touch.force.map(|f| f.normalized() as f32).unwrap_or(1.0),
1205                                    ),
1206                                    barrel_rotation: None,
1207                                    pointer_precision: 150.0,
1208                                });
1209                            }
1210                            winit::event::TouchPhase::Moved => {
1211                                vdom.dispatch_event(cvkg_core::Event::PointerMove {
1212                                    x,
1213                                    y,
1214                                    proximity_field: 0.0,
1215                                    tilt: None,
1216                                    azimuth: None,
1217                                    pressure: Some(
1218                                        touch.force.map(|f| f.normalized() as f32).unwrap_or(1.0),
1219                                    ),
1220                                    barrel_rotation: None,
1221                                    pointer_precision: 150.0,
1222                                });
1223                            }
1224                            winit::event::TouchPhase::Ended => {
1225                                vdom.dispatch_event(cvkg_core::Event::PointerUp {
1226                                    x,
1227                                    y,
1228                                    button: touch_btn,
1229                                    tilt: None,
1230                                    azimuth: None,
1231                                    pressure: Some(0.0),
1232                                    barrel_rotation: None,
1233                                    pointer_precision: 150.0,
1234                                });
1235                                // Dispatch PointerClick immediately following TouchEnd on the same location
1236                                vdom.dispatch_event(cvkg_core::Event::PointerClick {
1237                                    x,
1238                                    y,
1239                                    button: touch_btn,
1240                                    tilt: None,
1241                                    azimuth: None,
1242                                    pressure: Some(0.0),
1243                                    barrel_rotation: None,
1244                                    pointer_precision: 150.0,
1245                                });
1246                            }
1247                            winit::event::TouchPhase::Cancelled => {
1248                                vdom.dispatch_event(cvkg_core::Event::PointerUp {
1249                                    x,
1250                                    y,
1251                                    button: touch_btn,
1252                                    tilt: None,
1253                                    azimuth: None,
1254                                    pressure: Some(0.0),
1255                                    barrel_rotation: None,
1256                                    pointer_precision: 150.0,
1257                                });
1258                            }
1259                        }
1260                        state.window.request_redraw();
1261                    }
1262                }
1263                // ── Trackpad gestures (pinch-to-zoom, swipe) ──────────────────────
1264                // OS-agnostic: winit provides these on macOS trackpad, Windows precision
1265                // touchpads, and Linux (where supported). Falls back gracefully.
1266                WindowEvent::PinchGesture { delta, .. } => {
1267                    if let Some(vdom) = &state.vdom {
1268                        let scale = 1.0 + delta as f32;
1269                        let velocity = delta as f32;
1270                        vdom.dispatch_event(cvkg_core::Event::GesturePinch {
1271                            center: state.cursor_pos,
1272                            scale,
1273                            velocity,
1274                            phase: cvkg_core::TouchPhase::Moved,
1275                        });
1276                    }
1277                    // Provide micro-feedback on pinch
1278                    if let Some(audio) = &self.audio_engine {
1279                        audio.play_sound("nav_tick", 0.3);
1280                    }
1281                    self.haptic_engine
1282                        .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
1283                    state.window.request_redraw();
1284                }
1285                WindowEvent::RotationGesture { delta, .. } => {
1286                    if let Some(vdom) = &state.vdom {
1287                        let angle = delta;
1288                        vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
1289                            direction: [angle.cos(), angle.sin()],
1290                            velocity: delta.abs(),
1291                            phase: cvkg_core::TouchPhase::Moved,
1292                        });
1293                    }
1294                    state.window.request_redraw();
1295                }
1296                WindowEvent::KeyboardInput { event, .. } => {
1297                    if event.state == winit::event::ElementState::Pressed {
1298                        if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
1299                            // Cross-platform "command" key: ⌘ on macOS, Ctrl on all other OSes.
1300                            // This ensures keyboard shortcuts work identically on every platform
1301                            // without separate branches in every handler.
1302                            let is_cmd = if cfg!(target_os = "macos") {
1303                                self.modifiers.super_key()
1304                            } else {
1305                                self.modifiers.control_key()
1306                            };
1307                            let is_shift = self.modifiers.shift_key();
1308
1309                            if is_cmd {
1310                                match code {
1311                                    // ── Undo / Redo ───────────────────────────────
1312                                    winit::keyboard::KeyCode::KeyZ => {
1313                                        if is_shift {
1314                                            log::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
1315                                            let mut redo_action = None;
1316                                            cvkg_core::update_system_state(|s| {
1317                                                let mut s = s.clone();
1318                                                redo_action = s.undo_manager.redo();
1319                                                s
1320                                            });
1321                                            if let Some(action) = redo_action {
1322                                                action();
1323                                            }
1324                                            state.window.request_redraw();
1325                                        } else {
1326                                            log::info!("[Native] Shortcut: Undo (Cmd+Z)");
1327                                            let mut undo_action = None;
1328                                            cvkg_core::update_system_state(|s| {
1329                                                let mut s = s.clone();
1330                                                undo_action = s.undo_manager.undo();
1331                                                s
1332                                            });
1333                                            if let Some(action) = undo_action {
1334                                                action();
1335                                            }
1336                                            state.window.request_redraw();
1337                                        }
1338                                    }
1339                                    // Ctrl+Y as alternative Redo on non-macOS
1340                                    winit::keyboard::KeyCode::KeyY
1341                                        if !cfg!(target_os = "macos") =>
1342                                    {
1343                                        log::info!("[Native] Shortcut: Redo (Ctrl+Y)");
1344                                        let mut redo_action = None;
1345                                        cvkg_core::update_system_state(|s| {
1346                                            let mut s = s.clone();
1347                                            redo_action = s.undo_manager.redo();
1348                                            s
1349                                        });
1350                                        if let Some(action) = redo_action {
1351                                            action();
1352                                        }
1353                                        state.window.request_redraw();
1354                                    }
1355                                    // ── File operations ───────────────────────────
1356                                    winit::keyboard::KeyCode::KeyN => {
1357                                        log::info!("[Native] Shortcut: New Window (Cmd+N)");
1358                                        create_new_window = true;
1359                                    }
1360                                    winit::keyboard::KeyCode::KeyO => {
1361                                        log::info!("[Native] Shortcut: Open File (Cmd+O)");
1362                                        if let Some(vdom) = &state.vdom {
1363                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1364                                                key: "cmd+o".to_string(),
1365                                            });
1366                                        }
1367                                        state.window.request_redraw();
1368                                    }
1369                                    winit::keyboard::KeyCode::KeyS => {
1370                                        log::info!("[Native] Shortcut: Save (Cmd+S)");
1371                                        if let Some(vdom) = &state.vdom {
1372                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1373                                                key: "cmd+s".to_string(),
1374                                            });
1375                                        }
1376                                        state.window.request_redraw();
1377                                    }
1378                                    winit::keyboard::KeyCode::KeyW => {
1379                                        log::info!("[Native] Shortcut: Close Window (Cmd+W)");
1380                                        close_window = true;
1381                                    }
1382                                    winit::keyboard::KeyCode::KeyQ => {
1383                                        log::info!("[Native] Shortcut: Quit (Cmd+Q)");
1384                                        // Defer closing all windows until after the state borrow ends.
1385                                        quit_all = true;
1386                                    }
1387                                    // ── Clipboard ────────────────────────────────
1388                                    winit::keyboard::KeyCode::KeyC => {
1389                                        log::info!("[Native] Shortcut: Copy (Cmd+C)");
1390                                        if let Some(vdom) = &state.vdom {
1391                                            vdom.dispatch_event(cvkg_core::Event::Copy);
1392                                        }
1393                                        state.window.request_redraw();
1394                                    }
1395                                    winit::keyboard::KeyCode::KeyV => {
1396                                        log::info!("[Native] Shortcut: Paste (Cmd+V)");
1397                                        // Read the system clipboard. Fall back to empty string on
1398                                        // error so the Paste event is always delivered to the VDOM.
1399                                        let text = arboard::Clipboard::new()
1400                                            .ok()
1401                                            .and_then(|mut cb| cb.get_text().ok())
1402                                            .unwrap_or_default();
1403                                        if let Some(vdom) = &state.vdom {
1404                                            vdom.dispatch_event(cvkg_core::Event::Paste(text));
1405                                        }
1406                                        state.window.request_redraw();
1407                                    }
1408                                    winit::keyboard::KeyCode::KeyX => {
1409                                        log::info!("[Native] Shortcut: Cut (Cmd+X)");
1410                                        if let Some(vdom) = &state.vdom {
1411                                            vdom.dispatch_event(cvkg_core::Event::Cut);
1412                                        }
1413                                        state.window.request_redraw();
1414                                    }
1415                                    // ── Selection / search ────────────────────────
1416                                    winit::keyboard::KeyCode::KeyA => {
1417                                        log::info!("[Native] Shortcut: Select All (Cmd+A)");
1418                                        if let Some(vdom) = &state.vdom {
1419                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1420                                                key: "cmd+a".to_string(),
1421                                            });
1422                                        }
1423                                        state.window.request_redraw();
1424                                    }
1425                                    winit::keyboard::KeyCode::KeyF => {
1426                                        log::info!("[Native] Shortcut: Find (Cmd+F)");
1427                                        if let Some(vdom) = &state.vdom {
1428                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1429                                                key: "cmd+f".to_string(),
1430                                            });
1431                                        }
1432                                        state.window.request_redraw();
1433                                    }
1434                                    _ => {}
1435                                }
1436                            }
1437                        }
1438                    }
1439
1440                    if let Some(vdom) = &state.vdom
1441                        && let Some(cvkg_event) = convert_keyboard_event(event)
1442                    {
1443                        vdom.dispatch_event(cvkg_event);
1444                        state.window.request_redraw();
1445                    }
1446                }
1447
1448                WindowEvent::Ime(ime_event) => {
1449                    if let Some(vdom) = &state.vdom
1450                        && let Some(cvkg_event) = convert_ime_event(ime_event)
1451                    {
1452                        vdom.dispatch_event(cvkg_event);
1453                        state.window.request_redraw();
1454                    }
1455                }
1456                WindowEvent::ModifiersChanged(new_modifiers) => {
1457                    self.modifiers = new_modifiers.state();
1458                    let shift = self.modifiers.shift_key();
1459                    let ctrl = self.modifiers.control_key();
1460                    let alt = self.modifiers.alt_key();
1461                    let logo = self.modifiers.super_key();
1462                    cvkg_core::update_system_state(|st| {
1463                        let mut new_st = st.clone();
1464                        new_st.modifiers_shift = shift;
1465                        new_st.modifiers_ctrl = ctrl;
1466                        new_st.modifiers_alt = alt;
1467                        new_st.modifiers_logo = logo;
1468                        new_st
1469                    });
1470                }
1471                _ => {}
1472            }
1473        } // end of state block
1474
1475        if close_window {
1476            self.window_manager.close_window(id);
1477        }
1478        if quit_all {
1479            // Drain all windows; the is_empty check below will exit the event loop.
1480            for wid in self.window_manager.window_order().to_vec() {
1481                self.window_manager.close_window(wid);
1482            }
1483        }
1484        // Exit the event loop when all windows are closed (Cmd+W on last window, or Cmd+Q).
1485        if self.window_manager.windows.is_empty() {
1486            event_loop.exit();
1487        }
1488        if bring_to_front {
1489            self.window_manager.bring_to_front(id);
1490        }
1491        if create_new_window {
1492            self.window_manager.create_window(
1493                event_loop,
1494                &self.gpu,
1495                self.proxy.clone(),
1496                cvkg_core::WindowConfig {
1497                    title: "New CVKG Window".to_string(),
1498                    size: (800.0, 600.0),
1499                    ..Default::default()
1500                },
1501                false, // is_main
1502                &self.view,
1503            );
1504        }
1505    }
1506
1507    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1508        match event {
1509            AppEvent::AccessibilityAction(request) => {
1510                let node_id = cvkg_vdom::NodeId(request.target_node.0);
1511                let target_state = self.window_manager.windows.values_mut().find(|s| {
1512                    s.vdom
1513                        .as_ref()
1514                        .map_or(false, |v| v.nodes.contains_key(&node_id))
1515                });
1516
1517                if let Some(state) = target_state
1518                    && let Some(vdom) = &state.vdom
1519                    && let Some(node) = vdom.nodes.get(&node_id)
1520                    && request.action == accesskit::Action::Click
1521                {
1522                    let event = cvkg_core::Event::PointerClick {
1523                        x: node.layout.x + node.layout.width / 2.0,
1524                        y: node.layout.y + node.layout.height / 2.0,
1525                        button: 0, // Assume left click for accessibility actions
1526                        tilt: None,
1527                        azimuth: None,
1528                        pressure: Some(1.0),
1529                        barrel_rotation: None,
1530                        pointer_precision: 0.0,
1531                    };
1532                    vdom.dispatch_event(event);
1533                }
1534            }
1535            AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
1536                if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
1537                    if let Some(vdom) = &state.vdom {
1538                        let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
1539                        let mut nodes = Vec::new();
1540                        for (id, node) in &vdom.nodes {
1541                            nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
1542                        }
1543                        let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
1544                        if let Some(adapter) = &mut state.accesskit_adapter {
1545                            adapter.update_if_active(|| accesskit::TreeUpdate {
1546                                nodes,
1547                                tree: Some(tree),
1548                                focus: accesskit::NodeId(root_id),
1549                                tree_id: accesskit::TreeId::ROOT,
1550                            });
1551                        }
1552                    }
1553                }
1554            }
1555            AppEvent::CloseWindow(winit_id) => {
1556                self.window_manager.close_window(winit_id);
1557                if self.window_manager.windows.is_empty() {
1558                    event_loop.exit();
1559                }
1560            }
1561            AppEvent::SetTitle(winit_id, title) => {
1562                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1563                    data.window.set_title(&title);
1564                }
1565            }
1566            AppEvent::SetSize(winit_id, width, height) => {
1567                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1568                    let _ = data
1569                        .window
1570                        .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1571                }
1572            }
1573            AppEvent::SetVisible(winit_id, visible) => {
1574                if let Some(data) = self.window_manager.windows.get(&winit_id) {
1575                    data.window.set_visible(visible);
1576                }
1577            }
1578            AppEvent::BringToFront(winit_id) => {
1579                self.window_manager.bring_to_front(winit_id);
1580            }
1581        }
1582    }
1583
1584    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1585        // Apply Rage Decay: rage naturally settles to 0 over time.
1586        self.rage = (self.rage - 0.02).max(0.0);
1587
1588        // Frame Throttling: 60FPS target (16.6ms)
1589        let now = std::time::Instant::now();
1590        let target_interval = std::time::Duration::from_millis(16);
1591
1592        if now.duration_since(self.last_frame_time) >= target_interval {
1593            if self.rage > 0.01 {
1594                // Only log heartbeat when there is kinetic activity
1595                log::debug!("[Native] Heartbeat ticking (rage: {})", self.rage);
1596            }
1597            self.last_frame_time = now;
1598            for window_state in self.window_manager.windows.values() {
1599                window_state.window.request_redraw();
1600            }
1601            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1602                now + target_interval,
1603            ));
1604        } else {
1605            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1606                self.last_frame_time + target_interval,
1607            ));
1608        }
1609    }
1610}
1611
1612impl cvkg_core::ElapsedTime for NativeRenderer {
1613    fn delta_time(&self) -> f32 {
1614        self.delta_time
1615    }
1616
1617    fn elapsed_time(&self) -> f32 {
1618        self.elapsed_time
1619    }
1620}
1621
1622impl cvkg_core::Renderer for NativeRenderer {
1623    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1624        self.gpu
1625            .lock()
1626            .expect("GPU mutex poisoned: fill_rect")
1627            .fill_rect(rect, color);
1628    }
1629    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
1630        self.gpu
1631            .lock()
1632            .expect("GPU mutex poisoned: fill_rounded_rect")
1633            .fill_rounded_rect(rect, radius, color);
1634    }
1635    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1636        self.gpu
1637            .lock()
1638            .expect("GPU mutex poisoned: fill_ellipse")
1639            .fill_ellipse(rect, color);
1640    }
1641    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1642        self.gpu
1643            .lock()
1644            .expect("GPU mutex poisoned: stroke_rect")
1645            .stroke_rect(rect, color, stroke_width);
1646    }
1647    fn stroke_rounded_rect(
1648        &mut self,
1649        rect: cvkg_core::Rect,
1650        radius: f32,
1651        color: [f32; 4],
1652        stroke_width: f32,
1653    ) {
1654        self.gpu
1655            .lock()
1656            .expect("GPU mutex poisoned: stroke_rounded_rect")
1657            .stroke_rounded_rect(rect, radius, color, stroke_width);
1658    }
1659    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1660        self.gpu
1661            .lock()
1662            .expect("GPU mutex poisoned: stroke_ellipse")
1663            .stroke_ellipse(rect, color, stroke_width);
1664    }
1665    fn draw_line(
1666        &mut self,
1667        x1: f32,
1668        y1: f32,
1669        x2: f32,
1670        y2: f32,
1671        color: [f32; 4],
1672        stroke_width: f32,
1673    ) {
1674        self.gpu
1675            .lock()
1676            .expect("GPU mutex poisoned: draw_line")
1677            .draw_line(x1, y1, x2, y2, color, stroke_width);
1678    }
1679
1680    fn fill_glass_rect(&mut self, rect: cvkg_core::Rect, radius: f32, blur_radius: f32) {
1681        self.gpu
1682            .lock()
1683            .expect("GPU mutex poisoned: fill_glass_rect")
1684            .fill_glass_rect(rect, radius, blur_radius);
1685    }
1686
1687    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1688        self.gpu
1689            .lock()
1690            .expect("GPU mutex poisoned: draw_text")
1691            .draw_text(text, x, y, size, color);
1692    }
1693    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1694        self.gpu
1695            .lock()
1696            .expect("GPU mutex poisoned: measure_text")
1697            .measure_text(text, size)
1698    }
1699    fn draw_linear_gradient(
1700        &mut self,
1701        rect: cvkg_core::Rect,
1702        start_color: [f32; 4],
1703        end_color: [f32; 4],
1704        angle: f32,
1705    ) {
1706        self.gpu
1707            .lock()
1708            .expect("GPU mutex poisoned: draw_linear_gradient")
1709            .draw_linear_gradient(rect, start_color, end_color, angle);
1710    }
1711    fn draw_radial_gradient(
1712        &mut self,
1713        rect: cvkg_core::Rect,
1714        inner_color: [f32; 4],
1715        outer_color: [f32; 4],
1716    ) {
1717        self.gpu
1718            .lock()
1719            .expect("GPU mutex poisoned: draw_radial_gradient")
1720            .draw_radial_gradient(rect, inner_color, outer_color);
1721    }
1722    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
1723        self.gpu
1724            .lock()
1725            .expect("GPU mutex poisoned: draw_texture")
1726            .draw_texture(texture_id, rect);
1727    }
1728    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
1729        self.gpu
1730            .lock()
1731            .expect("GPU mutex poisoned: draw_image")
1732            .draw_image(image_name, rect);
1733    }
1734    fn load_image(&mut self, name: &str, data: &[u8]) {
1735        self.gpu
1736            .lock()
1737            .expect("GPU mutex poisoned: load_image")
1738            .load_image(name, data);
1739    }
1740    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
1741        self.gpu
1742            .lock()
1743            .expect("GPU mutex poisoned: push_clip_rect")
1744            .push_clip_rect(rect);
1745    }
1746    fn pop_clip_rect(&mut self) {
1747        self.gpu
1748            .lock()
1749            .expect("GPU mutex poisoned: pop_clip_rect")
1750            .pop_clip_rect();
1751    }
1752    fn push_opacity(&mut self, opacity: f32) {
1753        self.gpu
1754            .lock()
1755            .expect("GPU mutex poisoned: push_opacity")
1756            .push_opacity(opacity);
1757    }
1758    fn draw_3d_cube(&mut self, rect: cvkg_core::Rect, color: [f32; 4], rotation: [f32; 3]) {
1759        self.gpu
1760            .lock()
1761            .expect("GPU mutex poisoned: draw_3d_cube")
1762            .draw_3d_cube(rect, color, rotation);
1763    }
1764    fn pop_opacity(&mut self) {
1765        self.gpu
1766            .lock()
1767            .expect("GPU mutex poisoned: pop_opacity")
1768            .pop_opacity();
1769    }
1770    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
1771        self.gpu
1772            .lock()
1773            .expect("GPU mutex poisoned: bifrost")
1774            .bifrost(rect, blur, saturation, opacity);
1775    }
1776    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
1777        self.gpu
1778            .lock()
1779            .expect("GPU mutex poisoned: push_mjolnir_slice")
1780            .push_mjolnir_slice(angle, offset);
1781    }
1782    fn pop_mjolnir_slice(&mut self) {
1783        self.gpu
1784            .lock()
1785            .expect("GPU mutex poisoned: pop_mjolnir_slice")
1786            .pop_mjolnir_slice();
1787    }
1788    fn mjolnir_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
1789        self.gpu
1790            .lock()
1791            .expect("GPU mutex poisoned: mjolnir_shatter")
1792            .mjolnir_shatter(rect, pieces, force, color);
1793    }
1794    fn mjolnir_fluid_shatter(
1795        &mut self,
1796        rect: cvkg_core::Rect,
1797        pieces: u32,
1798        force: f32,
1799        color: [f32; 4],
1800    ) {
1801        self.gpu
1802            .lock()
1803            .expect("GPU mutex poisoned: mjolnir_fluid_shatter")
1804            .mjolnir_fluid_shatter(rect, pieces, force, color);
1805    }
1806    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1807        self.gpu
1808            .lock()
1809            .expect("GPU mutex poisoned: draw_mjolnir_bolt")
1810            .draw_mjolnir_bolt(from, to, color);
1811    }
1812    fn gungnir(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32, intensity: f32) {
1813        self.gpu
1814            .lock()
1815            .expect("GPU mutex poisoned: gungnir")
1816            .gungnir(rect, color, radius, intensity);
1817    }
1818    fn mani_glow(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32) {
1819        self.gpu
1820            .lock()
1821            .expect("GPU mutex poisoned: mani_glow")
1822            .mani_glow(rect, color, radius);
1823    }
1824    fn register_handler(
1825        &mut self,
1826        event_type: &str,
1827        handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1828    ) {
1829        self.gpu
1830            .lock()
1831            .expect("GPU mutex poisoned: register_handler")
1832            .register_handler(event_type, handler);
1833    }
1834    fn push_vnode(&mut self, rect: cvkg_core::Rect, name: &'static str) {
1835        self.gpu
1836            .lock()
1837            .expect("GPU mutex poisoned: push_vnode")
1838            .push_vnode(rect, name);
1839    }
1840    fn pop_vnode(&mut self) {
1841        self.gpu
1842            .lock()
1843            .expect("GPU mutex poisoned: pop_vnode")
1844            .pop_vnode();
1845    }
1846    // FIX #1: Removed duplicate definitions of set_z_index and get_z_index.
1847    // They appeared twice in this impl block (after pop_vnode and after register_shared_element),
1848    // which is a hard compiler error. Exactly one definition of each is kept here.
1849    fn set_z_index(&mut self, z: f32) {
1850        self.gpu
1851            .lock()
1852            .expect("GPU mutex poisoned: set_z_index")
1853            .set_z_index(z);
1854    }
1855    fn get_z_index(&self) -> f32 {
1856        self.gpu
1857            .lock()
1858            .expect("GPU mutex poisoned: get_z_index")
1859            .get_z_index()
1860    }
1861    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
1862        self.gpu
1863            .lock()
1864            .expect("GPU mutex poisoned: register_shared_element")
1865            .register_shared_element(id, rect);
1866    }
1867    fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1868        self.gpu
1869            .lock()
1870            .expect("GPU mutex poisoned: set_material")
1871            .set_material(material);
1872    }
1873    fn current_material(&self) -> cvkg_core::DrawMaterial {
1874        self.gpu
1875            .lock()
1876            .expect("GPU mutex poisoned: current_material")
1877            .current_material()
1878    }
1879    fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1880        self.gpu
1881            .lock()
1882            .expect("GPU mutex poisoned: serialize_svg")
1883            .serialize_svg(name)
1884    }
1885    fn apply_svg_filter(
1886        &mut self,
1887        name: &str,
1888        filter_id: &str,
1889        region: cvkg_core::Rect,
1890    ) -> Result<String, String> {
1891        self.gpu
1892            .lock()
1893            .expect("GPU mutex poisoned: apply_svg_filter")
1894            .apply_svg_filter(name, filter_id, region)
1895    }
1896    fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
1897        self.gpu
1898            .lock()
1899            .expect("GPU mutex poisoned: push_shadow")
1900            .push_shadow(radius, color, offset);
1901    }
1902    fn pop_shadow(&mut self) {
1903        self.gpu
1904            .lock()
1905            .expect("GPU mutex poisoned: pop_shadow")
1906            .pop_shadow();
1907    }
1908    fn push_affine(&mut self, transform: [f32; 6]) {
1909        self.gpu
1910            .lock()
1911            .expect("GPU mutex poisoned: push_affine")
1912            .push_affine(transform);
1913    }
1914    fn enter_portal(&mut self, z_index: i32) {
1915        // Portal layer rendering not yet supported in SurtrRenderer.
1916        // Content within portals renders inline as fallback.
1917        log::warn!(
1918            "Portal rendering (enter_portal) not yet implemented in GPU backend; z_index={}",
1919            z_index
1920        );
1921    }
1922    fn exit_portal(&mut self) {
1923        // Portal layer rendering not yet supported in SurtrRenderer.
1924        log::warn!("Portal rendering (exit_portal) not yet implemented in GPU backend");
1925    }
1926    fn viewport_size(&self) -> cvkg_core::Rect {
1927        let size = self.window.inner_size();
1928        let scale = self.window.scale_factor();
1929        let logical = size.to_logical::<f32>(scale);
1930        cvkg_core::Rect::new(0.0, 0.0, logical.width, logical.height)
1931    }
1932    fn announce(&mut self, message: &str, priority: cvkg_core::AnnouncementPriority) {
1933        // Delegate to AccessKit via the ShieldWall adapter if active.
1934        // For now, log the announcement. Full implementation requires
1935        // integration with the AccessKit tree update cycle.
1936        log::info!("Accessibility announcement [{:?}]: {}", priority, message);
1937    }
1938    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1939        self.gpu
1940            .lock()
1941            .expect("GPU mutex poisoned: load_svg")
1942            .load_svg(name, svg_data);
1943    }
1944    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
1945        self.gpu
1946            .lock()
1947            .expect("GPU mutex poisoned: draw_svg")
1948            .draw_svg(name, rect, None, 0);
1949    }
1950    fn draw_svg_with_offset(&mut self, name: &str, rect: cvkg_core::Rect, animation_time_offset: f32) {
1951        self.gpu
1952            .lock()
1953            .expect("GPU mutex poisoned: draw_svg_with_offset")
1954            .draw_svg_with_offset(name, rect, None, 0, animation_time_offset);
1955    }
1956    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1957        self.gpu
1958            .lock()
1959            .expect("GPU mutex poisoned: get_telemetry")
1960            .telemetry
1961            .clone()
1962    }
1963    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
1964        self.gpu
1965            .lock()
1966            .expect("GPU mutex poisoned: prewarm_vram")
1967            .prewarm_vram(assets);
1968    }
1969    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
1970        self.gpu
1971            .lock()
1972            .expect("GPU mutex poisoned: push_transform")
1973            .push_transform(translation, scale, rotation);
1974    }
1975    fn pop_transform(&mut self) {
1976        self.gpu
1977            .lock()
1978            .expect("GPU mutex poisoned: pop_transform")
1979            .pop_transform();
1980    }
1981
1982    fn set_berserker_mode(&mut self, state: cvkg_core::BerserkerMode) {
1983        self.berserker_mode = state;
1984
1985        // Berserker Determinism: Apply OS-level scheduler priority hints for GodMode.
1986        // SAFETY: setpriority is a POSIX syscall. We pass PRIO_PROCESS with pid=0 (self).
1987        // Failure is silently ignored via let _ because insufficient permissions are expected
1988        // in unprivileged environments and must not crash the render loop.
1989        if state == cvkg_core::BerserkerMode::GodMode {
1990            log::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
1991            #[cfg(target_os = "linux")]
1992            unsafe {
1993                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
1994            }
1995        } else {
1996            #[cfg(target_os = "linux")]
1997            unsafe {
1998                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
1999            }
2000        }
2001
2002        self.gpu
2003            .lock()
2004            .expect("GPU mutex poisoned: set_berserker_mode")
2005            .set_berserker_mode(state);
2006    }
2007
2008    fn set_rage(&mut self, rage: f32) {
2009        self.rage = rage;
2010        self.gpu
2011            .lock()
2012            .expect("GPU mutex poisoned: set_rage")
2013            .set_rage(rage);
2014    }
2015
2016    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
2017        self.gpu
2018            .lock()
2019            .expect("GPU mutex poisoned: memoize")
2020            .memoize(id, data_hash, render_fn);
2021    }
2022    fn request_redraw(&mut self) {
2023        self.window.request_redraw();
2024    }
2025
2026    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
2027    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
2028    ///
2029    /// FIX #4: capture_frame() returns a Future that borrows the SurtrRenderer, so the
2030    /// MutexGuard must remain alive until block_on completes — the guard cannot be dropped
2031    /// before the future is driven to completion. The lock is held for the duration of the
2032    /// GPU readback. This is acceptable because capture_png is an infrequent, explicit
2033    /// user-triggered operation (not called on the hot render path), so blocking other
2034    /// render calls for the readback duration is not a practical concern.
2035    fn capture_png(&mut self) -> Vec<u8> {
2036        log::info!("CAPTURING_FRAME: Initiating GPU readback...");
2037        // INVARIANT: The MutexGuard `gpu` must outlive the future returned by capture_frame()
2038        // because the future borrows from the SurtrRenderer. We therefore lock, block_on the
2039        // future (driving it to completion), and only then allow the guard to drop.
2040        let gpu = self.gpu.lock().expect("GPU mutex poisoned: capture_png");
2041        pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
2042            log::error!("GPU frame capture failed: {}", e);
2043            Vec::new() // Return empty buffer on failure — do not panic the render loop
2044        })
2045    }
2046
2047    fn print(&mut self) {
2048        log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
2049        // In a production environment, this would interface with CUPS/GDI/AirPrint.
2050        // For the Ulfhednar prototype, we simulate the handshake.
2051        println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
2052    }
2053}
2054
2055// ── Native Menu Bar Builder ───────────────────────────────────────────
2056
2057// ── Event Conversion Helpers ───────────────────────────────────────────
2058
2059fn convert_keyboard_event(event: winit::event::KeyEvent) -> Option<cvkg_core::Event> {
2060    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
2061        let key_str = format!("{:?}", code);
2062        if event.state == winit::event::ElementState::Pressed {
2063            Some(cvkg_core::Event::KeyDown { key: key_str })
2064        } else {
2065            Some(cvkg_core::Event::KeyUp { key: key_str })
2066        }
2067    } else {
2068        None
2069    }
2070}
2071
2072fn convert_ime_event(event: winit::event::Ime) -> Option<cvkg_core::Event> {
2073    if let winit::event::Ime::Commit(string) = event {
2074        Some(cvkg_core::Event::Ime(string))
2075    } else {
2076        None
2077    }
2078}
2079
2080fn convert_mouse_event(
2081    state: winit::event::ElementState,
2082    position: [f32; 2],
2083    button: u32,
2084) -> cvkg_core::Event {
2085    match state {
2086        winit::event::ElementState::Pressed => cvkg_core::Event::PointerDown {
2087            x: position[0],
2088            y: position[1],
2089            button,
2090            proximity_field: 0.0,
2091            tilt: None,
2092            azimuth: None,
2093            pressure: Some(1.0),
2094            barrel_rotation: None,
2095            pointer_precision: 0.0,
2096        },
2097        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
2098            x: position[0],
2099            y: position[1],
2100            button,
2101            tilt: None,
2102            azimuth: None,
2103            pressure: Some(0.0),
2104            barrel_rotation: None,
2105            pointer_precision: 0.0,
2106        },
2107    }
2108}
2109
2110// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
2111
2112struct ShieldWall {
2113    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
2114}
2115
2116impl accesskit::ActionHandler for ShieldWall {
2117    fn do_action(&mut self, request: accesskit::ActionRequest) {
2118        let _ = self
2119            .proxy
2120            .send_event(AppEvent::AccessibilityAction(request));
2121    }
2122}
2123
2124impl accesskit::ActivationHandler for ShieldWall {
2125    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
2126        let mut root = accesskit::Node::new(accesskit::Role::Window);
2127        root.set_label("CVKG Application");
2128
2129        let root_id = accesskit::NodeId(1);
2130        Some(accesskit::TreeUpdate {
2131            nodes: vec![(root_id, root)],
2132            tree: Some(accesskit::Tree::new(root_id)),
2133            focus: root_id,
2134            tree_id: accesskit::TreeId::ROOT,
2135        })
2136    }
2137}
2138
2139impl accesskit::DeactivationHandler for ShieldWall {
2140    fn deactivate_accessibility(&mut self) {}
2141}
2142
2143type AssetCacheMap =
2144    std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
2145
2146/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
2147///
2148/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
2149/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
2150/// without blocking concurrent render-loop readers.
2151pub struct NativeAssetManager {
2152    cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
2153}
2154
2155impl Default for NativeAssetManager {
2156    fn default() -> Self {
2157        Self::new()
2158    }
2159}
2160
2161impl NativeAssetManager {
2162    /// Create a new, empty NativeAssetManager.
2163    pub fn new() -> Self {
2164        Self {
2165            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
2166                std::collections::HashMap::new(),
2167            )),
2168        }
2169    }
2170}
2171
2172impl cvkg_core::AssetManager for NativeAssetManager {
2173    /// Return the cached asset state for `url`.
2174    ///
2175    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
2176    /// Slow path (cache miss): atomically insert a Loading sentinel via `rcu()`,
2177    /// then spawn a background thread for I/O. The `rcu()` closure may execute
2178    /// more than once under contention, so `already_tracked` is determined by
2179    /// whether the closure actually inserted the Loading entry (detected by checking
2180    /// the returned map). This prevents duplicate I/O threads for the same URL.
2181    ///
2182    /// FIX #5: The previous implementation set `already_tracked` inside the `rcu`
2183    /// closure body, which is incorrect because `rcu` retries the closure on
2184    /// contention — the bool would reflect only the last execution. The fix uses
2185    /// the fast-path check result plus the atomic `rcu` insertion to determine
2186    /// whether a thread must be spawned, making the logic correct under concurrency.
2187    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
2188        // Fast path: lock-free read from the current cache snapshot.
2189        if let Some(state) = self.cache.load().get(url) {
2190            return state.clone();
2191        }
2192
2193        let cache = self.cache.clone();
2194        let key = url.to_string();
2195
2196        // Slow path: atomically insert Loading if the key is absent.
2197        // `rcu` returns the final committed map; we inspect it to determine
2198        // whether *this* call was the one that inserted Loading (and thus
2199        // should spawn the I/O thread) versus a concurrent call that beat us.
2200        let mut we_inserted = false;
2201        self.cache.rcu(|map| {
2202            if map.contains_key(&key) {
2203                // Another caller already claimed this URL — do not insert.
2204                (**map).clone()
2205            } else {
2206                we_inserted = true;
2207                let mut m = (**map).clone();
2208                m.insert(key.clone(), cvkg_core::AssetState::Loading);
2209                m
2210            }
2211        });
2212
2213        // Only the caller that performed the insertion spawns the I/O thread,
2214        // preventing duplicate concurrent reads for the same asset URL.
2215        if we_inserted {
2216            let cache_inner = cache.clone();
2217            let key_inner = key.clone();
2218
2219            std::thread::spawn(move || {
2220                log::debug!("[Native] Asynchronously loading asset: {}", key_inner);
2221                let result = match std::fs::read(&key_inner) {
2222                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2223                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2224                };
2225
2226                cache_inner.rcu(move |map| {
2227                    let mut m = (**map).clone();
2228                    m.insert(key_inner.clone(), result.clone());
2229                    m
2230                });
2231            });
2232        }
2233
2234        cvkg_core::AssetState::Loading
2235    }
2236
2237    /// Trigger a background load of `url` without waiting for the result.
2238    ///
2239    /// FIX #6: The previous implementation had a bare fast-path check followed
2240    /// by an unconditional thread spawn, allowing two concurrent calls for the
2241    /// same URL to both spawn I/O threads. Now uses the same rcu-based insertion
2242    /// guard as `load_image` to ensure exactly one thread is spawned per URL.
2243    fn preload_image(&self, url: &str) {
2244        // Fast path: if already in cache (any state), no work to do.
2245        if self.cache.load().contains_key(url) {
2246            return;
2247        }
2248
2249        let cache = self.cache.clone();
2250        let key = url.to_string();
2251
2252        let mut we_inserted = false;
2253        self.cache.rcu(|map| {
2254            if map.contains_key(&key) {
2255                (**map).clone()
2256            } else {
2257                we_inserted = true;
2258                let mut m = (**map).clone();
2259                m.insert(key.clone(), cvkg_core::AssetState::Loading);
2260                m
2261            }
2262        });
2263
2264        if we_inserted {
2265            std::thread::spawn(move || {
2266                log::debug!("[Native] Preloading asset: {}", key);
2267                let result = match std::fs::read(&key) {
2268                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2269                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2270                };
2271
2272                cache.rcu(move |map| {
2273                    let mut m = (**map).clone();
2274                    m.insert(key.clone(), result.clone());
2275                    m
2276                });
2277            });
2278        }
2279    }
2280}
2281
2282#[cfg(test)]
2283mod tests {
2284    use super::*;
2285    use cvkg_core::AssetManager;
2286    use std::io::Write;
2287
2288    /// FIX #12: Replaced hardcoded relative path "test_asset.png" with a temp-dir path
2289    /// constructed from a unique per-test name. The previous path was written to the
2290    /// process working directory, which varies by invocation and causes collisions when
2291    /// tests run in parallel or when a prior run panics before cleanup.
2292    #[test]
2293    fn test_native_asset_manager_loading() {
2294        let manager = NativeAssetManager::new();
2295        let temp_path = std::env::temp_dir().join("cvkg_test_asset_loading.png");
2296        let temp_file_path = temp_path.to_str().expect("temp path must be valid UTF-8");
2297        let test_data = b"fake-image-data";
2298
2299        // Create a temporary file in the OS temp directory
2300        let mut file = std::fs::File::create(temp_file_path).unwrap();
2301        file.write_all(test_data).unwrap();
2302        drop(file);
2303
2304        // First call returns Loading and spawns the background I/O thread
2305        let mut state = manager.load_image(temp_file_path);
2306
2307        // Poll until Ready or timeout
2308        let start = std::time::Instant::now();
2309        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2310            std::thread::sleep(std::time::Duration::from_millis(10));
2311            state = manager.load_image(temp_file_path);
2312        }
2313
2314        if let cvkg_core::AssetState::Ready(data) = state {
2315            assert_eq!(&*data, test_data);
2316        } else {
2317            let _ = std::fs::remove_file(temp_file_path);
2318            panic!("Expected Ready state, got {:?}", state);
2319        }
2320
2321        // Verify fast path returns Ready immediately from cache
2322        let state2 = manager.load_image(temp_file_path);
2323        if let cvkg_core::AssetState::Ready(data) = state2 {
2324            assert_eq!(&*data, test_data);
2325        } else {
2326            let _ = std::fs::remove_file(temp_file_path);
2327            panic!("Expected Ready state (cached), got {:?}", state2);
2328        }
2329
2330        let _ = std::fs::remove_file(temp_file_path);
2331    }
2332
2333    #[test]
2334    fn test_native_asset_manager_error() {
2335        let manager = NativeAssetManager::new();
2336        let path = "non_existent_file_cvkg_test.png";
2337        let mut state = manager.load_image(path);
2338
2339        let start = std::time::Instant::now();
2340        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2341            std::thread::sleep(std::time::Duration::from_millis(10));
2342            state = manager.load_image(path);
2343        }
2344
2345        if let cvkg_core::AssetState::Error(_) = state {
2346            // Expected — non-existent file must produce an Error state
2347        } else {
2348            panic!("Expected Error state, got {:?}", state);
2349        }
2350    }
2351
2352    #[test]
2353    fn test_event_conversion() {
2354        // Mouse press event
2355        let event = convert_mouse_event(winit::event::ElementState::Pressed, [10.0, 20.0], 0);
2356        if let cvkg_core::Event::PointerDown { x, y, button, .. } = event {
2357            assert_eq!(x, 10.0);
2358            assert_eq!(y, 20.0);
2359            assert_eq!(button, 0);
2360        } else {
2361            panic!("Expected PointerDown");
2362        }
2363
2364        // IME commit event
2365        let event = convert_ime_event(winit::event::Ime::Commit("hello".to_string()));
2366        if let Some(cvkg_core::Event::Ime(s)) = event {
2367            assert_eq!(s, "hello");
2368        } else {
2369            panic!("Expected Ime event");
2370        }
2371    }
2372}
2373
2374/// load_icon — Searches known asset directories for 'icon.png'.
2375/// Returns a winit Icon if found and decodable, None otherwise.
2376/// All failures are logged at warn level — missing icons are non-fatal.
2377fn load_icon() -> Option<winit::window::Icon> {
2378    // FIX #13: Replaced unwrap_or_default() with unwrap_or_else that logs the failure.
2379    // unwrap_or_default() produced an empty PathBuf silently, making all subsequent
2380    // icon path lookups silently fail with no diagnostic output.
2381    let base = std::env::current_dir().unwrap_or_else(|e| {
2382        log::warn!(
2383            "[Native] Failed to get current directory for icon search: {}",
2384            e
2385        );
2386        std::path::PathBuf::new()
2387    });
2388
2389    let mut candidates = vec![
2390        base.join("icon.png"),
2391        base.join("crates/ulfhednar/icons/icon.png"),
2392        base.join("ulfhednar/icons/icon.png"),
2393        base.join("crates/ulfhednar/assets/icon.png"),
2394        base.join("ulfhednar/assets/icon.png"),
2395        base.join("assets/icon.png"),
2396    ];
2397
2398    // Also search relative to the executable directory
2399    if let Ok(exe_path) = std::env::current_exe()
2400        && let Some(exe_dir) = exe_path.parent()
2401    {
2402        candidates.push(exe_dir.join("icons/icon.png"));
2403        candidates.push(exe_dir.join("assets/icon.png"));
2404        candidates.push(exe_dir.join("icon.png"));
2405        if let Some(parent) = exe_dir.parent() {
2406            candidates.push(parent.join("icons/icon.png"));
2407            candidates.push(parent.join("assets/icon.png"));
2408            candidates.push(parent.join("icon.png"));
2409        }
2410    }
2411
2412    for path in candidates {
2413        if !path.exists() {
2414            log::debug!("[Native] Icon candidate not found: {:?}", path);
2415            continue;
2416        }
2417
2418        match image::open(&path) {
2419            Ok(img) => {
2420                let rgba = img.to_rgba8();
2421                let (width, height) = rgba.dimensions();
2422                match winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
2423                    Ok(icon) => {
2424                        log::info!("[Native] Successfully loaded app icon from: {:?}", path);
2425                        return Some(icon);
2426                    }
2427                    Err(e) => {
2428                        log::warn!("[Native] Icon format error at {:?}: {}", path, e);
2429                    }
2430                }
2431            }
2432            Err(e) => {
2433                log::warn!("[Native] Failed to open icon image at {:?}: {}", path, e);
2434            }
2435        }
2436    }
2437
2438    log::warn!(
2439        "[Native] Failed to find icon.png in any search path (CWD: {:?})",
2440        base
2441    );
2442    None
2443}
2444
2445// =============================================================================
2446// AUDIO / HAPTIC ENGINES — Cross-platform micro-feedback
2447// =============================================================================
2448
2449/// Cross-platform audio engine using rodio for spatialized sound cues.
2450/// Uses rodio 0.21 API: OutputStreamBuilder::open_default_stream() returns
2451/// OutputStream directly. Playback via Sink::try_new(&stream.mixer()) + append.
2452pub struct RodioAudioEngine {
2453    _stream: rodio::OutputStream,
2454}
2455
2456// OutputStream is not Send+Sync on macOS due to CoreAudio, but we only use it
2457// from the main thread. The AudioEngine trait requires Send+Sync for use in
2458// App struct fields, which is safe here because we never move it across threads.
2459unsafe impl Send for RodioAudioEngine {}
2460unsafe impl Sync for RodioAudioEngine {}
2461
2462impl RodioAudioEngine {
2463    /// Create a new audio engine. Falls back to None if audio init fails.
2464    pub fn new() -> Option<Self> {
2465        match rodio::OutputStreamBuilder::open_default_stream() {
2466            Ok(stream) => {
2467                log::info!("[Native] Audio engine initialized (rodio)");
2468                Some(Self { _stream: stream })
2469            }
2470            Err(e) => {
2471                log::warn!("[Native] Audio init failed (no sound): {}", e);
2472                None
2473            }
2474        }
2475    }
2476}
2477
2478impl cvkg_core::AudioEngine for RodioAudioEngine {
2479    fn play_sound(&self, name: &str, volume: f32) {
2480        let data: &[u8] = match name {
2481            "nav_tick" => cvkg_core::sounds::NAVIGATION_TICK,
2482            "success_chime" => cvkg_core::sounds::SUCCESS_CHIME,
2483            "warning_tone" => cvkg_core::sounds::WARNING_TONE,
2484            _ => {
2485                log::warn!("[Native] Unknown sound: {}", name);
2486                return;
2487            }
2488        };
2489        self.play_buffer(data, volume);
2490    }
2491
2492    fn play_buffer(&self, data: &[u8], _volume: f32) {
2493        use std::io::Cursor;
2494        let cursor = Cursor::new(data.to_vec());
2495        let mixer = self._stream.mixer();
2496        match rodio::play(mixer, cursor) {
2497            Ok(_sink) => {}
2498            Err(e) => log::warn!("[Native] Audio play failed: {}", e),
2499        }
2500    }
2501
2502    fn play_spatial(&self, name: &str, _position: [f32; 3], volume: f32) {
2503        // Spatial audio: play sound without positional attenuation (OS-agnostic fallback)
2504        self.play_sound(name, volume);
2505    }
2506}
2507
2508/// Visual haptic engine that translates haptic requests into visual micro-animations.
2509/// Used as a cross-platform fallback where native haptics are unavailable.
2510pub struct VisualHapticEngine {
2511    last_impact: std::sync::Mutex<std::time::Instant>,
2512}
2513
2514impl Default for VisualHapticEngine {
2515    fn default() -> Self {
2516        Self::new()
2517    }
2518}
2519
2520impl VisualHapticEngine {
2521    pub fn new() -> Self {
2522        Self {
2523            last_impact: std::sync::Mutex::new(std::time::Instant::now()),
2524        }
2525    }
2526}
2527
2528impl cvkg_core::HapticEngine for VisualHapticEngine {
2529    fn impact(&self, intensity: cvkg_core::HapticIntensity) {
2530        let _ = intensity;
2531        *self.last_impact.lock().unwrap() = std::time::Instant::now();
2532    }
2533    fn selection(&self) {
2534        self.impact(cvkg_core::HapticIntensity::Light);
2535    }
2536    fn success(&self) {
2537        self.impact(cvkg_core::HapticIntensity::Medium);
2538    }
2539    fn warning(&self) {
2540        self.impact(cvkg_core::HapticIntensity::Medium);
2541    }
2542    fn error(&self) {
2543        self.impact(cvkg_core::HapticIntensity::Heavy);
2544    }
2545    fn visual_tick(&self, _intensity: f32) {
2546        *self.last_impact.lock().unwrap() = std::time::Instant::now();
2547    }
2548}