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::{FocusableId, FrameRenderer, KvasirId, RenderStateSnapshot, 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::PhysicalPosition<f32>, 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.x as f32;
252        let py = pos.y 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// Thread-local raw pointer to the locked SurtrRenderer for the duration of one render pass.
329// CONTRACT: Set to non-null only while the MutexGuard is live on the call stack in render_frame_locked().
330// All NativeRenderer draw calls use this pointer to avoid per-call mutex lock overhead.
331// SAFETY: The pointer is valid because the MutexGuard is held for the entire duration the pointer is set.
332thread_local! {
333    static GPU_FRAME_PTR: std::cell::Cell<*mut cvkg_render_gpu::SurtrRenderer> =
334        const { std::cell::Cell::new(std::ptr::null_mut()) };
335}
336
337/// Native renderer backend implementing the Renderer trait.
338/// It wraps a shared SurtrRenderer for high-performance GPU drawing.
339/// During a render pass, GPU_FRAME_PTR is set so draw calls bypass the mutex.
340pub struct NativeRenderer {
341    gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
342    delta_time: f32,
343    elapsed_time: f32,
344    berserker_mode: cvkg_core::BerserkerMode,
345    rage: f32,
346    window: Arc<Window>,
347}
348
349impl NativeRenderer {
350    /// Returns a reference to the GPU renderer.
351    /// If GPU_FRAME_PTR is set (we're inside a locked render pass) uses that directly.
352    /// Otherwise falls back to acquiring the mutex (safe for calls outside the render pass).
353    ///
354    /// # Safety
355    /// GPU_FRAME_PTR is only non-null when a MutexGuard is live on the same thread's call stack.
356    #[inline(always)]
357    fn gpu_ref(&mut self) -> impl std::ops::DerefMut<Target = cvkg_render_gpu::SurtrRenderer> + '_ {
358        GPU_FRAME_PTR.with(|ptr| {
359            let raw = ptr.get();
360            if !raw.is_null() {
361                // SAFETY: Pointer is valid and the mutex guard is live above us on the call stack.
362                GpuRef::Ptr(unsafe { &mut *raw })
363            } else {
364                GpuRef::Guard(self.gpu.lock().unwrap_or_else(|p| p.into_inner()))
365            }
366        })
367    }
368
369    /// Read-only variant for &self Renderer methods.
370    /// Uses the same thread_local fast path; falls back to mutex for out-of-pass calls.
371    ///
372    /// # Safety
373    /// GPU_FRAME_PTR is only non-null when a MutexGuard is live above us on the call stack.
374    #[inline(always)]
375    fn gpu_ref_shared(&self) -> impl std::ops::Deref<Target = cvkg_render_gpu::SurtrRenderer> + '_ {
376        GPU_FRAME_PTR.with(|ptr| {
377            let raw = ptr.get();
378            if !raw.is_null() {
379                // SAFETY: Pointer is valid; the mutex guard is held for the render pass duration.
380                // We only read via this path during &self methods, which is safe.
381                GpuRefShared::Ptr(unsafe { &*raw })
382            } else {
383                GpuRefShared::Guard(self.gpu.lock().unwrap_or_else(|p| p.into_inner()))
384            }
385        })
386    }
387}
388
389/// Returned by NativeRenderer::gpu_ref() — either a direct pointer ref or a mutex guard.
390enum GpuRef<'a> {
391    Ptr(&'a mut cvkg_render_gpu::SurtrRenderer),
392    Guard(std::sync::MutexGuard<'a, cvkg_render_gpu::SurtrRenderer>),
393}
394
395impl<'a> std::ops::Deref for GpuRef<'a> {
396    type Target = cvkg_render_gpu::SurtrRenderer;
397    fn deref(&self) -> &Self::Target {
398        match self {
399            GpuRef::Ptr(r) => r,
400            GpuRef::Guard(g) => g,
401        }
402    }
403}
404
405impl<'a> std::ops::DerefMut for GpuRef<'a> {
406    fn deref_mut(&mut self) -> &mut Self::Target {
407        match self {
408            GpuRef::Ptr(r) => r,
409            GpuRef::Guard(g) => &mut *g,
410        }
411    }
412}
413
414/// Read-only variant returned by NativeRenderer::gpu_ref_shared().
415enum GpuRefShared<'a> {
416    Ptr(&'a cvkg_render_gpu::SurtrRenderer),
417    Guard(std::sync::MutexGuard<'a, cvkg_render_gpu::SurtrRenderer>),
418}
419
420impl<'a> std::ops::Deref for GpuRefShared<'a> {
421    type Target = cvkg_render_gpu::SurtrRenderer;
422    fn deref(&self) -> &Self::Target {
423        match self {
424            GpuRefShared::Ptr(r) => r,
425            GpuRefShared::Guard(g) => g,
426        }
427    }
428}
429
430/// Custom events for the native application event loop, handling accessibility
431/// callbacks and routing window lifecycle control events from background threads.
432#[derive(Debug)]
433pub enum AppEvent {
434    /// Action request from the accessibility subsystem.
435    AccessibilityAction(accesskit::ActionRequest),
436    /// Request to close a specific window.
437    CloseWindow(winit::window::WindowId),
438    /// Request to set the title bar string of a window.
439    SetTitle(winit::window::WindowId, String),
440    /// Request to resize a window.
441    SetSize(winit::window::WindowId, f32, f32),
442    /// Request to change visibility of a window.
443    SetVisible(winit::window::WindowId, bool),
444    /// Request to bring a window to the front and focus it.
445    BringToFront(winit::window::WindowId),
446    /// Initial accessibility tree requested by screen reader.
447    AccessibilityInitialTreeRequested(winit::window::WindowId),
448}
449
450impl From<accesskit_winit::Event> for AppEvent {
451    fn from(event: accesskit_winit::Event) -> Self {
452        match event.window_event {
453            accesskit_winit::WindowEvent::ActionRequested(req) => {
454                AppEvent::AccessibilityAction(req)
455            }
456            accesskit_winit::WindowEvent::InitialTreeRequested => {
457                AppEvent::AccessibilityInitialTreeRequested(event.window_id)
458            }
459            _ => AppEvent::AccessibilityAction(accesskit::ActionRequest {
460                action: accesskit::Action::Focus,
461                target_node: accesskit::NodeId(0),
462                target_tree: accesskit::TreeId::ROOT,
463                data: None,
464            }),
465        }
466    }
467}
468
469impl NativeRenderer {
470    /// Create a new NativeRenderer (internal use by App)
471    fn new(
472        window: Arc<Window>,
473        gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
474        delta_time: f32,
475        elapsed_time: f32,
476        berserker_mode: cvkg_core::BerserkerMode,
477        rage: f32,
478    ) -> Self {
479        Self {
480            gpu,
481            delta_time,
482            elapsed_time,
483            berserker_mode,
484            rage,
485            window,
486        }
487    }
488
489    /// Start the CVKG native application with the given view.
490    /// `prewarm_assets` is a list of (name, raw_bytes) pairs uploaded to the GPU
491    /// texture atlas on the first frame before any draw calls.
492    pub fn run<V: cvkg_core::View + 'static>(view: V, prewarm_assets: Option<Vec<(String, Vec<u8>)>>) {
493        let event_loop = EventLoop::<AppEvent>::with_user_event()
494            .build()
495            .expect("failed to create winit event loop: platform initialization failed");
496        event_loop.set_control_flow(ControlFlow::Wait);
497
498        let mut app = App {
499            view,
500            window_manager: WindowManager::new(),
501            gpu: None,
502            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
503            proxy: event_loop.create_proxy(),
504            start_time: std::time::Instant::now(),
505            last_frame_time: std::time::Instant::now(),
506            berserker_mode: cvkg_core::BerserkerMode::Normal,
507            rage: 0.0,
508            state_detector: WindowStateDetector::new(),
509            frame_budget: cvkg_core::FrameBudgetTracker::default_120fps(),
510            modifiers: winit::keyboard::ModifiersState::default(),
511            audio_engine: None,
512            haptic_engine: Arc::new(VisualHapticEngine::new()),
513            pending_prewarm: prewarm_assets,
514        };
515
516        event_loop.run_app(&mut app).expect("winit event loop terminated with error");
517    }
518
519    /// Convenience: run with a single background image loaded from a file path.
520    /// The image is loaded from disk and pre-warmed on the first frame.
521    /// `image_name` is the key used in `draw_image` / `draw_background_image`.
522    pub fn run_with_background<V: cvkg_core::View + 'static>(view: V, image_name: &str, image_path: &str) {
523        let image_data = std::fs::read(image_path)
524            .unwrap_or_else(|e| panic!("Failed to load background image '{}': {}", image_path, e));
525        let assets = vec![(image_name.to_string(), image_data)];
526        Self::run(view, Some(assets));
527    }
528}
529
530/// Native implementation of the cvkg_core::Window trait.
531/// Communicates state updates back to the winit event loop thread using an EventLoopProxy.
532struct NativeWindowWrapper {
533    winit_id: winit::window::WindowId,
534    window: Arc<winit::window::Window>,
535    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
536    is_key: Arc<std::sync::atomic::AtomicBool>,
537    is_main: bool,
538}
539
540impl cvkg_core::Window for NativeWindowWrapper {
541    /// Request that this window be closed.
542    fn close(&self) {
543        let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
544    }
545
546    /// Change the title bar text of this window.
547    fn set_title(&self, title: &str) {
548        let _ = self
549            .proxy
550            .send_event(AppEvent::SetTitle(self.winit_id, title.to_string()));
551    }
552
553    /// Request updating this window's dimensions.
554    fn set_size(&self, width: f32, height: f32) {
555        let _ = self
556            .proxy
557            .send_event(AppEvent::SetSize(self.winit_id, width, height));
558    }
559
560    /// Return true if this window has key focus.
561    fn is_key(&self) -> bool {
562        self.is_key.load(std::sync::atomic::Ordering::SeqCst)
563    }
564
565    /// Return true if this is the primary application window.
566    fn is_main(&self) -> bool {
567        self.is_main
568    }
569
570    /// Return true if this window is visible.
571    fn is_visible(&self) -> bool {
572        self.window.is_visible().unwrap_or(false)
573    }
574
575    /// Show or hide this window.
576    fn set_visible(&self, visible: bool) {
577        let _ = self
578            .proxy
579            .send_event(AppEvent::SetVisible(self.winit_id, visible));
580    }
581
582    /// Focus and bring this window to the foreground.
583    fn bring_to_front(&self) {
584        let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
585    }
586}
587
588/// Dynamic manager for all active native windows and their rendering contexts.
589pub struct WindowManager {
590    /// Mapping from native winit WindowId to internal WindowData.
591    pub windows: std::collections::HashMap<winit::window::WindowId, WindowData>,
592    /// Stack of windows ordered from back to front (end of vector is top-most).
593    pub window_stack: Vec<winit::window::WindowId>,
594    /// Mapping of winit window IDs to core IDs.
595    pub winit_to_core: std::collections::HashMap<winit::window::WindowId, cvkg_core::WindowId>,
596    /// Mapping of core window IDs to winit IDs.
597    pub core_to_winit: std::collections::HashMap<cvkg_core::WindowId, winit::window::WindowId>,
598    /// Monotonic counter to allocate unique core window IDs.
599    pub next_core_id: u64,
600}
601
602impl Default for WindowManager {
603    fn default() -> Self {
604        Self::new()
605    }
606}
607
608impl WindowManager {
609    /// Create an empty WindowManager.
610    pub fn new() -> Self {
611        Self {
612            windows: std::collections::HashMap::new(),
613            window_stack: Vec::new(),
614            winit_to_core: std::collections::HashMap::new(),
615            core_to_winit: std::collections::HashMap::new(),
616            next_core_id: 1,
617        }
618    }
619
620    /// Create and register a new native window.
621    pub fn create_window(
622        &mut self,
623        event_loop: &ActiveEventLoop,
624        gpu: &Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
625        proxy: winit::event_loop::EventLoopProxy<AppEvent>,
626        config: cvkg_core::WindowConfig,
627        is_main: bool,
628        view: &impl cvkg_core::View,
629    ) -> cvkg_core::WindowHandle {
630        let mut window_attrs = Window::default_attributes()
631            .with_title(&config.title)
632            .with_visible(true)
633            .with_transparent(config.transparent)
634            .with_decorations(config.decorations)
635            .with_inner_size(winit::dpi::LogicalSize::new(config.size.0, config.size.1));
636
637        if let Some(min) = config.min_size {
638            window_attrs =
639                window_attrs.with_min_inner_size(winit::dpi::LogicalSize::new(min.0, min.1));
640        }
641        if let Some(max) = config.max_size {
642            window_attrs =
643                window_attrs.with_max_inner_size(winit::dpi::LogicalSize::new(max.0, max.1));
644        }
645
646        let winit_level = match config.level {
647            cvkg_core::WindowLevel::Normal => winit::window::WindowLevel::Normal,
648            cvkg_core::WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
649            cvkg_core::WindowLevel::PopUpMenu => winit::window::WindowLevel::AlwaysOnTop,
650        };
651        window_attrs = window_attrs.with_window_level(winit_level);
652
653        #[cfg(target_os = "macos")]
654        {
655            use winit::platform::macos::WindowAttributesExtMacOS;
656            window_attrs = window_attrs
657                .with_titlebar_transparent(true)
658                .with_title_hidden(true)
659                .with_fullsize_content_view(true)
660                .with_has_shadow(true);
661        }
662
663        #[cfg(target_os = "windows")]
664        {
665            // Windows-specific window attributes:
666            // WHY: Restores window shadow for undecorated windows to maintain Tahoe design aesthetics.
667            // CONTRACT: with_undecorated_shadow requires the winit platform-specific extension for Windows.
668            use winit::platform::windows::WindowAttributesExtWindows;
669            window_attrs = window_attrs.with_undecorated_shadow(true);
670        }
671
672        let window = Arc::new(
673            event_loop
674                .create_window(window_attrs)
675                .expect("failed to create native window: display connection may be unavailable"),
676        );
677
678        let winit_id = window.id();
679        let core_id = cvkg_core::WindowId(self.next_core_id);
680        self.next_core_id += 1;
681
682        let is_key_focused = Arc::new(std::sync::atomic::AtomicBool::new(true));
683
684        let wrapper = Arc::new(NativeWindowWrapper {
685            winit_id,
686            window: window.clone(),
687            proxy: proxy.clone(),
688            is_key: is_key_focused.clone(),
689            is_main,
690        });
691
692        let handle = cvkg_core::WindowHandle::new(core_id, wrapper);
693
694        let vdom = cvkg_vdom::VDom::build(
695            view,
696            cvkg_core::Rect::new(0.0, 0.0, config.size.0, config.size.1),
697        );
698
699        // On Linux, the accesskit_winit adapter automatically initializes the
700        // AT-SPI bus connection via accesskit_unix (added as a dependency).
701        // Screen readers and other assistive technologies will connect through this.
702        #[cfg(target_os = "linux")]
703        {
704            log::info!("[Accessibility] AT-SPI backend available (accesskit_unix)");
705        }
706
707        let accesskit_adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
708            event_loop,
709            &window,
710            proxy.clone(),
711        ));
712
713        let data = WindowData {
714            window: window.clone(),
715            accesskit_adapter,
716            vdom: Some(vdom),
717            cursor_pos: [0.0, 0.0],
718            cursor_velocity: [0.0, 0.0],
719            last_redraw_start: std::time::Instant::now(),
720            frame_history: std::collections::VecDeque::with_capacity(60),
721            frame_count: 0,
722            last_pos: None,
723            needs_cursor_update: false,
724            is_dragging: false,
725            drag_start_pos: [0.0, 0.0],
726            drag_button: 0,
727            drag_threshold: 5.0,
728            active_pointer_target: None,
729            active_pointer_target_type: None,
730            active_pointer_target_key: None,
731            active_pointer_pos: None,
732            active_pointer_precision: 0.0,
733            is_key_focused,
734            is_main,
735            core_id,
736            window_handle: handle.clone(),
737            focus_manager: cvkg_core::FocusManager::new(),
738            focused_node_id: None,
739            last_touch_time: None,
740        };
741
742        self.windows.insert(winit_id, data);
743        self.window_stack.push(winit_id);
744        self.winit_to_core.insert(winit_id, core_id);
745        self.core_to_winit.insert(core_id, winit_id);
746
747        if let Some(gpu_mutex) = gpu {
748            gpu_mutex.lock().unwrap_or_else(|p| p.into_inner()).register_window(window.clone());
749        }
750
751        handle
752    }
753
754    /// Close and unregister a native window.
755    pub fn close_window(&mut self, winit_id: winit::window::WindowId) {
756        self.windows.remove(&winit_id);
757        self.window_stack.retain(|id| *id != winit_id);
758        if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
759            self.core_to_winit.remove(&core_id);
760        }
761    }
762
763    /// Bring a native window to the foreground and focus it.
764    pub fn bring_to_front(&mut self, winit_id: winit::window::WindowId) {
765        self.window_stack.retain(|id| *id != winit_id);
766        self.window_stack.push(winit_id);
767        if let Some(data) = self.windows.get(&winit_id) {
768            data.window.focus_window();
769        }
770    }
771
772    /// Get a reference to a window's data.
773    pub fn window(&self, winit_id: winit::window::WindowId) -> Option<&WindowData> {
774        self.windows.get(&winit_id)
775    }
776
777    /// Get a mutable reference to a window's data.
778    pub fn window_mut(&mut self, winit_id: winit::window::WindowId) -> Option<&mut WindowData> {
779        self.windows.get_mut(&winit_id)
780    }
781
782    /// Return the list of window IDs in current Z-order stack.
783    pub fn window_order(&self) -> &[winit::window::WindowId] {
784        &self.window_stack
785    }
786}
787
788pub struct WindowData {
789    window: Arc<Window>,
790    accesskit_adapter: Option<accesskit_winit::Adapter>,
791    vdom: Option<cvkg_vdom::VDom>,
792    cursor_pos: [f32; 2],
793    cursor_velocity: [f32; 2],
794    /// The instant the last redraw finished, used for measuring inter-frame gap timing.
795    last_redraw_start: std::time::Instant,
796    /// Sliding window of frame times for tail latency (P99) calculation.
797    frame_history: std::collections::VecDeque<f32>,
798    /// Total frames rendered on this window.
799    frame_count: u64,
800    /// Last window position for shake detection.
801    last_pos: Option<[i32; 2]>,
802    /// Set when mouse moves; cleared when redraw processes. Prevents redundant
803    /// VDOM rebuilds when cursor moves faster than the display refresh rate.
804    needs_cursor_update: bool,
805    // ── Drag tracking ──────────────────────────────────────────────────────
806    /// Whether a drag is currently in progress.
807    is_dragging: bool,
808    /// The position where the drag started.
809    drag_start_pos: [f32; 2],
810    /// The button that initiated the drag.
811    drag_button: u32,
812    /// Drag threshold in logical pixels (pointer must move this far to start drag).
813    drag_threshold: f32,
814    /// Pointer target captured on press so release/click stay stable through rebuilds.
815    active_pointer_target: Option<cvkg_vdom::NodeId>,
816    /// Stashed component_type of the pressed target, used to verify identity across rebuilds.
817    active_pointer_target_type: Option<String>,
818    /// Stashed key of the pressed target, used to verify identity across rebuilds.
819    active_pointer_target_key: Option<String>,
820    /// Pointer position captured on press for fallback hit-testing.
821    active_pointer_pos: Option<[f32; 2]>,
822    /// Pointer precision captured on press for fallback hit-testing.
823    active_pointer_precision: f32,
824
825    // ── Multi-window tracking ──────────────────────────────────────────────
826    is_key_focused: Arc<std::sync::atomic::AtomicBool>,
827    is_main: bool,
828    core_id: cvkg_core::WindowId,
829    window_handle: cvkg_core::WindowHandle,
830
831    // ── Focus navigation ───────────────────────────────────────────────────
832    focus_manager: cvkg_core::FocusManager,
833    focused_node_id: Option<cvkg_vdom::NodeId>,
834    
835    // -- Input disambiguation --
836    last_touch_time: Option<std::time::Instant>,
837}
838
839struct App<V: cvkg_core::View> {
840    view: V,
841    window_manager: WindowManager,
842    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
843    #[allow(dead_code)]
844    asset_manager: std::sync::Arc<NativeAssetManager>,
845    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
846    start_time: std::time::Instant,
847    last_frame_time: std::time::Instant,
848    berserker_mode: cvkg_core::BerserkerMode,
849    rage: f32,
850    /// Tracks the current window state for render-loop decisions.
851    state_detector: WindowStateDetector,
852    /// Global frame budget used for explicit per-phase telemetry.
853    frame_budget: cvkg_core::FrameBudgetTracker,
854    /// Tracks active modifier key states (Ctrl, Shift, Command, etc.).
855    modifiers: winit::keyboard::ModifiersState,
856    /// Cross-platform audio engine for spatialized sound cues.
857    audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
858    /// Visual haptic engine for micro-feedback animations.
859    haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
860    /// Assets to prewarm on the first frame (name, raw bytes). Drained once.
861    pending_prewarm: Option<Vec<(String, Vec<u8>)>>,
862}
863
864impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
865    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
866        if self.gpu.is_none() {
867            // Detect and apply system accessibility preferences at startup
868            let a11y_prefs = cvkg_core::AccessibilityPreferences::detect_from_system();
869            cvkg_core::set_accessibility_preferences(a11y_prefs);
870            if a11y_prefs.reduce_motion
871                || a11y_prefs.reduce_transparency
872                || a11y_prefs.increase_contrast
873            {
874                log::info!(
875                    "[Native] Accessibility prefs: motion={} transparency={} contrast={}",
876                    a11y_prefs.reduce_motion,
877                    a11y_prefs.reduce_transparency,
878                    a11y_prefs.increase_contrast
879                );
880            }
881
882            // Detect and apply system theme (dark/light)
883            let system_theme = cvkg_core::detect_system_theme();
884            log::info!("[Native] System theme detected: {:?}", system_theme);
885
886            // Initialize cross-platform audio engine
887            self.audio_engine =
888                RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
889
890            // Initialize visual haptic engine for micro-feedback
891            self.haptic_engine = Arc::new(VisualHapticEngine::new());
892
893            log::info!("[Native] App instance (resumed): {:p}", self);
894
895            let config = cvkg_core::WindowConfig {
896                title: "CVKG Berserker".to_string(),
897                size: (1280.0, 720.0),
898                min_size: None,
899                max_size: None,
900                resizable: true,
901                transparent: true,
902                decorations: true,
903                level: cvkg_core::WindowLevel::Normal,
904            };
905
906            let handle = self.window_manager.create_window(
907                event_loop,
908                &self.gpu,
909                self.proxy.clone(),
910                config,
911                true, // is_main
912                &self.view,
913            );
914
915            let winit_id = self
916                .window_manager
917                .core_to_winit
918                .get(&handle.id)
919                .copied()
920                .unwrap_or_else(|| panic!("winit_id not found for window handle: window may have been destroyed"));
921            let window = self
922                .window_manager
923                .windows
924                .get(&winit_id)
925                .unwrap()
926                .window
927                .clone();
928
929            // Immediately set self.gpu to prevent re-entry
930            let mut gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
931
932            // Phase 2.3: Pre-shape static labels to warm the text cache.
933            // These strings are rendered every frame by the berserker demo
934            // (NornirBar menu items, dock labels, overlay labels).
935            // Pre-shaping avoids the first-frame HarfBuzz cost.
936            static PREFETCH_LABELS: &[(&str, f32)] = &[
937                // NornirBar menu items
938                ("File", 13.0),
939                ("Edit", 13.0),
940                ("View", 13.0),
941                ("Window", 13.0),
942                ("Help", 13.0),
943                // Title / overlay labels (common sizes)
944                ("Berserker", 14.0),
945                ("Rage", 12.0),
946                ("FPS", 12.0),
947                ("Frame", 12.0),
948                ("Draw", 12.0),
949                ("Layout", 12.0),
950                ("Submit", 12.0),
951                ("Browser", 12.0),
952                ("Chat", 12.0),
953                ("Code", 12.0),
954                ("Terminal", 12.0),
955            ];
956            gpu.prewarm_text_cache(PREFETCH_LABELS);
957
958            self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
959
960            log::info!("[Native] Initialization complete.");
961            window.request_redraw();
962        }
963    }
964
965    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
966        if matches!(cause, winit::event::StartCause::Poll) {
967            // Too noisy
968        } else {
969            // Lowered to trace to prevent logs flooding under standard debug levels
970            log::trace!("[Native] Event Loop Wake: {:?}", cause);
971        }
972    }
973
974    fn device_event(
975        &mut self,
976        _event_loop: &ActiveEventLoop,
977        _device_id: winit::event::DeviceId,
978        event: winit::event::DeviceEvent,
979    ) {
980        if matches!(event, winit::event::DeviceEvent::MouseMotion { .. }) {
981            // log::trace!("[Native] Raw Mouse Motion");
982        } else {
983            // Log device raw events at trace level to prevent I/O blocking performance issues
984            // under high mouse-polling rates on systems with direct input mapping.
985            log::trace!("[Native] DEVICE EVENT: {:?}", event);
986        }
987    }
988
989    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
990        if !matches!(event, WindowEvent::RedrawRequested)
991            && !matches!(event, WindowEvent::CursorMoved { .. })
992        {
993            log::info!(
994                "[Native] App instance: {:p} | WINDOW EVENT: {:?}",
995                self,
996                event
997            );
998        }
999
1000        let gpu_arc = if let Some(g) = &self.gpu {
1001            g.clone()
1002        } else {
1003            log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
1004            return;
1005        };
1006
1007        let mut close_window = false;
1008        let mut bring_to_front = false;
1009        let mut create_new_window = false;
1010        // Cmd+Q was pressed -- close all windows after the state block ends.
1011        let mut quit_all = false;
1012
1013        {
1014            let state = if let Some(s) = self.window_manager.windows.get_mut(&id) {
1015                s
1016            } else {
1017                return;
1018            };
1019
1020            match event {
1021                WindowEvent::Moved(pos) => {
1022                    let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
1023                    let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
1024                    let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
1025
1026                    if speed > 0.1 {
1027                        // Significant kinetic injection
1028                        self.rage = (self.rage + 0.2).min(1.0);
1029                        log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
1030                    }
1031
1032                    state.last_pos = Some([pos.x, pos.y]);
1033                    state.window.request_redraw();
1034                }
1035                WindowEvent::DroppedFile(path) => {
1036                    if let Some(vdom) = &state.vdom {
1037                        vdom.dispatch_event(cvkg_core::Event::FileDrop {
1038                            x: state.cursor_pos[0],
1039                            y: state.cursor_pos[1],
1040                            path: path.to_string_lossy().into_owned(),
1041                        });
1042                    }
1043                }
1044                WindowEvent::CloseRequested => {
1045                    let close_action = cvkg_core::WindowCloseAction::Allow;
1046                    match close_action {
1047                        cvkg_core::WindowCloseAction::Allow
1048                        | cvkg_core::WindowCloseAction::Confirm => {
1049                            close_window = true;
1050                        }
1051                        cvkg_core::WindowCloseAction::Deny => {
1052                            log::info!("[Native] Close request denied for window {:?}", id);
1053                        }
1054                    }
1055                }
1056                WindowEvent::Resized(physical_size) => {
1057                    gpu_arc
1058                        .lock()
1059                        .unwrap_or_else(|p| p.into_inner())
1060                        .resize(
1061                            id,
1062                            physical_size.width,
1063                            physical_size.height,
1064                            state.window.scale_factor() as f32,
1065                        );
1066                    state.window.request_redraw();
1067                }
1068                WindowEvent::Focused(focused) => {
1069                    log::info!("[Native] Window focus changed: {}", focused);
1070                    state
1071                        .is_key_focused
1072                        .store(focused, std::sync::atomic::Ordering::SeqCst);
1073                    if focused {
1074                        bring_to_front = true;
1075                    }
1076                }
1077                WindowEvent::RedrawRequested => {
1078                    if state.frame_count % 60 == 0 {
1079                        log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
1080                    }
1081                    let size = state.window.inner_size();
1082                    let scale = state.window.scale_factor();
1083                    let logical_size = size.to_logical::<f32>(scale);
1084
1085                    let rect = cvkg_core::Rect {
1086                        x: 0.0,
1087                        y: 0.0,
1088                        width: logical_size.width,
1089                        height: logical_size.height,
1090                    };
1091
1092                    // Record the start of this redraw and snapshot the previous frame's
1093                    // start time before overwriting it, so inter-frame gap is measurable.
1094                    let redraw_start = std::time::Instant::now();
1095                    let last_redraw_start = state.last_redraw_start;
1096                    // Update last_redraw_start immediately so the next frame measures correctly
1097                    // even if this frame returns early.
1098                    state.last_redraw_start = redraw_start;
1099                    self.frame_budget.new_frame();
1100
1101                    // Build new vdom and diff (layout pass)
1102                    let layout_start = std::time::Instant::now();
1103                    let view_changed = self.view.changed();
1104
1105                    // Phase 1.2: Skip VDom rebuild when view hasn't changed.
1106                    let new_vdom: Option<cvkg_vdom::VDom> = if view_changed {
1107                        let vdom_start = std::time::Instant::now();
1108                        let vdom = cvkg_vdom::VDom::build(&self.view, rect);
1109                        let vdom_elapsed = vdom_start.elapsed();
1110                        if vdom_elapsed > std::time::Duration::from_millis(1) {
1111                            log::warn!("[Native] VDom::build took {:?} ({} nodes)", vdom_elapsed, vdom.nodes.len());
1112                        }
1113                        Some(vdom)
1114                    } else {
1115                        None
1116                    };
1117
1118                    // Dispatch cursor events if the mouse moved since last frame
1119                    if state.needs_cursor_update {
1120                        if let Some(vdom) = &state.vdom {
1121                            vdom.dispatch_event(cvkg_core::Event::PointerMove {
1122                                x: state.cursor_pos[0],
1123                                y: state.cursor_pos[1],
1124                                proximity_field: 0.0,
1125                                tilt: None,
1126                                azimuth: None,
1127                                pressure: Some(1.0),
1128                                barrel_rotation: None,
1129                                pointer_precision: 0.0,
1130                            });
1131                        }
1132                        state.needs_cursor_update = false;
1133                    }
1134                    let layout_end = std::time::Instant::now();
1135                    self.frame_budget.subsystem_finish(1);
1136
1137                    // Apply patches to the accessibility tree and the previous VDOM.
1138                    // When new_vdom is None (view unchanged), skip diff entirely.
1139                    let state_flush_start = std::time::Instant::now();
1140                    #[allow(unused)]
1141                    let mut diff_patches = None;
1142                    match (new_vdom, &mut state.vdom) {
1143                        (Some(new_vdom), Some(prev_vdom)) => {
1144                            let diff_start = std::time::Instant::now();
1145                            let patches = prev_vdom.diff(&new_vdom);
1146                            let diff_elapsed = diff_start.elapsed();
1147                            if diff_elapsed > std::time::Duration::from_millis(1) {
1148                                log::warn!("[Native] VDom::diff took {:?} ({} patches)", diff_elapsed, patches.len());
1149                            }
1150                            diff_patches = Some(patches);
1151                            let patches = diff_patches.as_ref().unwrap();
1152                            let mut nodes = Vec::new();
1153                            for patch in patches {
1154                                if let cvkg_vdom::VDomPatch::Create(node)
1155                                | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
1156                                {
1157                                    nodes.push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
1158                                } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
1159                                    && let Some(node) = new_vdom.nodes.get(id)
1160                                {
1161                                    nodes.push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
1162                                } else if let cvkg_vdom::VDomPatch::Remove(id) = patch {
1163                                    // Unregister removed nodes from focus manager to prevent
1164                                    // unbounded growth of the focus order list.
1165                                    state.focus_manager.unregister(&FocusableId::from(id.0.to_string()));
1166                                }
1167                            }
1168                            let focused_id = state.focused_node_id.map(|id| accesskit::NodeId(id.0)).unwrap_or(accesskit::NodeId(1));
1169                            for patch in diff_patches.as_ref().unwrap() {
1170                                if let cvkg_vdom::VDomPatch::Create(node)
1171                                | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
1172                                {
1173                                    if node.is_focusable() {
1174                                        state.focus_manager.register(node.id.0.to_string());
1175                                    }
1176                                }
1177                            }
1178                            if !nodes.is_empty() {
1179                                if let Some(adapter) = &mut state.accesskit_adapter {
1180                                    adapter.update_if_active(|| accesskit::TreeUpdate {
1181                                        nodes,
1182                                        tree: None,
1183                                        focus: focused_id,
1184                                        tree_id: accesskit::TreeId::ROOT,
1185                                    });
1186                                }
1187                            }
1188                            prev_vdom.apply_patches(diff_patches.unwrap());
1189                            state.vdom = Some(new_vdom);
1190                        }
1191                        (Some(new_vdom), None) => {
1192                            state.vdom = Some(new_vdom);
1193                        }
1194                        (None, _) => {
1195                            // View unchanged -- keep existing state.vdom as-is.
1196                        }
1197                    }
1198                    let state_flush_end = std::time::Instant::now();
1199                    self.frame_budget.subsystem_finish(0);
1200
1201                    let _draw_start = std::time::Instant::now();
1202                    let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
1203                    let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
1204
1205                    // Compute safe area insets based on current window state
1206                    let safe_area = crate::SafeAreaInsets::for_window_state(self.state_detector.state());
1207                    let content_rect = cvkg_core::Rect {
1208                        x: safe_area.left,
1209                        y: safe_area.top,
1210                        width: rect.width - safe_area.left - safe_area.right,
1211                        height: rect.height - safe_area.top - safe_area.bottom,
1212                    };
1213                    let layout_deadline = std::time::Instant::now()
1214                        + self.frame_budget.allocations()[1].time_slice;
1215                    cvkg_core::LayoutCache::set_layout_budget_deadline(Some(layout_deadline));
1216
1217                    let mut renderer = NativeRenderer::new(
1218                        state.window.clone(),
1219                        gpu_arc.clone(),
1220                        delta_time,
1221                        elapsed_time,
1222                        self.berserker_mode,
1223                        self.rage,
1224                    );
1225
1226                    // Single GPU lock for the entire frame: update mouse, prewarm, begin, draw, render, submit.
1227                    // This eliminates two extra lock/unlock cycles per frame.
1228                    let cpu_draw_start = std::time::Instant::now();
1229                    let mut gpu = gpu_arc.lock().unwrap_or_else(|p| p.into_inner());
1230                    let gpu_lock_time = cpu_draw_start.elapsed().as_secs_f32() * 1000.0;
1231
1232                    // Update mouse position
1233                    gpu.update_mouse(state.cursor_pos, state.cursor_velocity);
1234
1235                    // One-time prewarm: drain any pending assets into the GPU texture atlas
1236                    if let Some(assets) = self.pending_prewarm.take() {
1237                        log::info!("[Native] Pre-warming {} assets on first frame", assets.len());
1238                        gpu.prewarm_vram(assets);
1239                    }
1240
1241                    // Begin frame
1242                    let encoder = gpu.begin_frame(id);
1243                    let begin_frame_time = cpu_draw_start.elapsed().as_secs_f32() * 1000.0 - gpu_lock_time;
1244
1245                    // Render pass: publish pointer, draw, clear pointer
1246                    {
1247                        let raw: *mut cvkg_render_gpu::SurtrRenderer = &mut *gpu;
1248                        GPU_FRAME_PTR.with(|ptr| ptr.set(raw));
1249                        let render_start = std::time::Instant::now();
1250                        self.view.render(&mut renderer, content_rect);
1251                        let render_time = render_start.elapsed().as_secs_f32() * 1000.0;
1252                        GPU_FRAME_PTR.with(|ptr| ptr.set(std::ptr::null_mut()));
1253                        if render_time > 5.0 {
1254                            log::warn!("[Native] view.render() took {:.2}ms (gpu_lock={:.2}ms, begin_frame={:.2}ms)", render_time, gpu_lock_time, begin_frame_time);
1255                        }
1256                    }
1257                    let cpu_draw_end = std::time::Instant::now();
1258                    cvkg_core::LayoutCache::clear_layout_budget_deadline();
1259
1260                    self.frame_budget.subsystem_finish(2);
1261
1262                    // Submit the frame (still under the same lock)
1263                    let gpu_render_start = std::time::Instant::now();
1264                    gpu.render_frame();
1265                    let gpu_render_end = std::time::Instant::now();
1266
1267                    // end_frame internally does: get_current_texture (vsync wait),
1268                    // render graph execution, queue.submit, and present.
1269                    gpu.end_frame(encoder);
1270                    let gpu_submit_end = std::time::Instant::now();
1271
1272                    // GPU guard drops here, releasing the lock
1273
1274                    // Detailed timing breakdown every 60 frames
1275                    if state.frame_count % 60 == 0 {
1276                        let cpu_draw = cpu_draw_end.duration_since(cpu_draw_start);
1277                        let gpu_render = gpu_render_end.duration_since(gpu_render_start);
1278                        let gpu_submit = gpu_submit_end.duration_since(gpu_render_end);
1279                        let total = gpu_submit_end.duration_since(redraw_start);
1280                        log::info!(
1281                            "[Native] Frame breakdown: cpu_draw={:?} gpu_render={:?} gpu_submit(end_frame)={:?} total={:?}",
1282                            cpu_draw, gpu_render, gpu_submit, total
1283                        );
1284                        log::info!(
1285                            "[Native] NOTE: gpu_submit includes surface.get_current_texture() vsync wait + render graph + queue.submit + present"
1286                        );
1287                    }
1288
1289                    // Build telemetry from this frame's timing measurements.
1290                    // NOTE: input_time_ms measures the inter-frame gap (time from end of last frame
1291                    // to start of this one), not input dispatch latency. The field name is defined
1292                    // in cvkg_core::TelemetryData and kept as-is to match that struct.
1293                    let mut telemetry = cvkg_core::TelemetryData::default();
1294                    telemetry.input_time_ms =
1295                        redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
1296                    telemetry.layout_time_ms =
1297                        layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
1298                    telemetry.state_flush_time_ms = state_flush_end
1299                        .duration_since(state_flush_start)
1300                        .as_secs_f32()
1301                        * 1000.0;
1302                    telemetry.draw_time_ms =
1303                        cpu_draw_end.duration_since(cpu_draw_start).as_secs_f32() * 1000.0;
1304                    telemetry.gpu_submit_time_ms = gpu_submit_end
1305                        .duration_since(cpu_draw_end)
1306                        .as_secs_f32()
1307                        * 1000.0;
1308
1309                    // Total frame time from redraw request to GPU submission complete
1310                    let frame_time_ms =
1311                        gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
1312                    telemetry.frame_time_ms = frame_time_ms;
1313                    telemetry.frame_budget_ms = self.frame_budget.total().as_secs_f32() * 1000.0;
1314                    telemetry.frame_budget_remaining_ms =
1315                        telemetry.frame_budget_ms - telemetry.frame_time_ms;
1316                    telemetry.layout_budget_remaining_ms = self
1317                        .frame_budget
1318                        .allocations()
1319                        .get(1)
1320                        .map(|alloc| alloc.time_slice.as_secs_f32() * 1000.0 - telemetry.layout_time_ms)
1321                        .unwrap_or(0.0);
1322                    telemetry.frame_over_budget = !self.frame_budget.frame_within_budget()
1323                        || telemetry.frame_budget_remaining_ms < 0.0;
1324                    telemetry.layout_over_budget = !self.frame_budget.is_within_budget(1)
1325                        || telemetry.layout_budget_remaining_ms < 0.0;
1326
1327                    // Log detailed frame time breakdown for performance diagnostics
1328                    log::info!(
1329                        "[Native] Frame timings: layout={:.2}ms state={:.2}ms draw={:.2}ms submit={:.2}ms total={:.2}ms",
1330                        telemetry.layout_time_ms,
1331                        telemetry.state_flush_time_ms,
1332                        telemetry.draw_time_ms,
1333                        telemetry.gpu_submit_time_ms,
1334                        telemetry.frame_time_ms
1335                    );
1336
1337                    // Tail Latency Tracking (P99 and Jitter) over a 100-frame sliding window.
1338                    state.frame_history.push_back(frame_time_ms);
1339                    if state.frame_history.len() > 100 {
1340                        state.frame_history.pop_front();
1341                    }
1342
1343                    let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
1344                    sorted_frames
1345                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1346
1347                    if !sorted_frames.is_empty() {
1348                        let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
1349                        telemetry.p99_frame_time_ms =
1350                            sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
1351
1352                        // Jitter: standard deviation of frame times over the sliding window.
1353                        let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
1354                        let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
1355                            / sorted_frames.len() as f32;
1356                        telemetry.frame_jitter_ms = variance.sqrt();
1357                    }
1358
1359                    // FIX #8: hardware_stall_detected is now reset each frame based on current
1360                    // jitter rather than being set once and never cleared. A single jittery frame
1361                    // no longer permanently flags the session. Jitter > 20ms is a heuristic for
1362                    // scheduling disruption (GC, OS preemption, slow layout) -- not a confirmed
1363                    // hardware stall, but the field name is defined in cvkg_core::TelemetryData.
1364                    telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
1365                    if telemetry.frame_over_budget {
1366                        log::warn!(
1367                            "[Native] Frame budget exceeded by {:.2}ms (layout remaining {:.2}ms)",
1368                            -telemetry.frame_budget_remaining_ms,
1369                            telemetry.layout_budget_remaining_ms
1370                        );
1371                    }
1372
1373                    state.frame_count += 1;
1374
1375                    telemetry.berserker_rage = self.rage;
1376                    gpu.telemetry = telemetry;
1377
1378                    // Drive the continuous animation loop: immediately schedule the next frame.
1379                    // Without this, winit's Wait mode only redraws on OS input events (mouse
1380                    // moves), which produces ~20fps driven by cursor poll rate instead of
1381                    // the 120fps target. This single call is the animation loop.
1382                    state.window.request_redraw();
1383                }
1384                WindowEvent::CursorEntered { .. } => {
1385                    log::info!("[Native] Cursor ENTERED window");
1386                    if let Some(vdom) = &state.vdom {
1387                        vdom.dispatch_event(cvkg_core::Event::PointerEnter);
1388                    }
1389                    state.window.request_redraw();
1390                }
1391                WindowEvent::CursorLeft { .. } => {
1392                    log::info!("[Native] Cursor LEFT window");
1393                    if let Some(vdom) = &state.vdom {
1394                        vdom.dispatch_event(cvkg_core::Event::PointerLeave);
1395                    }
1396                    state.window.request_redraw();
1397                }
1398                WindowEvent::CursorMoved { position, .. } => {
1399                    let scale = state.window.scale_factor();
1400                    let logical = position.to_logical::<f32>(scale);
1401                    let elapsed = state.last_redraw_start.elapsed().as_secs_f32().max(0.001);
1402                    let dx = logical.x - state.cursor_pos[0];
1403                    let dy = logical.y - state.cursor_pos[1];
1404                    state.cursor_velocity = [dx / elapsed, dy / elapsed];
1405                    state.cursor_pos = [logical.x, logical.y];
1406                    // Check if we've moved past the drag threshold
1407                    if !state.is_dragging {
1408                        let ddx = state.cursor_pos[0] - state.drag_start_pos[0];
1409                        let ddy = state.cursor_pos[1] - state.drag_start_pos[1];
1410                        let dist_sq = ddx * ddx + ddy * ddy;
1411                        if dist_sq > state.drag_threshold * state.drag_threshold {
1412                            state.is_dragging = true;
1413                        }
1414                    }
1415                    state.needs_cursor_update = true;
1416                    // Don't request_redraw here -- the redraw will process the cursor update.
1417                    // Only request a redraw if we're not already in a redraw cycle.
1418                    if state.frame_count == 0 {
1419                        state.window.request_redraw();
1420                    }
1421                }
1422                WindowEvent::MouseInput {
1423                    state: mouse_state,
1424                    button,
1425                    ..
1426                } => {
1427                    log::info!(
1428                        "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
1429                        mouse_state,
1430                        button,
1431                        state.cursor_pos
1432                    );
1433                    if let Some(touch_time) = state.last_touch_time {
1434                        if touch_time.elapsed().as_millis() < 500 {
1435                            log::info!("[Native] Ignoring MouseInput (synthesized from Touch)");
1436                            return;
1437                        }
1438                    }
1439                    if let Some(vdom) = &state.vdom {
1440                        let btn_id = match button {
1441                            winit::event::MouseButton::Left => 0,
1442                            winit::event::MouseButton::Right => 2,
1443                            winit::event::MouseButton::Middle => 1,
1444                            winit::event::MouseButton::Back => 3,
1445                            winit::event::MouseButton::Forward => 4,
1446                            winit::event::MouseButton::Other(id) => id as u32,
1447                        };
1448
1449                        match mouse_state {
1450                            winit::event::ElementState::Pressed => {
1451                                // Record drag start position for click/drag disambiguation
1452                                state.drag_start_pos = state.cursor_pos;
1453                                state.is_dragging = false;
1454                                state.drag_button = btn_id;
1455                                state.active_pointer_pos = Some(state.cursor_pos);
1456                                state.active_pointer_precision = 0.0;
1457                                state.active_pointer_target = vdom
1458                                    .hit_test(state.cursor_pos[0], state.cursor_pos[1], 0.0)
1459                                    .map(|(id, _)| id);
1460                                // Stash component_type and key for identity verification on release.
1461                                // NodeIds are stable across rebuilds when nodes have keys, but
1462                                // we still verify identity to be safe against hash collisions.
1463                                if let Some(target_id) = state.active_pointer_target {
1464                                    if let Some(node) = vdom.nodes.get(&target_id) {
1465                                        state.active_pointer_target_type = Some(node.component_type.clone());
1466                                        state.active_pointer_target_key = node.key.clone();
1467                                    }
1468                                }
1469                                log::info!("[Native] Dispatching PointerDown to VDOM");
1470                                vdom.dispatch_event(cvkg_core::Event::PointerDown {
1471                                    x: state.cursor_pos[0],
1472                                    y: state.cursor_pos[1],
1473                                    button: btn_id,
1474                                    proximity_field: 0.0,
1475                                    tilt: None,
1476                                    azimuth: None,
1477                                    pressure: Some(1.0),
1478                                    barrel_rotation: None,
1479                                    pointer_precision: 0.0,
1480                                });
1481                            }
1482                            winit::event::ElementState::Released => {
1483                                log::info!("[Native] Dispatching PointerUp to VDOM");
1484                                let fallback_target = state
1485                                    .active_pointer_pos
1486                                    .and_then(|pos| {
1487                                        vdom.hit_test(pos[0], pos[1], state.active_pointer_precision)
1488                                            .map(|(id, _)| id)
1489                                    })
1490                                    .or_else(|| {
1491                                        vdom.hit_test(
1492                                            state.cursor_pos[0],
1493                                            state.cursor_pos[1],
1494                                            state.active_pointer_precision,
1495                                        )
1496                                        .map(|(id, _)| id)
1497                                    });
1498                                // Verify the cached target is the same logical node.
1499                                // When key is None, identity can't be verified reliably
1500                                // (many nodes have None keys), so fall back to fresh hit-test.
1501                                let target = state
1502                                    .active_pointer_target
1503                                    .filter(|target| {
1504                                        if state.active_pointer_target_key.is_none() {
1505                                            log::debug!("[Native] Target verification: key is None, skipping cache");
1506                                            return false;
1507                                        }
1508                                        let verified = vdom.nodes.get(target).map_or(false, |node| {
1509                                            let type_match = Some(&node.component_type) == state.active_pointer_target_type.as_ref();
1510                                            let key_match = node.key == state.active_pointer_target_key;
1511                                            log::debug!("[Native] Target verify: id={:?} type={} key={:?} type_match={} key_match={}",
1512                                                target, node.component_type, node.key, type_match, key_match);
1513                                            type_match && key_match
1514                                        });
1515                                        if !verified {
1516                                            log::debug!("[Native] Target verification failed for {:?}, using fallback", target);
1517                                        }
1518                                        verified
1519                                    })
1520                                    .or(fallback_target);
1521                                let pointer_up = cvkg_core::Event::PointerUp {
1522                                    x: state.cursor_pos[0],
1523                                    y: state.cursor_pos[1],
1524                                    button: btn_id,
1525                                    tilt: None,
1526                                    azimuth: None,
1527                                    pressure: Some(0.0),
1528                                    barrel_rotation: None,
1529                                    pointer_precision: 0.0,
1530                                };
1531                                let pointer_click = cvkg_core::Event::PointerClick {
1532                                    x: state.cursor_pos[0],
1533                                    y: state.cursor_pos[1],
1534                                    button: btn_id,
1535                                    tilt: None,
1536                                    azimuth: None,
1537                                    pressure: Some(0.0),
1538                                    barrel_rotation: None,
1539                                    pointer_precision: 0.0,
1540                                };
1541                                if let Some(target) = target {
1542                                    vdom.dispatch_event_to_target(target, pointer_up);
1543                                } else {
1544                                    vdom.dispatch_event(pointer_up);
1545                                }
1546                                // Only dispatch PointerClick if we didn't drag
1547                                if !state.is_dragging {
1548                                    if let Some(target) = target {
1549                                        log::info!("[Native] Dispatching PointerClick to VDOM (target={:?})", target);
1550                                        vdom.dispatch_event_to_target(target, pointer_click);
1551                                    } else {
1552                                        log::info!("[Native] Dispatching PointerClick to VDOM (no target, bubbling)");
1553                                        vdom.dispatch_event(pointer_click);
1554                                    }
1555                                } else {
1556                                    log::info!("[Native] Skipping PointerClick (is_dragging=true)");
1557                                }
1558                                // Reset drag state
1559                                state.is_dragging = false;
1560                                state.active_pointer_target = None;
1561                                state.active_pointer_target_type = None;
1562                                state.active_pointer_target_key = None;
1563                                state.active_pointer_pos = None;
1564                            }
1565                        }
1566                        state.window.request_redraw();
1567                    } else {
1568                        log::warn!("[Native] Mouse input received but state.vdom is None!");
1569                    }
1570                }
1571                WindowEvent::MouseWheel { delta, .. } => {
1572                    if let Some(vdom) = &state.vdom {
1573                        let (dx, dy) = match delta {
1574                            winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
1575                            winit::event::MouseScrollDelta::PixelDelta(pos) => {
1576                                (pos.x as f32, pos.y as f32)
1577                            }
1578                        };
1579                        vdom.dispatch_event(cvkg_core::Event::PointerWheel {
1580                            x: state.cursor_pos[0],
1581                            y: state.cursor_pos[1],
1582                            delta_x: dx,
1583                            delta_y: dy,
1584                            pointer_precision: 0.0,
1585                        });
1586                        state.window.request_redraw();
1587                    }
1588                }
1589                // ── Touch screen inputs ──────────────────────────────────────────
1590                // Map native winit touchscreen events to VDOM Pointer events using
1591                // low-precision fat-finger bounding expansion (150px proximity field).
1592                WindowEvent::Touch(touch) => {
1593                    state.last_touch_time = Some(std::time::Instant::now());
1594                    if let Some(vdom) = &state.vdom {
1595                        let scale = state.window.scale_factor();
1596                        let logical = touch.location.to_logical::<f32>(scale);
1597                        let x = logical.x;
1598                        let y = logical.y;
1599                        let touch_btn = 0; // Touch maps to primary/left button
1600                        match touch.phase {
1601                            winit::event::TouchPhase::Started => {
1602                                log::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
1603                                // Record drag start position for click/drag disambiguation
1604                                state.drag_start_pos = [x, y];
1605                                state.is_dragging = false;
1606                                state.drag_button = touch_btn as u32;
1607                                state.active_pointer_pos = Some([x, y]);
1608                                state.active_pointer_precision = 150.0;
1609                                state.active_pointer_target = vdom.hit_test(x, y, 150.0).map(|(id, _)| id);
1610                                if let Some(target_id) = state.active_pointer_target {
1611                                    if let Some(node) = vdom.nodes.get(&target_id) {
1612                                        state.active_pointer_target_type = Some(node.component_type.clone());
1613                                        state.active_pointer_target_key = node.key.clone();
1614                                    }
1615                                }
1616                                vdom.dispatch_event(cvkg_core::Event::PointerDown {
1617                                    x,
1618                                    y,
1619                                    button: touch_btn,
1620                                    proximity_field: 0.0,
1621                                    tilt: None,
1622                                    azimuth: None,
1623                                    pressure: Some(
1624                                        touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
1625                                    ),
1626                                    barrel_rotation: None,
1627                                    pointer_precision: 150.0,
1628                                });
1629                            }
1630                            winit::event::TouchPhase::Moved => {
1631                                // Check if we've moved past the drag threshold
1632                                if !state.is_dragging {
1633                                    let ddx = x - state.drag_start_pos[0];
1634                                    let ddy = y - state.drag_start_pos[1];
1635                                    let dist_sq = ddx * ddx + ddy * ddy;
1636                                    if dist_sq > state.drag_threshold * state.drag_threshold {
1637                                        state.is_dragging = true;
1638                                    }
1639                                }
1640                                vdom.dispatch_event(cvkg_core::Event::PointerMove {
1641                                    x,
1642                                    y,
1643                                    proximity_field: 0.0,
1644                                    tilt: None,
1645                                    azimuth: None,
1646                                    pressure: Some(
1647                                        touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
1648                                    ),
1649                                    barrel_rotation: None,
1650                                    pointer_precision: 150.0,
1651                                });
1652                            }
1653                            winit::event::TouchPhase::Ended => {
1654                                let fallback_target = state
1655                                    .active_pointer_pos
1656                                    .and_then(|pos| {
1657                                        vdom.hit_test(pos[0], pos[1], state.active_pointer_precision)
1658                                            .map(|(id, _)| id)
1659                                    })
1660                                    .or_else(|| vdom.hit_test(x, y, state.active_pointer_precision).map(|(id, _)| id));
1661                                // Verify the cached target is the same logical node.
1662                                let target = state
1663                                    .active_pointer_target
1664                                    .filter(|target| {
1665                                        vdom.nodes.get(target).map_or(false, |node| {
1666                                            Some(&node.component_type) == state.active_pointer_target_type.as_ref()
1667                                                && node.key == state.active_pointer_target_key
1668                                        })
1669                                    })
1670                                    .or(fallback_target);
1671                                let pointer_up = cvkg_core::Event::PointerUp {
1672                                    x,
1673                                    y,
1674                                    button: touch_btn,
1675                                    tilt: None,
1676                                    azimuth: None,
1677                                    pressure: Some(0.0),
1678                                    barrel_rotation: None,
1679                                    pointer_precision: 150.0,
1680                                };
1681                                let pointer_click = cvkg_core::Event::PointerClick {
1682                                    x,
1683                                    y,
1684                                    button: touch_btn,
1685                                    tilt: None,
1686                                    azimuth: None,
1687                                    pressure: Some(0.0),
1688                                    barrel_rotation: None,
1689                                    pointer_precision: 150.0,
1690                                };
1691                                if let Some(target) = target {
1692                                    vdom.dispatch_event_to_target(target, pointer_up);
1693                                } else {
1694                                    vdom.dispatch_event(pointer_up);
1695                                }
1696                                // Only dispatch PointerClick if we didn't drag
1697                                if !state.is_dragging {
1698                                    if let Some(target) = target {
1699                                        log::info!("[Native] Dispatching PointerClick to VDOM (target={:?})", target);
1700                                        vdom.dispatch_event_to_target(target, pointer_click);
1701                                    } else {
1702                                        log::info!("[Native] Dispatching PointerClick to VDOM (no target, bubbling)");
1703                                        vdom.dispatch_event(pointer_click);
1704                                    }
1705                                } else {
1706                                    log::info!("[Native] Skipping PointerClick (is_dragging=true)");
1707                                }
1708                                // Reset drag state
1709                                state.is_dragging = false;
1710                                state.active_pointer_target = None;
1711                                state.active_pointer_target_type = None;
1712                                state.active_pointer_target_key = None;
1713                                state.active_pointer_pos = None;
1714                            }
1715                            winit::event::TouchPhase::Cancelled => {
1716                                vdom.dispatch_event(cvkg_core::Event::PointerUp {
1717                                    x,
1718                                    y,
1719                                    button: touch_btn,
1720                                    tilt: None,
1721                                    azimuth: None,
1722                                    pressure: Some(0.0),
1723                                    barrel_rotation: None,
1724                                    pointer_precision: 150.0,
1725                                });
1726                                state.active_pointer_target = None;
1727                                state.active_pointer_pos = None;
1728                            }
1729                        }
1730                        state.window.request_redraw();
1731                    }
1732                }
1733                // ── Trackpad gestures (pinch-to-zoom, swipe) ──────────────────────
1734                // OS-agnostic: winit provides these on macOS trackpad, Windows precision
1735                // touchpads, and Linux (where supported). Falls back gracefully.
1736                WindowEvent::PinchGesture { delta, .. } => {
1737                    if let Some(vdom) = &state.vdom {
1738                        let scale = 1.0 + delta as f32;
1739                        let velocity = delta as f32;
1740                        vdom.dispatch_event(cvkg_core::Event::GesturePinch {
1741                            center: state.cursor_pos,
1742                            scale,
1743                            velocity,
1744                            phase: cvkg_core::TouchPhase::Moved,
1745                        });
1746                    }
1747                    // Provide micro-feedback on pinch
1748                    if let Some(audio) = &self.audio_engine {
1749                        audio.play_sound("nav_tick", 0.3);
1750                    }
1751                    self.haptic_engine
1752                        .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
1753                    state.window.request_redraw();
1754                }
1755                WindowEvent::RotationGesture { delta, .. } => {
1756                    if let Some(vdom) = &state.vdom {
1757                        let angle = delta;
1758                        vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
1759                            direction: [angle.cos(), angle.sin()],
1760                            velocity: delta.abs(),
1761                            phase: cvkg_core::TouchPhase::Moved,
1762                        });
1763                    }
1764                    state.window.request_redraw();
1765                }
1766                WindowEvent::KeyboardInput { event, .. } => {
1767                    if event.state == winit::event::ElementState::Pressed {
1768                        if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
1769                            // Cross-platform "command" key: ⌘ on macOS, Ctrl on all other OSes.
1770                            // This ensures keyboard shortcuts work identically on every platform
1771                            // without separate branches in every handler.
1772                            let is_cmd = if cfg!(target_os = "macos") {
1773                                self.modifiers.super_key()
1774                            } else {
1775                                self.modifiers.control_key()
1776                            };
1777                            let is_shift = self.modifiers.shift_key();
1778
1779                            if is_cmd {
1780                                match code {
1781                                    // ── Undo / Redo ───────────────────────────────
1782                                    winit::keyboard::KeyCode::KeyZ => {
1783                                        if is_shift {
1784                                            log::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
1785                                            let mut redo_action = None;
1786                                            cvkg_core::update_system_state(|s| {
1787                                                let mut s = s.clone();
1788                                                redo_action = s.undo_manager.redo();
1789                                                s
1790                                            });
1791                                            if let Some(action) = redo_action {
1792                                                action();
1793                                            }
1794                                            state.window.request_redraw();
1795                                        } else {
1796                                            log::info!("[Native] Shortcut: Undo (Cmd+Z)");
1797                                            let mut undo_action = None;
1798                                            cvkg_core::update_system_state(|s| {
1799                                                let mut s = s.clone();
1800                                                undo_action = s.undo_manager.undo();
1801                                                s
1802                                            });
1803                                            if let Some(action) = undo_action {
1804                                                action();
1805                                            }
1806                                            state.window.request_redraw();
1807                                        }
1808                                    }
1809                                    // Ctrl+Y as alternative Redo on non-macOS
1810                                    winit::keyboard::KeyCode::KeyY
1811                                        if !cfg!(target_os = "macos") =>
1812                                    {
1813                                        log::info!("[Native] Shortcut: Redo (Ctrl+Y)");
1814                                        let mut redo_action = None;
1815                                        cvkg_core::update_system_state(|s| {
1816                                            let mut s = s.clone();
1817                                            redo_action = s.undo_manager.redo();
1818                                            s
1819                                        });
1820                                        if let Some(action) = redo_action {
1821                                            action();
1822                                        }
1823                                        state.window.request_redraw();
1824                                    }
1825                                    // ── File operations ───────────────────────────
1826                                    winit::keyboard::KeyCode::KeyN => {
1827                                        log::info!("[Native] Shortcut: New Window (Cmd+N)");
1828                                        create_new_window = true;
1829                                    }
1830                                    winit::keyboard::KeyCode::KeyO => {
1831                                        log::info!("[Native] Shortcut: Open File (Cmd+O)");
1832                                        if let Some(vdom) = &state.vdom {
1833                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1834                                                key: "cmd+o".to_string(),
1835                                                modifiers: cvkg_core::KeyModifiers::default(),
1836                                            });
1837                                        }
1838                                        state.window.request_redraw();
1839                                    }
1840                                    winit::keyboard::KeyCode::KeyS => {
1841                                        log::info!("[Native] Shortcut: Save (Cmd+S)");
1842                                        if let Some(vdom) = &state.vdom {
1843                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1844                                                key: "cmd+s".to_string(),
1845                                                modifiers: cvkg_core::KeyModifiers::default(),
1846                                            });
1847                                        }
1848                                        state.window.request_redraw();
1849                                    }
1850                                    winit::keyboard::KeyCode::KeyW => {
1851                                        log::info!("[Native] Shortcut: Close Window (Cmd+W)");
1852                                        close_window = true;
1853                                    }
1854                                    winit::keyboard::KeyCode::KeyQ => {
1855                                        log::info!("[Native] Shortcut: Quit (Cmd+Q)");
1856                                        // Defer closing all windows until after the state borrow ends.
1857                                        quit_all = true;
1858                                    }
1859                                    // ── Clipboard ────────────────────────────────
1860                                    winit::keyboard::KeyCode::KeyC => {
1861                                        log::info!("[Native] Shortcut: Copy (Cmd+C)");
1862                                        if let Some(vdom) = &state.vdom {
1863                                            vdom.dispatch_event(cvkg_core::Event::Copy);
1864                                        }
1865                                        state.window.request_redraw();
1866                                    }
1867                                    winit::keyboard::KeyCode::KeyV => {
1868                                        log::info!("[Native] Shortcut: Paste (Cmd+V)");
1869                                        // Read the system clipboard. Fall back to empty string on
1870                                        // error so the Paste event is always delivered to the VDOM.
1871                                        let text = arboard::Clipboard::new()
1872                                            .ok()
1873                                            .and_then(|mut cb| cb.get_text().ok())
1874                                            .unwrap_or_default();
1875                                        if let Some(vdom) = &state.vdom {
1876                                            vdom.dispatch_event(cvkg_core::Event::Paste(text));
1877                                        }
1878                                        state.window.request_redraw();
1879                                    }
1880                                    winit::keyboard::KeyCode::KeyX => {
1881                                        log::info!("[Native] Shortcut: Cut (Cmd+X)");
1882                                        if let Some(vdom) = &state.vdom {
1883                                            vdom.dispatch_event(cvkg_core::Event::Cut);
1884                                        }
1885                                        state.window.request_redraw();
1886                                    }
1887                                    // ── Fullscreen toggle ────────────────────────
1888                                    winit::keyboard::KeyCode::F11 => {
1889                                        let is_fullscreen = state.window.fullscreen().is_some();
1890                                        if is_fullscreen {
1891                                            state.window.set_fullscreen(None);
1892                                            log::info!("[Native] Fullscreen OFF");
1893                                        } else {
1894                                            if let Some(monitor) = state.window.current_monitor() {
1895                                                if let Some(mode) = monitor.video_modes().next() {
1896                                                    let w = mode.size().width;
1897                                                    let h = mode.size().height;
1898                                                    let rr = mode.refresh_rate_millihertz();
1899                                                    state.window.set_fullscreen(Some(winit::window::Fullscreen::Exclusive(mode)));
1900                                                    log::info!("[Native] Fullscreen ON (exclusive: {}x{}@{:?}Hz)", w, h, rr);
1901                                                }
1902                                            } else {
1903                                                state.window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
1904                                                log::info!("[Native] Fullscreen ON (borderless)");
1905                                            }
1906                                        }
1907                                        state.window.request_redraw();
1908                                    }
1909                                    // ── Selection / search ────────────────────────
1910                                    winit::keyboard::KeyCode::KeyA => {
1911                                        log::info!("[Native] Shortcut: Select All (Cmd+A)");
1912                                        if let Some(vdom) = &state.vdom {
1913                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1914                                                key: "cmd+a".to_string(),
1915                                                modifiers: cvkg_core::KeyModifiers::default(),
1916                                            });
1917                                        }
1918                                        state.window.request_redraw();
1919                                    }
1920                                    winit::keyboard::KeyCode::KeyF => {
1921                                        log::info!("[Native] Shortcut: Find (Cmd+F)");
1922                                        if let Some(vdom) = &state.vdom {
1923                                            vdom.dispatch_event(cvkg_core::Event::KeyDown {
1924                                                key: "cmd+f".to_string(),
1925                                                modifiers: cvkg_core::KeyModifiers::default(),
1926                                            });
1927                                        }
1928                                        state.window.request_redraw();
1929                                    }
1930                                    // ── Focus navigation: Tab / Shift+Tab ───────────────
1931                                    winit::keyboard::KeyCode::Tab => {
1932                                        if is_shift {
1933                                            if let Some(id) = state.focus_manager.focus_prev() {
1934                                                if let Ok(node_id) = id.as_str().parse::<u64>() {
1935                                                    state.focused_node_id = Some(cvkg_core::KvasirId(node_id));
1936                                                    log::info!("[Native] Focus previous: {:?}", node_id);
1937                                                }
1938                                            }
1939                                        } else {
1940                                            if let Some(id) = state.focus_manager.focus_next() {
1941                                                if let Ok(node_id) = id.as_str().parse::<u64>() {
1942                                                    state.focused_node_id = Some(cvkg_core::KvasirId(node_id));
1943                                                    log::info!("[Native] Focus next: {:?}", node_id);
1944                                                }
1945                                            }
1946                                        }
1947                                        state.window.request_redraw();
1948                                    }
1949                                    _ => {}
1950                                }
1951                            }
1952                        }
1953                    }
1954
1955                    if let Some(vdom) = &state.vdom
1956                        && let Some(cvkg_event) = convert_keyboard_event(event, &self.modifiers)
1957                    {
1958                        vdom.dispatch_event(cvkg_event);
1959                        state.window.request_redraw();
1960                    }
1961                }
1962
1963                WindowEvent::Ime(ime_event) => {
1964                    if let Some(vdom) = &state.vdom
1965                        && let Some(cvkg_event) = convert_ime_event(ime_event)
1966                    {
1967                        vdom.dispatch_event(cvkg_event);
1968                        state.window.request_redraw();
1969                    }
1970                }
1971                WindowEvent::ModifiersChanged(new_modifiers) => {
1972                    self.modifiers = new_modifiers.state();
1973                    let shift = self.modifiers.shift_key();
1974                    let ctrl = self.modifiers.control_key();
1975                    let alt = self.modifiers.alt_key();
1976                    let logo = self.modifiers.super_key();
1977                    cvkg_core::update_system_state(|st| {
1978                        let mut new_st = st.clone();
1979                        new_st.modifiers_shift = shift;
1980                        new_st.modifiers_ctrl = ctrl;
1981                        new_st.modifiers_alt = alt;
1982                        new_st.modifiers_logo = logo;
1983                        new_st
1984                    });
1985                }
1986                WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1987                    // Update the scale factor and request a redraw.
1988                    // The surface will be reconfigured on the next frame via the
1989                    // existing resize path in begin_frame.
1990                    let _ = scale_factor;
1991                    if let Some(ctx) = self.window_manager.windows.get(&id) {
1992                        ctx.window.request_redraw();
1993                    }
1994                }
1995                _ => {}
1996            }
1997        } // end of state block
1998
1999        if close_window {
2000            self.window_manager.close_window(id);
2001        }
2002        if quit_all {
2003            // Drain all windows; the is_empty check below will exit the event loop.
2004            for wid in self.window_manager.window_order().to_vec() {
2005                self.window_manager.close_window(wid);
2006            }
2007        }
2008        // Exit the event loop when all windows are closed (Cmd+W on last window, or Cmd+Q).
2009        if self.window_manager.windows.is_empty() {
2010            event_loop.exit();
2011        }
2012        if bring_to_front {
2013            self.window_manager.bring_to_front(id);
2014        }
2015        if create_new_window {
2016            self.window_manager.create_window(
2017                event_loop,
2018                &self.gpu,
2019                self.proxy.clone(),
2020                cvkg_core::WindowConfig {
2021                    title: "New CVKG Window".to_string(),
2022                    size: (800.0, 600.0),
2023                    ..Default::default()
2024                },
2025                false, // is_main
2026                &self.view,
2027            );
2028        }
2029    }
2030
2031    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
2032        match event {
2033            AppEvent::AccessibilityAction(request) => {
2034                let node_id = cvkg_core::KvasirId(request.target_node.0);
2035                let target_state = self.window_manager.windows.values_mut().find(|s| {
2036                    s.vdom
2037                        .as_ref()
2038                        .map_or(false, |v| v.nodes.contains_key(&node_id))
2039                });
2040
2041                if let Some(state) = target_state
2042                    && let Some(vdom) = &state.vdom
2043                    && let Some(node) = vdom.nodes.get(&node_id)
2044                    && request.action == accesskit::Action::Click
2045                {
2046                    let event = cvkg_core::Event::PointerClick {
2047                        x: node.layout.x + node.layout.width / 2.0,
2048                        y: node.layout.y + node.layout.height / 2.0,
2049                        button: 0, // Assume left click for accessibility actions
2050                        tilt: None,
2051                        azimuth: None,
2052                        pressure: Some(1.0),
2053                        barrel_rotation: None,
2054                        pointer_precision: 0.0,
2055                    };
2056                    vdom.dispatch_event(event);
2057                }
2058            }
2059            AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
2060                if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
2061                    if let Some(vdom) = &state.vdom {
2062                        let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
2063                        let mut nodes = Vec::new();
2064                        for (id, node) in &vdom.nodes {
2065                            nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
2066                        }
2067                        let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
2068                        if let Some(adapter) = &mut state.accesskit_adapter {
2069                            adapter.update_if_active(|| accesskit::TreeUpdate {
2070                                nodes,
2071                                tree: Some(tree),
2072                                focus: accesskit::NodeId(root_id),
2073                                tree_id: accesskit::TreeId::ROOT,
2074                            });
2075                        }
2076                    }
2077                }
2078            }
2079            AppEvent::CloseWindow(winit_id) => {
2080                self.window_manager.close_window(winit_id);
2081                if self.window_manager.windows.is_empty() {
2082                    event_loop.exit();
2083                }
2084            }
2085            AppEvent::SetTitle(winit_id, title) => {
2086                if let Some(data) = self.window_manager.windows.get(&winit_id) {
2087                    data.window.set_title(&title);
2088                }
2089            }
2090            AppEvent::SetSize(winit_id, width, height) => {
2091                if let Some(data) = self.window_manager.windows.get(&winit_id) {
2092                    let _ = data
2093                        .window
2094                        .request_inner_size(winit::dpi::LogicalSize::new(width, height));
2095                }
2096            }
2097            AppEvent::SetVisible(winit_id, visible) => {
2098                if let Some(data) = self.window_manager.windows.get(&winit_id) {
2099                    data.window.set_visible(visible);
2100                }
2101            }
2102            AppEvent::BringToFront(winit_id) => {
2103                self.window_manager.bring_to_front(winit_id);
2104            }
2105        }
2106    }
2107
2108    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2109        // Apply Rage Decay: rage naturally settles to 0 over time.
2110        self.rage = (self.rage - 0.02).max(0.0);
2111
2112        // Frame Throttling: 120FPS target (8.33ms). Heartbeat timer for idle wakeup only.
2113        // The primary render loop is driven by request_redraw() inside RedrawRequested.
2114        let now = std::time::Instant::now();
2115        let target_interval = std::time::Duration::from_micros(8_333); // 120fps
2116
2117        if now.duration_since(self.last_frame_time) >= target_interval {
2118            self.last_frame_time = now;
2119            // Only request redraw if the view has actually changed.
2120            // changed() returns true when rage, menu, or counters differ from last frame.
2121            // This avoids unnecessary GPU work for static frames (rage == 0, no interaction).
2122            let needs_redraw = self.view.changed();
2123            if needs_redraw {
2124                for window_state in self.window_manager.windows.values() {
2125                    window_state.window.request_redraw();
2126                }
2127            }
2128            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
2129                now + target_interval,
2130            ));
2131        } else {
2132            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
2133                self.last_frame_time + target_interval,
2134            ));
2135        }
2136    }
2137}
2138
2139impl cvkg_core::ElapsedTime for NativeRenderer {
2140    fn delta_time(&self) -> f32 {
2141        self.delta_time
2142    }
2143
2144    fn elapsed_time(&self) -> f32 {
2145        self.elapsed_time
2146    }
2147}
2148
2149impl cvkg_core::Renderer for NativeRenderer {
2150    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
2151        self.gpu_ref().fill_rect(rect, color);
2152    }
2153    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
2154        self.gpu_ref().fill_rounded_rect(rect, radius, color);
2155    }
2156    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
2157        self.gpu_ref().fill_ellipse(rect, color);
2158    }
2159    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
2160        self.gpu_ref().stroke_rect(rect, color, stroke_width);
2161    }
2162    fn stroke_rounded_rect(
2163        &mut self,
2164        rect: cvkg_core::Rect,
2165        radius: f32,
2166        color: [f32; 4],
2167        stroke_width: f32,
2168    ) {
2169        self.gpu_ref().stroke_rounded_rect(rect, radius, color, stroke_width);
2170    }
2171    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
2172        self.gpu_ref().stroke_ellipse(rect, color, stroke_width);
2173    }
2174    fn draw_line(
2175        &mut self,
2176        x1: f32,
2177        y1: f32,
2178        x2: f32,
2179        y2: f32,
2180        color: [f32; 4],
2181        stroke_width: f32,
2182    ) {
2183        self.gpu_ref()
2184            .draw_line(x1, y1, x2, y2, color, stroke_width);
2185    }
2186
2187    fn fill_glass_rect(&mut self, rect: cvkg_core::Rect, radius: f32, blur_radius: f32) {
2188        self.gpu_ref()
2189            .fill_glass_rect(rect, radius, blur_radius);
2190    }
2191
2192    fn fill_glass_rect_with_intensity(&mut self, rect: cvkg_core::Rect, radius: f32, blur_radius: f32, glass_intensity: f32) {
2193        self.gpu_ref()
2194            .fill_glass_rect_with_intensity(rect, radius, blur_radius, glass_intensity);
2195    }
2196
2197    fn fill_glass_rect_with_pressure(&mut self, rect: cvkg_core::Rect, radius: f32, blur_radius: f32, pressure: f32) {
2198        // Scale glass intensity by pressure: full pressure = full glass, no pressure = solid
2199        self.gpu_ref()
2200            .fill_glass_rect_with_intensity(rect, radius, blur_radius, pressure);
2201    }
2202
2203    fn fill_squircle(&mut self, rect: cvkg_core::Rect, n: f32, color: [f32; 4]) {
2204        self.gpu_ref()
2205            .fill_squircle(rect, n, color);
2206    }
2207
2208    fn stroke_squircle(&mut self, rect: cvkg_core::Rect, n: f32, color: [f32; 4], stroke_width: f32) {
2209        self.gpu_ref()
2210            .stroke_squircle(rect, n, color, stroke_width);
2211    }
2212
2213    fn draw_focus_ring(&mut self, rect: cvkg_core::Rect, radius: f32, offset: f32, width: f32, color: [f32; 4]) {
2214        self.gpu_ref()
2215            .draw_focus_ring(rect, radius, offset, width, color);
2216    }
2217
2218
2219    fn draw_linear_gradient(
2220        &mut self,
2221        rect: cvkg_core::Rect,
2222        start_color: [f32; 4],
2223        end_color: [f32; 4],
2224        angle: f32,
2225    ) {
2226        self.gpu_ref()
2227            .draw_linear_gradient(rect, start_color, end_color, angle);
2228    }
2229    fn draw_radial_gradient(
2230        &mut self,
2231        rect: cvkg_core::Rect,
2232        inner_color: [f32; 4],
2233        outer_color: [f32; 4],
2234    ) {
2235        self.gpu_ref()
2236            .draw_radial_gradient(rect, inner_color, outer_color);
2237    }
2238    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
2239        self.gpu_ref()
2240            .draw_texture(texture_id, rect);
2241    }
2242    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
2243        self.gpu_ref()
2244            .draw_image(image_name, rect);
2245    }
2246    fn load_image(&mut self, name: &str, data: &[u8]) {
2247        self.gpu_ref()
2248            .load_image(name, data);
2249    }
2250    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
2251        self.gpu_ref()
2252            .push_clip_rect(rect);
2253    }
2254    fn pop_clip_rect(&mut self) {
2255        self.gpu_ref()
2256            .pop_clip_rect();
2257    }
2258    fn push_opacity(&mut self, opacity: f32) {
2259        self.gpu_ref()
2260            .push_opacity(opacity);
2261    }
2262    fn draw_3d_cube(&mut self, rect: cvkg_core::Rect, color: [f32; 4], rotation: [f32; 3]) {
2263        self.gpu_ref()
2264            .draw_3d_cube(rect, color, rotation);
2265    }
2266    /// Render a 3D scene graph node using the GPU backend.
2267    ///
2268    /// # Contract
2269    /// Delegates to the locked GPU renderer instance to queue the 3D meshes for rendering.
2270    fn render_scene_node_3d(
2271        &mut self,
2272        position: [f32; 3],
2273        rotation: [f32; 4],
2274        scale: [f32; 3],
2275        color: [f32; 4],
2276        meshes: &[cvkg_core::Mesh],
2277    ) {
2278        self.gpu_ref()
2279            .render_scene_node_3d(position, rotation, scale, color, meshes);
2280    }
2281    fn pop_opacity(&mut self) {
2282        self.gpu_ref()
2283            .pop_opacity();
2284    }
2285    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
2286        self.gpu_ref()
2287            .bifrost(rect, blur, saturation, opacity);
2288    }
2289    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
2290        self.gpu_ref()
2291            .push_mjolnir_slice(angle, offset);
2292    }
2293    fn pop_mjolnir_slice(&mut self) {
2294        self.gpu_ref()
2295            .pop_mjolnir_slice();
2296    }
2297    fn mjolnir_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
2298        self.gpu_ref()
2299            .mjolnir_shatter(rect, pieces, force, color);
2300    }
2301    fn mjolnir_fluid_shatter(
2302        &mut self,
2303        rect: cvkg_core::Rect,
2304        pieces: u32,
2305        force: f32,
2306        color: [f32; 4],
2307    ) {
2308        self.gpu_ref()
2309            .mjolnir_fluid_shatter(rect, pieces, force, color);
2310    }
2311    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
2312        self.gpu_ref()
2313            .draw_mjolnir_bolt(from, to, color);
2314    }
2315    fn gungnir(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32, intensity: f32) {
2316        self.gpu_ref()
2317            .gungnir(rect, color, radius, intensity);
2318    }
2319    fn mani_glow(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32) {
2320        self.gpu_ref()
2321            .mani_glow(rect, color, radius);
2322    }
2323    fn register_handler(
2324        &mut self,
2325        event_type: &str,
2326        handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
2327    ) {
2328        self.gpu_ref()
2329            .register_handler(event_type, handler);
2330    }
2331    fn push_vnode(&mut self, rect: cvkg_core::Rect, name: &'static str) {
2332        self.gpu_ref()
2333            .push_vnode(rect, name);
2334    }
2335    fn pop_vnode(&mut self) {
2336        self.gpu_ref()
2337            .pop_vnode();
2338    }
2339    // FIX #1: Removed duplicate definitions of set_z_index and get_z_index.
2340    // They appeared twice in this impl block (after pop_vnode and after register_shared_element),
2341    // which is a hard compiler error. Exactly one definition of each is kept here.
2342    fn set_z_index(&mut self, z: f32) {
2343        self.gpu_ref()
2344            .set_z_index(z);
2345    }
2346    fn get_z_index(&self) -> f32 {
2347        self.gpu_ref_shared()
2348            .get_z_index()
2349    }
2350    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
2351        self.gpu_ref()
2352            .register_shared_element(id, rect);
2353    }
2354    fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
2355        self.gpu_ref()
2356            .set_material(material);
2357    }
2358    fn current_material(&self) -> cvkg_core::DrawMaterial {
2359        self.gpu_ref_shared()
2360            .current_material()
2361    }
2362    fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
2363        self.gpu_ref()
2364            .serialize_svg(name)
2365    }
2366    fn apply_svg_filter(
2367        &mut self,
2368        name: &str,
2369        filter_id: &str,
2370        region: cvkg_core::Rect,
2371    ) -> Result<String, String> {
2372        self.gpu_ref()
2373            .apply_svg_filter(name, filter_id, region)
2374    }
2375    fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
2376        self.gpu_ref()
2377            .push_shadow(radius, color, offset);
2378    }
2379    fn pop_shadow(&mut self) {
2380        self.gpu_ref()
2381            .pop_shadow();
2382    }
2383    fn push_affine(&mut self, transform: [f32; 6]) {
2384        self.gpu_ref()
2385            .push_affine(transform);
2386    }
2387    fn enter_portal(&mut self, z_index: i32) {
2388        // Portal layer rendering not yet supported in SurtrRenderer.
2389        // Content within portals renders inline as fallback.
2390        log::warn!(
2391            "Portal rendering (enter_portal) not yet implemented in GPU backend; z_index={}",
2392            z_index
2393        );
2394    }
2395    fn exit_portal(&mut self) {
2396        // Portal layer rendering not yet supported in SurtrRenderer.
2397        log::warn!("Portal rendering (exit_portal) not yet implemented in GPU backend");
2398    }
2399    fn viewport_size(&self) -> cvkg_core::Rect {
2400        let size = self.window.inner_size();
2401        let scale = self.window.scale_factor();
2402        let logical = size.to_logical::<f32>(scale);
2403        cvkg_core::Rect::new(0.0, 0.0, logical.width, logical.height)
2404    }
2405    fn announce(&mut self, message: &str, priority: cvkg_core::AnnouncementPriority) {
2406        // Delegate to AccessKit via the ShieldWall adapter if active.
2407        // For now, log the announcement. Full implementation requires
2408        // integration with the AccessKit tree update cycle.
2409        log::info!("Accessibility announcement [{:?}]: {}", priority, message);
2410    }
2411    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
2412        self.gpu_ref()
2413            .load_svg(name, svg_data);
2414    }
2415    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
2416        self.gpu_ref()
2417            .draw_svg(name, rect, None, 0);
2418    }
2419    fn draw_svg_with_offset(&mut self, name: &str, rect: cvkg_core::Rect, animation_time_offset: f32) {
2420        self.gpu_ref()
2421            .draw_svg_with_offset(name, rect, None, 0, animation_time_offset);
2422    }
2423    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
2424        self.gpu_ref_shared()
2425            .telemetry
2426            .clone()
2427    }
2428    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
2429        self.gpu_ref()
2430            .prewarm_vram(assets);
2431    }
2432
2433    /// Return the text scale factor of the GPU renderer.
2434    ///
2435    /// # Contract
2436    /// delegates to the locked GPU renderer instance to retrieve the correct scale factor.
2437    fn text_scale_factor(&self) -> f32 {
2438        self.gpu_ref_shared()
2439            .text_scale_factor()
2440    }
2441
2442    /// Return whether the current frame is over its time budget.
2443    ///
2444    /// # Contract
2445    /// delegates to the locked GPU renderer instance to check budget status.
2446    fn is_over_budget(&self) -> bool {
2447        self.gpu_ref_shared()
2448            .is_over_budget()
2449    }
2450
2451    /// Draws simple unformatted text at the specified coordinates.
2452    ///
2453    /// # Contract
2454    /// delegates to the locked GPU renderer instance to perform cached text rendering.
2455    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
2456        self.gpu_ref()
2457            .draw_text(text, x, y, size, color);
2458    }
2459
2460    /// Measures the dimensions of the text if rendered at the specified size.
2461    ///
2462    /// # Contract
2463    /// delegates to the locked GPU renderer instance to look up cached text dimensions.
2464    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2465        self.gpu_ref()
2466            .measure_text(text, size)
2467    }
2468
2469    /// Shapes a rich text layout with the specified font spans.
2470    ///
2471    /// # Contract
2472    /// delegates to the locked GPU renderer instance to perform text layout and shaping.
2473    fn shape_rich_text(
2474        &mut self,
2475        spans: &[runic_text::TextSpan],
2476        max_width: Option<f32>,
2477        align: runic_text::TextAlign,
2478        overflow: runic_text::TextOverflow,
2479    ) -> Option<runic_text::ShapedText> {
2480        self.gpu_ref()
2481            .shape_rich_text(spans, max_width, align, overflow)
2482    }
2483
2484    /// Draws a previously shaped text layout at the specified coordinates.
2485    ///
2486    /// # Contract
2487    /// delegates to the locked GPU renderer instance to emit glyph instances.
2488    fn draw_shaped_text(&mut self, shaped: &runic_text::ShapedText, x: f32, y: f32) {
2489        self.gpu_ref()
2490            .draw_shaped_text(shaped, x, y);
2491    }
2492
2493    /// Fills a rounded rectangle with glass material, custom tint color, and intensity.
2494    ///
2495    /// # Contract
2496    /// delegates to the locked GPU renderer instance to draw frosted glass panels.
2497    fn fill_glass_rect_with_tint(
2498        &mut self,
2499        rect: cvkg_core::Rect,
2500        radius: f32,
2501        blur_radius: f32,
2502        tint_color: [f32; 4],
2503        glass_intensity: f32,
2504    ) {
2505        self.gpu_ref()
2506            .fill_glass_rect_with_tint(rect, radius, blur_radius, tint_color, glass_intensity);
2507    }
2508
2509    /// Sets the color theme of the renderer.
2510    ///
2511    /// # Contract
2512    /// delegates to the locked GPU renderer instance to update global themes.
2513    fn set_theme(&mut self, theme: cvkg_core::ColorTheme) {
2514        self.gpu_ref()
2515            .set_theme(theme);
2516    }
2517
2518    /// Triggers a screen-shatter physics event at the specified origin.
2519    ///
2520    /// # Contract
2521    /// delegates to the locked GPU renderer instance to dispatch shatter compute effects.
2522    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
2523        self.gpu_ref()
2524            .trigger_shatter_event(origin, force);
2525    }
2526
2527    /// Sets the fireball light source position for specular glass highlights.
2528    ///
2529    /// # Contract
2530    /// delegates to the locked GPU renderer instance to update fireball coordinates.
2531    fn set_fireball_pos(&mut self, pos: [f32; 2]) {
2532        self.gpu_ref()
2533            .set_fireball_pos(pos);
2534    }
2535
2536    /// Sets the active scene preset by name.
2537    ///
2538    /// # Contract
2539    /// delegates to the locked GPU renderer instance to configure scene shaders.
2540    fn set_scene(&mut self, scene: &str) {
2541        self.gpu_ref()
2542            .set_scene(scene);
2543    }
2544
2545    /// Sets the active scene preset by ID.
2546    ///
2547    /// # Contract
2548    /// delegates to the locked GPU renderer instance to configure scene shaders.
2549    fn set_scene_preset(&mut self, preset: u32) {
2550        self.gpu_ref()
2551            .set_scene_preset(preset);
2552    }
2553
2554    /// Sets the default canvas background color.
2555    ///
2556    /// # Contract
2557    /// delegates to the locked GPU renderer instance to configure background clears.
2558    fn set_default_background_color(&mut self, color: [f32; 4]) {
2559        self.gpu_ref()
2560            .set_default_background_color(color);
2561    }
2562    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
2563        self.gpu_ref()
2564            .push_transform(translation, scale, rotation);
2565    }
2566    fn pop_transform(&mut self) {
2567        self.gpu_ref()
2568            .pop_transform();
2569    }
2570
2571    fn set_berserker_mode(&mut self, state: cvkg_core::BerserkerMode) {
2572        self.berserker_mode = state;
2573
2574        // Berserker Determinism: Apply OS-level scheduler priority hints for GodMode.
2575        // SAFETY: setpriority is a POSIX syscall. We pass PRIO_PROCESS with pid=0 (self).
2576        // Failure is silently ignored via let _ because insufficient permissions are expected
2577        // in unprivileged environments and must not crash the render loop.
2578        if state == cvkg_core::BerserkerMode::GodMode {
2579            log::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
2580            #[cfg(target_os = "linux")]
2581            unsafe {
2582                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
2583            }
2584        } else {
2585            #[cfg(target_os = "linux")]
2586            unsafe {
2587                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
2588            }
2589        }
2590
2591        self.gpu_ref()
2592            .set_berserker_mode(state);
2593    }
2594
2595    fn set_rage(&mut self, rage: f32) {
2596        self.rage = rage;
2597        self.gpu_ref()
2598            .set_rage(rage);
2599    }
2600
2601    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
2602        self.gpu_ref()
2603            .memoize(id, data_hash, render_fn);
2604    }
2605
2606    fn snapshot_render_state(&self) -> RenderStateSnapshot {
2607        self.gpu_ref_shared()
2608            .snapshot_render_state()
2609    }
2610
2611    fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
2612        self.gpu_ref()
2613            .restore_render_state(snap);
2614    }
2615    fn request_redraw(&mut self) {
2616        self.window.request_redraw();
2617    }
2618
2619    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
2620    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
2621    ///
2622    /// FIX #4: capture_frame() returns a Future that borrows the SurtrRenderer, so the
2623    /// MutexGuard must remain alive until block_on completes -- the guard cannot be dropped
2624    /// before the future is driven to completion. The lock is held for the duration of the
2625    /// GPU readback. This is acceptable because capture_png is an infrequent, explicit
2626    /// user-triggered operation (not called on the hot render path), so blocking other
2627    /// render calls for the readback duration is not a practical concern.
2628    fn capture_png(&mut self) -> Vec<u8> {
2629        log::info!("CAPTURING_FRAME: Initiating GPU readback...");
2630        // INVARIANT: The MutexGuard `gpu` must outlive the future returned by capture_frame()
2631        // because the future borrows from the SurtrRenderer. We therefore lock, block_on the
2632        // future (driving it to completion), and only then allow the guard to drop.
2633        let gpu = self.gpu.lock().unwrap_or_else(|p| p.into_inner());
2634        pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
2635            log::error!("GPU frame capture failed: {}", e);
2636            Vec::new() // Return empty buffer on failure -- do not panic the render loop
2637        })
2638    }
2639
2640    fn print(&mut self) {
2641        log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
2642        // In a production environment, this would interface with CUPS/GDI/AirPrint.
2643        // For the Ulfhednar prototype, we simulate the handshake.
2644        println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
2645    }
2646}
2647
2648// ── Native Menu Bar Builder ───────────────────────────────────────────
2649
2650// ── Event Conversion Helpers ───────────────────────────────────────────
2651
2652fn convert_keyboard_event(event: winit::event::KeyEvent, modifiers: &winit::keyboard::ModifiersState) -> Option<cvkg_core::Event> {
2653    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
2654        let key_str = format!("{:?}", code);
2655        let cvkg_mods = cvkg_core::KeyModifiers {
2656            shift: modifiers.shift_key(),
2657            ctrl: modifiers.control_key(),
2658            alt: modifiers.alt_key(),
2659            meta: modifiers.super_key(),
2660        };
2661        if event.state == winit::event::ElementState::Pressed {
2662            Some(cvkg_core::Event::KeyDown { key: key_str, modifiers: cvkg_mods })
2663        } else {
2664            Some(cvkg_core::Event::KeyUp { key: key_str, modifiers: cvkg_mods })
2665        }
2666    } else {
2667        None
2668    }
2669}
2670
2671fn convert_ime_event(event: winit::event::Ime) -> Option<cvkg_core::Event> {
2672    if let winit::event::Ime::Commit(string) = event {
2673        Some(cvkg_core::Event::Ime(string))
2674    } else {
2675        None
2676    }
2677}
2678
2679fn convert_mouse_event(
2680    state: winit::event::ElementState,
2681    position: [f32; 2],
2682    button: u32,
2683) -> cvkg_core::Event {
2684    match state {
2685        winit::event::ElementState::Pressed => cvkg_core::Event::PointerDown {
2686            x: position[0],
2687            y: position[1],
2688            button,
2689            proximity_field: 0.0,
2690            tilt: None,
2691            azimuth: None,
2692            pressure: Some(1.0),
2693            barrel_rotation: None,
2694            pointer_precision: 0.0,
2695        },
2696        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
2697            x: position[0],
2698            y: position[1],
2699            button,
2700            tilt: None,
2701            azimuth: None,
2702            pressure: Some(0.0),
2703            barrel_rotation: None,
2704            pointer_precision: 0.0,
2705        },
2706    }
2707}
2708
2709// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
2710
2711struct ShieldWall {
2712    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
2713}
2714
2715impl accesskit::ActionHandler for ShieldWall {
2716    fn do_action(&mut self, request: accesskit::ActionRequest) {
2717        let _ = self
2718            .proxy
2719            .send_event(AppEvent::AccessibilityAction(request));
2720    }
2721}
2722
2723impl accesskit::ActivationHandler for ShieldWall {
2724    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
2725        let mut root = accesskit::Node::new(accesskit::Role::Window);
2726        root.set_label("CVKG Application");
2727
2728        let root_id = accesskit::NodeId(1);
2729        Some(accesskit::TreeUpdate {
2730            nodes: vec![(root_id, root)],
2731            tree: Some(accesskit::Tree::new(root_id)),
2732            focus: root_id,
2733            tree_id: accesskit::TreeId::ROOT,
2734        })
2735    }
2736}
2737
2738impl accesskit::DeactivationHandler for ShieldWall {
2739    fn deactivate_accessibility(&mut self) {}
2740}
2741
2742type AssetCacheMap =
2743    std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
2744
2745/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
2746///
2747/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
2748/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
2749/// without blocking concurrent render-loop readers.
2750pub struct NativeAssetManager {
2751    cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
2752}
2753
2754impl Default for NativeAssetManager {
2755    fn default() -> Self {
2756        Self::new()
2757    }
2758}
2759
2760impl NativeAssetManager {
2761    /// Create a new, empty NativeAssetManager.
2762    pub fn new() -> Self {
2763        Self {
2764            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
2765                std::collections::HashMap::new(),
2766            )),
2767        }
2768    }
2769}
2770
2771impl cvkg_core::AssetManager for NativeAssetManager {
2772    /// Return the cached asset state for `url`.
2773    ///
2774    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
2775    /// Slow path (cache miss): atomically insert a Loading sentinel via `rcu()`,
2776    /// then spawn a background thread for I/O. The `rcu()` closure may execute
2777    /// more than once under contention, so `already_tracked` is determined by
2778    /// whether the closure actually inserted the Loading entry (detected by checking
2779    /// the returned map). This prevents duplicate I/O threads for the same URL.
2780    ///
2781    /// FIX #5: The previous implementation set `already_tracked` inside the `rcu`
2782    /// closure body, which is incorrect because `rcu` retries the closure on
2783    /// contention -- the bool would reflect only the last execution. The fix uses
2784    /// the fast-path check result plus the atomic `rcu` insertion to determine
2785    /// whether a thread must be spawned, making the logic correct under concurrency.
2786    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
2787        // Fast path: lock-free read from the current cache snapshot.
2788        if let Some(state) = self.cache.load().get(url) {
2789            return state.clone();
2790        }
2791
2792        let cache = self.cache.clone();
2793        let key = url.to_string();
2794
2795        // Slow path: atomically insert Loading if the key is absent.
2796        // `rcu` returns the final committed map; we inspect it to determine
2797        // whether *this* call was the one that inserted Loading (and thus
2798        // should spawn the I/O thread) versus a concurrent call that beat us.
2799        let mut we_inserted = false;
2800        self.cache.rcu(|map| {
2801            if map.contains_key(&key) {
2802                // Another caller already claimed this URL -- do not insert.
2803                (**map).clone()
2804            } else {
2805                we_inserted = true;
2806                let mut m = (**map).clone();
2807                m.insert(key.clone(), cvkg_core::AssetState::Loading);
2808                m
2809            }
2810        });
2811
2812        // Only the caller that performed the insertion spawns the I/O thread,
2813        // preventing duplicate concurrent reads for the same asset URL.
2814        if we_inserted {
2815            let cache_inner = cache.clone();
2816            let key_inner = key.clone();
2817
2818            std::thread::spawn(move || {
2819                log::debug!("[Native] Asynchronously loading asset: {}", key_inner);
2820                let result = match std::fs::read(&key_inner) {
2821                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2822                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2823                };
2824
2825                cache_inner.rcu(move |map| {
2826                    let mut m = (**map).clone();
2827                    m.insert(key_inner.clone(), result.clone());
2828                    m
2829                });
2830            });
2831        }
2832
2833        cvkg_core::AssetState::Loading
2834    }
2835
2836    /// Trigger a background load of `url` without waiting for the result.
2837    ///
2838    /// FIX #6: The previous implementation had a bare fast-path check followed
2839    /// by an unconditional thread spawn, allowing two concurrent calls for the
2840    /// same URL to both spawn I/O threads. Now uses the same rcu-based insertion
2841    /// guard as `load_image` to ensure exactly one thread is spawned per URL.
2842    fn preload_image(&self, url: &str) {
2843        // Fast path: if already in cache (any state), no work to do.
2844        if self.cache.load().contains_key(url) {
2845            return;
2846        }
2847
2848        let cache = self.cache.clone();
2849        let key = url.to_string();
2850
2851        let mut we_inserted = false;
2852        self.cache.rcu(|map| {
2853            if map.contains_key(&key) {
2854                (**map).clone()
2855            } else {
2856                we_inserted = true;
2857                let mut m = (**map).clone();
2858                m.insert(key.clone(), cvkg_core::AssetState::Loading);
2859                m
2860            }
2861        });
2862
2863        if we_inserted {
2864            std::thread::spawn(move || {
2865                log::debug!("[Native] Preloading asset: {}", key);
2866                let result = match std::fs::read(&key) {
2867                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2868                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2869                };
2870
2871                cache.rcu(move |map| {
2872                    let mut m = (**map).clone();
2873                    m.insert(key.clone(), result.clone());
2874                    m
2875                });
2876            });
2877        }
2878    }
2879}
2880
2881#[cfg(test)]
2882mod tests {
2883    use super::*;
2884    use cvkg_core::AssetManager;
2885    use cvkg_vdom::{AriaProps, LayoutRect, VDom, VNode};
2886    use std::collections::HashMap;
2887    use std::io::Write;
2888    use std::sync::{Arc, Mutex};
2889
2890    fn interactive_node(
2891        id: u64,
2892        component_type: &str,
2893        x: f32,
2894        y: f32,
2895        width: f32,
2896        height: f32,
2897        aria_role: &str,
2898    ) -> VNode {
2899        VNode {
2900            id: cvkg_core::KvasirId(id),
2901            key: None,
2902            component_type: component_type.to_string(),
2903            props: HashMap::new(),
2904            state: None,
2905            layout: LayoutRect {
2906                x,
2907                y,
2908                width,
2909                height,
2910            },
2911            children: Vec::new(),
2912            aria_role: aria_role.to_string(),
2913            aria_props: AriaProps::default(),
2914            portal_target: None,
2915            sdf_shape: Some(cvkg_core::layout::SdfShape::Rect(cvkg_core::Rect {
2916                x,
2917                y,
2918                width,
2919                height,
2920            })),
2921        }
2922    }
2923
2924    fn route_pointer_sequence_through_native_capture(
2925        pressed_vdom: &VDom,
2926        rebuilt_vdom: &VDom,
2927        x: f32,
2928        y: f32,
2929        button: u32,
2930    ) -> (
2931        cvkg_core::EventResponse,
2932        cvkg_core::EventResponse,
2933        cvkg_core::EventResponse,
2934    ) {
2935        let active_target = pressed_vdom.hit_test(x, y, 0.0).map(|(id, _)| id);
2936        let mut applied_vdom = VDom::new();
2937        applied_vdom.root = pressed_vdom.root;
2938        applied_vdom.nodes = pressed_vdom.nodes.clone();
2939        applied_vdom.parents = pressed_vdom.parents.clone();
2940        applied_vdom.event_handlers = pressed_vdom.event_handlers.clone();
2941        let down = active_target
2942            .map(|target| {
2943                applied_vdom.dispatch_event_to_target(
2944                    target,
2945                    cvkg_core::Event::PointerDown {
2946                        x,
2947                        y,
2948                        button,
2949                        proximity_field: 0.0,
2950                        tilt: None,
2951                        azimuth: None,
2952                        pressure: Some(1.0),
2953                        barrel_rotation: None,
2954                        pointer_precision: 0.0,
2955                    },
2956                )
2957            })
2958            .unwrap_or_else(|| {
2959                applied_vdom.dispatch_event(cvkg_core::Event::PointerDown {
2960                    x,
2961                    y,
2962                    button,
2963                    proximity_field: 0.0,
2964                    tilt: None,
2965                    azimuth: None,
2966                    pressure: Some(1.0),
2967                    barrel_rotation: None,
2968                    pointer_precision: 0.0,
2969                })
2970        });
2971
2972        applied_vdom.apply_patches(pressed_vdom.diff(rebuilt_vdom));
2973
2974        let fallback_target = applied_vdom.hit_test(x, y, 0.0).map(|(id, _)| id);
2975        let resolved_target = active_target
2976            .filter(|target| applied_vdom.nodes.contains_key(target))
2977            .or(fallback_target);
2978
2979        let pointer_up = cvkg_core::Event::PointerUp {
2980            x,
2981            y,
2982            button,
2983            tilt: None,
2984            azimuth: None,
2985            pressure: Some(0.0),
2986            barrel_rotation: None,
2987            pointer_precision: 0.0,
2988        };
2989        let pointer_click = cvkg_core::Event::PointerClick {
2990            x,
2991            y,
2992            button,
2993            tilt: None,
2994            azimuth: None,
2995            pressure: Some(0.0),
2996            barrel_rotation: None,
2997            pointer_precision: 0.0,
2998        };
2999
3000        let up = resolved_target
3001            .map(|target| applied_vdom.dispatch_event_to_target(target, pointer_up.clone()))
3002            .unwrap_or_else(|| applied_vdom.dispatch_event(pointer_up));
3003        let click = resolved_target
3004            .map(|target| applied_vdom.dispatch_event_to_target(target, pointer_click.clone()))
3005            .unwrap_or_else(|| applied_vdom.dispatch_event(pointer_click));
3006
3007        (down, up, click)
3008    }
3009
3010    /// FIX #12: Replaced hardcoded relative path "test_asset.png" with a temp-dir path
3011    /// constructed from a unique per-test name. The previous path was written to the
3012    /// process working directory, which varies by invocation and causes collisions when
3013    /// tests run in parallel or when a prior run panics before cleanup.
3014    #[test]
3015    fn test_native_asset_manager_loading() {
3016        let manager = NativeAssetManager::new();
3017        let temp_path = std::env::temp_dir().join("cvkg_test_asset_loading.png");
3018        let temp_file_path = temp_path.to_str().expect("temp path contains invalid UTF-8: OS temp directory is corrupted");
3019        let test_data = b"fake-image-data";
3020
3021        // Create a temporary file in the OS temp directory
3022        let mut file = std::fs::File::create(temp_file_path).unwrap();
3023        file.write_all(test_data).unwrap();
3024        drop(file);
3025
3026        // First call returns Loading and spawns the background I/O thread
3027        let mut state = manager.load_image(temp_file_path);
3028
3029        // Poll until Ready or timeout
3030        let start = std::time::Instant::now();
3031        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
3032            std::thread::sleep(std::time::Duration::from_millis(10));
3033            state = manager.load_image(temp_file_path);
3034        }
3035
3036        if let cvkg_core::AssetState::Ready(data) = state {
3037            assert_eq!(&*data, test_data);
3038        } else {
3039            let _ = std::fs::remove_file(temp_file_path);
3040            panic!("Expected Ready state, got {:?}", state);
3041        }
3042
3043        // Verify fast path returns Ready immediately from cache
3044        let state2 = manager.load_image(temp_file_path);
3045        if let cvkg_core::AssetState::Ready(data) = state2 {
3046            assert_eq!(&*data, test_data);
3047        } else {
3048            let _ = std::fs::remove_file(temp_file_path);
3049            panic!("Expected Ready state (cached), got {:?}", state2);
3050        }
3051
3052        let _ = std::fs::remove_file(temp_file_path);
3053    }
3054
3055    #[test]
3056    fn test_native_asset_manager_error() {
3057        let manager = NativeAssetManager::new();
3058        let path = "non_existent_file_cvkg_test.png";
3059        let mut state = manager.load_image(path);
3060
3061        let start = std::time::Instant::now();
3062        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
3063            std::thread::sleep(std::time::Duration::from_millis(10));
3064            state = manager.load_image(path);
3065        }
3066
3067        if let cvkg_core::AssetState::Error(_) = state {
3068            // Expected -- non-existent file must produce an Error state
3069        } else {
3070            panic!("Expected Error state, got {:?}", state);
3071        }
3072    }
3073
3074    #[test]
3075    fn test_event_conversion() {
3076        // Mouse press event
3077        let event = convert_mouse_event(winit::event::ElementState::Pressed, [10.0, 20.0], 0);
3078        if let cvkg_core::Event::PointerDown { x, y, button, .. } = event {
3079            assert_eq!(x, 10.0);
3080            assert_eq!(y, 20.0);
3081            assert_eq!(button, 0);
3082        } else {
3083            panic!("Expected PointerDown");
3084        }
3085
3086        // IME commit event
3087        let event = convert_ime_event(winit::event::Ime::Commit("hello".to_string()));
3088        if let Some(cvkg_core::Event::Ime(s)) = event {
3089            assert_eq!(s, "hello");
3090        } else {
3091            panic!("Expected Ime event");
3092        }
3093    }
3094
3095    #[test]
3096    fn native_pointer_capture_survives_rebuild_sequence() {
3097        let fired = Arc::new(Mutex::new(Vec::<&'static str>::new()));
3098
3099        let mut pressed = VDom::new();
3100        let root_id = cvkg_core::KvasirId(1);
3101        let button_id = cvkg_core::KvasirId(2);
3102        let mut root = interactive_node(1, "Root", 0.0, 0.0, 240.0, 240.0, "group");
3103        root.children = vec![button_id];
3104        let button = interactive_node(2, "Button", 20.0, 20.0, 80.0, 40.0, "button");
3105
3106        let fired_down = Arc::clone(&fired);
3107        let fired_up = Arc::clone(&fired);
3108        let fired_click = Arc::clone(&fired);
3109        pressed.event_handlers.insert(
3110            button_id,
3111            vec![
3112                (
3113                    "pointerdown".to_string(),
3114                    Arc::new(move |_| {
3115                        fired_down.lock().unwrap().push("down");
3116                    }) as _,
3117                ),
3118                (
3119                    "pointerup".to_string(),
3120                    Arc::new(move |_| {
3121                        fired_up.lock().unwrap().push("up");
3122                    }) as _,
3123                ),
3124                (
3125                    "pointerclick".to_string(),
3126                    Arc::new(move |_| {
3127                        fired_click.lock().unwrap().push("click");
3128                    }) as _,
3129                ),
3130            ]
3131            .into_iter()
3132            .collect(),
3133        );
3134        pressed.root = Some(root_id);
3135        pressed.nodes.insert(root_id, root);
3136        pressed.nodes.insert(button_id, button);
3137        pressed.parents.insert(button_id, root_id);
3138
3139        let mut rebuilt = VDom::new();
3140        let mut rebuilt_root = interactive_node(1, "Root", 0.0, 0.0, 240.0, 240.0, "group");
3141        rebuilt_root.children = vec![button_id];
3142        let rebuilt_button = interactive_node(2, "Button", 20.0, 20.0, 80.0, 40.0, "button");
3143        rebuilt.event_handlers = pressed.event_handlers.clone();
3144        rebuilt.root = Some(root_id);
3145        rebuilt.nodes.insert(root_id, rebuilt_root);
3146        rebuilt.nodes.insert(button_id, rebuilt_button);
3147        rebuilt.parents.insert(button_id, root_id);
3148
3149        let (down, up, click) =
3150            route_pointer_sequence_through_native_capture(&pressed, &rebuilt, 30.0, 30.0, 0);
3151
3152        assert_eq!(down, cvkg_core::EventResponse::Handled);
3153        assert_eq!(up, cvkg_core::EventResponse::Handled);
3154        assert_eq!(click, cvkg_core::EventResponse::Handled);
3155        assert_eq!(*fired.lock().unwrap(), vec!["down", "up", "click"]);
3156    }
3157
3158    #[test]
3159    fn native_pointer_capture_falls_back_to_rebuilt_target() {
3160        let fired = Arc::new(Mutex::new(Vec::<&'static str>::new()));
3161
3162        let mut pressed = VDom::new();
3163        let root_id = cvkg_core::KvasirId(1);
3164        let old_button_id = cvkg_core::KvasirId(2);
3165        let mut root = interactive_node(1, "Root", 0.0, 0.0, 240.0, 240.0, "group");
3166        root.children = vec![old_button_id];
3167        let button = interactive_node(2, "Button", 20.0, 20.0, 80.0, 40.0, "button");
3168
3169        let fired_down = Arc::clone(&fired);
3170        let fired_up = Arc::clone(&fired);
3171        let fired_click = Arc::clone(&fired);
3172        pressed.event_handlers.insert(
3173            old_button_id,
3174            vec![
3175                (
3176                    "pointerdown".to_string(),
3177                    Arc::new(move |_| {
3178                        fired_down.lock().unwrap().push("down");
3179                    }) as _,
3180                ),
3181                (
3182                    "pointerup".to_string(),
3183                    Arc::new(move |_| {
3184                        fired_up.lock().unwrap().push("up");
3185                    }) as _,
3186                ),
3187                (
3188                    "pointerclick".to_string(),
3189                    Arc::new(move |_| {
3190                        fired_click.lock().unwrap().push("click");
3191                    }) as _,
3192                ),
3193            ]
3194            .into_iter()
3195            .collect(),
3196        );
3197        pressed.root = Some(root_id);
3198        pressed.nodes.insert(root_id, root);
3199        pressed.nodes.insert(old_button_id, button);
3200        pressed.parents.insert(old_button_id, root_id);
3201
3202        let mut rebuilt = VDom::new();
3203        let mut rebuilt_root = interactive_node(1, "Root", 0.0, 0.0, 240.0, 240.0, "group");
3204        let rebuilt_button_id = cvkg_core::KvasirId(3);
3205        rebuilt_root.children = vec![rebuilt_button_id];
3206        let rebuilt_button = interactive_node(3, "Button", 20.0, 20.0, 80.0, 40.0, "button");
3207        rebuilt.event_handlers = pressed.event_handlers.clone();
3208        rebuilt.root = Some(root_id);
3209        rebuilt.nodes.insert(root_id, rebuilt_root);
3210        rebuilt.nodes.insert(rebuilt_button_id, rebuilt_button);
3211        rebuilt.parents.insert(rebuilt_button_id, root_id);
3212
3213        let (down, up, click) =
3214            route_pointer_sequence_through_native_capture(&pressed, &rebuilt, 30.0, 30.0, 0);
3215
3216        assert_eq!(down, cvkg_core::EventResponse::Handled);
3217        assert_eq!(up, cvkg_core::EventResponse::Handled);
3218        assert_eq!(click, cvkg_core::EventResponse::Handled);
3219        assert_eq!(*fired.lock().unwrap(), vec!["down", "up", "click"]);
3220    }
3221}
3222
3223/// load_icon -- Searches known asset directories for 'icon.png'.
3224/// Returns a winit Icon if found and decodable, None otherwise.
3225/// All failures are logged at warn level -- missing icons are non-fatal.
3226fn load_icon() -> Option<winit::window::Icon> {
3227    // FIX #13: Replaced unwrap_or_default() with unwrap_or_else that logs the failure.
3228    // unwrap_or_default() produced an empty PathBuf silently, making all subsequent
3229    // icon path lookups silently fail with no diagnostic output.
3230    let base = std::env::current_dir().unwrap_or_else(|e| {
3231        log::warn!(
3232            "[Native] Failed to get current directory for icon search: {}",
3233            e
3234        );
3235        std::path::PathBuf::new()
3236    });
3237
3238    let mut candidates = vec![
3239        base.join("icon.png"),
3240        base.join("crates/ulfhednar/icons/icon.png"),
3241        base.join("ulfhednar/icons/icon.png"),
3242        base.join("crates/ulfhednar/assets/icon.png"),
3243        base.join("ulfhednar/assets/icon.png"),
3244        base.join("assets/icon.png"),
3245    ];
3246
3247    // Also search relative to the executable directory
3248    if let Ok(exe_path) = std::env::current_exe()
3249        && let Some(exe_dir) = exe_path.parent()
3250    {
3251        candidates.push(exe_dir.join("icons/icon.png"));
3252        candidates.push(exe_dir.join("assets/icon.png"));
3253        candidates.push(exe_dir.join("icon.png"));
3254        if let Some(parent) = exe_dir.parent() {
3255            candidates.push(parent.join("icons/icon.png"));
3256            candidates.push(parent.join("assets/icon.png"));
3257            candidates.push(parent.join("icon.png"));
3258        }
3259    }
3260
3261    for path in candidates {
3262        if !path.exists() {
3263            log::debug!("[Native] Icon candidate not found: {:?}", path);
3264            continue;
3265        }
3266
3267        match image::open(&path) {
3268            Ok(img) => {
3269                let rgba = img.to_rgba8();
3270                let (width, height) = rgba.dimensions();
3271                match winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
3272                    Ok(icon) => {
3273                        log::info!("[Native] Successfully loaded app icon from: {:?}", path);
3274                        return Some(icon);
3275                    }
3276                    Err(e) => {
3277                        log::warn!("[Native] Icon format error at {:?}: {}", path, e);
3278                    }
3279                }
3280            }
3281            Err(e) => {
3282                log::warn!("[Native] Failed to open icon image at {:?}: {}", path, e);
3283            }
3284        }
3285    }
3286
3287    log::warn!(
3288        "[Native] Failed to find icon.png in any search path (CWD: {:?})",
3289        base
3290    );
3291    None
3292}
3293
3294// =============================================================================
3295// AUDIO / HAPTIC ENGINES -- Cross-platform micro-feedback
3296// =============================================================================
3297
3298/// Cross-platform audio engine using rodio for spatialized sound cues.
3299/// Uses rodio 0.21 API: OutputStreamBuilder::open_default_stream() returns
3300/// OutputStream directly. Playback via Sink::try_new(&stream.mixer()) + append.
3301pub struct RodioAudioEngine {
3302    _stream: rodio::OutputStream,
3303}
3304
3305// OutputStream is not Send+Sync on macOS due to CoreAudio, but we only use it
3306// from the main thread. The AudioEngine trait requires Send+Sync for use in
3307// App struct fields, which is safe here because we never move it across threads.
3308unsafe impl Send for RodioAudioEngine {}
3309unsafe impl Sync for RodioAudioEngine {}
3310
3311impl RodioAudioEngine {
3312    /// Create a new audio engine. Falls back to None if audio init fails.
3313    pub fn new() -> Option<Self> {
3314        match rodio::OutputStreamBuilder::open_default_stream() {
3315            Ok(stream) => {
3316                log::info!("[Native] Audio engine initialized (rodio)");
3317                Some(Self { _stream: stream })
3318            }
3319            Err(e) => {
3320                log::warn!("[Native] Audio init failed (no sound): {}", e);
3321                None
3322            }
3323        }
3324    }
3325}
3326
3327impl cvkg_core::AudioEngine for RodioAudioEngine {
3328    fn play_sound(&self, name: &str, volume: f32) {
3329        let data: &[u8] = match name {
3330            "nav_tick" => cvkg_core::sounds::NAVIGATION_TICK,
3331            "success_chime" => cvkg_core::sounds::SUCCESS_CHIME,
3332            "warning_tone" => cvkg_core::sounds::WARNING_TONE,
3333            _ => {
3334                log::warn!("[Native] Unknown sound: {}", name);
3335                return;
3336            }
3337        };
3338        self.play_buffer(data, volume);
3339    }
3340
3341    fn play_buffer(&self, data: &[u8], _volume: f32) {
3342        use std::io::Cursor;
3343        let cursor = Cursor::new(data.to_vec());
3344        let mixer = self._stream.mixer();
3345        match rodio::play(mixer, cursor) {
3346            Ok(_sink) => {}
3347            Err(e) => log::warn!("[Native] Audio play failed: {}", e),
3348        }
3349    }
3350
3351    fn play_spatial(&self, name: &str, _position: [f32; 3], volume: f32) {
3352        // Spatial audio: play sound without positional attenuation (OS-agnostic fallback)
3353        self.play_sound(name, volume);
3354    }
3355}
3356
3357/// Visual haptic engine that translates haptic requests into visual micro-animations.
3358/// Used as a cross-platform fallback where native haptics are unavailable.
3359pub struct VisualHapticEngine {
3360    last_impact: std::sync::Mutex<std::time::Instant>,
3361}
3362
3363impl Default for VisualHapticEngine {
3364    fn default() -> Self {
3365        Self::new()
3366    }
3367}
3368
3369impl VisualHapticEngine {
3370    pub fn new() -> Self {
3371        Self {
3372            last_impact: std::sync::Mutex::new(std::time::Instant::now()),
3373        }
3374    }
3375}
3376
3377impl cvkg_core::HapticEngine for VisualHapticEngine {
3378    fn impact(&self, intensity: cvkg_core::HapticIntensity) {
3379        let _ = intensity;
3380        *self.last_impact.lock().unwrap_or_else(|p| p.into_inner()) = std::time::Instant::now();
3381    }
3382    fn selection(&self) {
3383        self.impact(cvkg_core::HapticIntensity::Light);
3384    }
3385    fn success(&self) {
3386        self.impact(cvkg_core::HapticIntensity::Medium);
3387    }
3388    fn warning(&self) {
3389        self.impact(cvkg_core::HapticIntensity::Medium);
3390    }
3391    fn error(&self) {
3392        self.impact(cvkg_core::HapticIntensity::Heavy);
3393    }
3394    fn visual_tick(&self, _intensity: f32) {
3395        *self.last_impact.lock().unwrap_or_else(|p| p.into_inner()) = std::time::Instant::now();
3396    }
3397}
3398
3399// =============================================================================
3400// P1-46: Backend Translation Contracts
3401// =============================================================================
3402//
3403// Formalizes the translation contract between CVKG's scene graph and
3404// platform-native representations. Each widget type has a documented
3405/// mapping to platform APIs.
3406
3407/// Translation contract for a CVKG widget to its native representation.
3408#[derive(Debug, Clone)]
3409pub struct TranslationContract {
3410    /// CVKG widget type name.
3411    pub cvkg_type: &'static str,
3412    /// Platform-specific type name (e.g., "NSView", "HWND", "GTKWidget").
3413    pub platform_type: &'static str,
3414    /// Whether this widget uses native controls or custom rendering.
3415    pub rendering_mode: RenderingMode,
3416    /// Whether accessibility is handled natively.
3417    pub native_accessibility: bool,
3418}
3419
3420/// Rendering mode for a widget.
3421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3422pub enum RenderingMode {
3423    /// Use native platform controls (buttons, text fields, etc.).
3424    Native,
3425    /// Use CVKG's GPU renderer for custom-drawn content.
3426    Custom,
3427    /// Hybrid: native container with custom rendering inside.
3428    Hybrid,
3429}
3430
3431/// Registry of translation contracts for all widget types.
3432pub struct TranslationContractRegistry {
3433    contracts: Vec<TranslationContract>,
3434}
3435
3436impl TranslationContractRegistry {
3437    pub fn new() -> Self {
3438        Self {
3439            contracts: vec![
3440                TranslationContract {
3441                    cvkg_type: "Button",
3442                    platform_type: "NSButton/Button/GTKButton",
3443                    rendering_mode: RenderingMode::Native,
3444                    native_accessibility: true,
3445                },
3446                TranslationContract {
3447                    cvkg_type: "TextInput",
3448                    platform_type: "NSTextField/TextBox/GTKEntry",
3449                    rendering_mode: RenderingMode::Native,
3450                    native_accessibility: true,
3451                },
3452                TranslationContract {
3453                    cvkg_type: "Canvas",
3454                    platform_type: "NSView/HWND/GtkDrawingArea",
3455                    rendering_mode: RenderingMode::Custom,
3456                    native_accessibility: false,
3457                },
3458                TranslationContract {
3459                    cvkg_type: "TreeView",
3460                    platform_type: "NSTableView/TreeView/GTKTreeView",
3461                    rendering_mode: RenderingMode::Hybrid,
3462                    native_accessibility: true,
3463                },
3464            ],
3465        }
3466    }
3467
3468    /// Look up the contract for a CVKG widget type.
3469    pub fn find(&self, cvkg_type: &str) -> Option<&TranslationContract> {
3470        self.contracts.iter().find(|c| c.cvkg_type == cvkg_type)
3471    }
3472}
3473
3474impl Default for TranslationContractRegistry {
3475    fn default() -> Self {
3476        Self::new()
3477    }
3478}
3479
3480// =============================================================================
3481// P1-47: Window Management Contracts
3482// =============================================================================
3483
3484/// Window capability matrix per platform.
3485#[derive(Debug, Clone)]
3486pub struct WindowCapabilityMatrix {
3487    /// Platform name.
3488    pub platform: &'static str,
3489    /// Supported window types.
3490    pub window_types: Vec<WindowType>,
3491    /// Whether tabbed windows are supported.
3492    pub tabbed_windows: bool,
3493    /// Whether tiled windows are supported.
3494    pub tiled_windows: bool,
3495    /// Whether floating panels are supported.
3496    pub floating_panels: bool,
3497    /// Whether sheets/popovers are supported.
3498    pub sheets: bool,
3499}
3500
3501/// Window type.
3502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3503pub enum WindowType {
3504    Document,
3505    Panel,
3506    Popover,
3507    Dialog,
3508    Tooltip,
3509}
3510
3511impl WindowCapabilityMatrix {
3512    /// Get the capability matrix for the current platform.
3513    pub fn for_current_platform() -> Self {
3514        #[cfg(target_os = "macos")]
3515        return Self {
3516            platform: "macOS",
3517            window_types: vec![
3518                WindowType::Document,
3519                WindowType::Panel,
3520                WindowType::Popover,
3521                WindowType::Dialog,
3522                WindowType::Tooltip,
3523            ],
3524            tabbed_windows: true,
3525            tiled_windows: true,
3526            floating_panels: true,
3527            sheets: true,
3528        };
3529
3530        #[cfg(target_os = "windows")]
3531        return Self {
3532            platform: "Windows",
3533            window_types: vec![
3534                WindowType::Document,
3535                WindowType::Panel,
3536                WindowType::Dialog,
3537                WindowType::Tooltip,
3538            ],
3539            tabbed_windows: true,
3540            tiled_windows: true,
3541            floating_panels: true,
3542            sheets: false,
3543        };
3544
3545        #[cfg(target_os = "linux")]
3546        return Self {
3547            platform: "Linux",
3548            window_types: vec![
3549                WindowType::Document,
3550                WindowType::Panel,
3551                WindowType::Dialog,
3552                WindowType::Tooltip,
3553            ],
3554            tabbed_windows: false,
3555            tiled_windows: true,
3556            floating_panels: true,
3557            sheets: false,
3558        };
3559
3560        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
3561        return Self {
3562            platform: "Unknown",
3563            window_types: vec![WindowType::Document],
3564            tabbed_windows: false,
3565            tiled_windows: false,
3566            floating_panels: false,
3567            sheets: false,
3568        };
3569    }
3570}
3571
3572// =============================================================================
3573// P1-49: Widget State Synchronization
3574// =============================================================================
3575
3576/// Bidirectional state synchronization between CVKG and native widgets.
3577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3578pub enum SyncDirection {
3579    /// CVKG state drives native widget.
3580    CvkgToNative,
3581    /// Native widget state drives CVKG.
3582    NativeToCvkg,
3583    /// Both directions.
3584    Bidirectional,
3585}
3586
3587/// State synchronization contract for a widget.
3588#[derive(Debug, Clone)]
3589pub struct StateSyncContract {
3590    /// Widget type name.
3591    pub widget_type: &'static str,
3592    /// Synchronization direction.
3593    pub direction: SyncDirection,
3594    /// Whether to debounce rapid changes.
3595    pub debounce: bool,
3596    /// Debounce interval in milliseconds.
3597    pub debounce_ms: u64,
3598}
3599
3600/// Registry of state synchronization contracts.
3601pub struct StateSyncRegistry {
3602    contracts: Vec<StateSyncContract>,
3603}
3604
3605impl StateSyncRegistry {
3606    pub fn new() -> Self {
3607        Self {
3608            contracts: vec![
3609                StateSyncContract {
3610                    widget_type: "Button",
3611                    direction: SyncDirection::Bidirectional,
3612                    debounce: false,
3613                    debounce_ms: 0,
3614                },
3615                StateSyncContract {
3616                    widget_type: "TextInput",
3617                    direction: SyncDirection::Bidirectional,
3618                    debounce: true,
3619                    debounce_ms: 50,
3620                },
3621                StateSyncContract {
3622                    widget_type: "Slider",
3623                    direction: SyncDirection::Bidirectional,
3624                    debounce: true,
3625                    debounce_ms: 16,
3626                },
3627                StateSyncContract {
3628                    widget_type: "Checkbox",
3629                    direction: SyncDirection::Bidirectional,
3630                    debounce: false,
3631                    debounce_ms: 0,
3632                },
3633            ],
3634        }
3635    }
3636
3637    pub fn find(&self, widget_type: &str) -> Option<&StateSyncContract> {
3638        self.contracts.iter().find(|c| c.widget_type == widget_type)
3639    }
3640}
3641
3642impl Default for StateSyncRegistry {
3643    fn default() -> Self {
3644        Self::new()
3645    }
3646}
3647
3648// =============================================================================
3649// P1-51: Large UI Scalability (Native)
3650// =============================================================================
3651
3652/// Widget virtualization configuration for large UIs.
3653#[derive(Debug, Clone, Copy)]
3654pub struct WidgetVirtualizationConfig {
3655    /// Number of widgets to render outside the viewport (buffer).
3656    pub buffer_size: usize,
3657    /// Whether to recycle widget native handles.
3658    pub recycle_handles: bool,
3659    /// Maximum number of active native handles.
3660    pub max_active_handles: usize,
3661}
3662
3663impl Default for WidgetVirtualizationConfig {
3664    fn default() -> Self {
3665        Self {
3666            buffer_size: 5,
3667            recycle_handles: true,
3668            max_active_handles: 100,
3669        }
3670    }
3671}
3672
3673// =============================================================================
3674// P1-50: Semantic Role Mapping
3675// =============================================================================
3676
3677/// Explicit mapping from AccessKit/CVKG role to platform accessibility concepts:
3678/// macOS (AXRole), Windows (UIA ControlType), and Linux (ATK Role).
3679#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3680pub struct SemanticRoleMapping {
3681    /// The input AccessKit role.
3682    pub role: accesskit::Role,
3683    /// macOS AXRole string.
3684    pub mac_ax_role: &'static str,
3685    /// Windows UI Automation ControlType constant name or ID string.
3686    pub win_uia_control_type: &'static str,
3687    /// Linux ATK Role constant name or ID string.
3688    pub linux_atk_role: &'static str,
3689}
3690
3691/// Registry of semantic accessibility mappings.
3692pub struct SemanticRoleRegistry {
3693    mappings: Vec<SemanticRoleMapping>,
3694}
3695
3696impl SemanticRoleRegistry {
3697    pub fn new() -> Self {
3698        Self {
3699            mappings: vec![
3700                SemanticRoleMapping {
3701                    role: accesskit::Role::Button,
3702                    mac_ax_role: "AXButton",
3703                    win_uia_control_type: "UIA_ButtonControlTypeId",
3704                    linux_atk_role: "ATK_ROLE_PUSH_BUTTON",
3705                },
3706                SemanticRoleMapping {
3707                    role: accesskit::Role::TextInput,
3708                    mac_ax_role: "AXTextField",
3709                    win_uia_control_type: "UIA_EditControlTypeId",
3710                    linux_atk_role: "ATK_ROLE_ENTRY",
3711                },
3712                SemanticRoleMapping {
3713                    role: accesskit::Role::CheckBox,
3714                    mac_ax_role: "AXCheckBox",
3715                    win_uia_control_type: "UIA_CheckBoxControlTypeId",
3716                    linux_atk_role: "ATK_ROLE_CHECK_BOX",
3717                },
3718                SemanticRoleMapping {
3719                    role: accesskit::Role::Slider,
3720                    mac_ax_role: "AXSlider",
3721                    win_uia_control_type: "UIA_SliderControlTypeId",
3722                    linux_atk_role: "ATK_ROLE_SLIDER",
3723                },
3724                SemanticRoleMapping {
3725                    role: accesskit::Role::Label,
3726                    mac_ax_role: "AXStaticText",
3727                    win_uia_control_type: "UIA_TextControlTypeId",
3728                    linux_atk_role: "ATK_ROLE_LABEL",
3729                },
3730            ],
3731        }
3732    }
3733
3734    /// Look up the platform mappings for a given role.
3735    pub fn find(&self, role: accesskit::Role) -> Option<&SemanticRoleMapping> {
3736        self.mappings.iter().find(|m| m.role == role)
3737    }
3738}
3739
3740impl Default for SemanticRoleRegistry {
3741    fn default() -> Self {
3742        Self::new()
3743    }
3744}
3745
3746
3747// =============================================================================
3748// P2-39: Multi-Monitor Support
3749// =============================================================================
3750
3751/// P2-39: Multi-monitor support contract config for mixed DPI and refresh rates.
3752#[derive(Debug, Clone)]
3753pub struct MonitorConfig {
3754    /// Friendly name of the monitor (e.g. "Primary", "External").
3755    pub name: String,
3756    /// Spatial origin position in physical coordinates.
3757    pub position: (i32, i32),
3758    /// Size in physical pixels.
3759    pub size: (u32, u32),
3760    /// DPI scaling factor.
3761    pub scale_factor: f64,
3762    /// Refresh rate in Hz.
3763    pub refresh_rate: u32,
3764}
3765
3766/// P2-39: Manages multi-monitor layouts, tracking scale factor updates
3767/// and DPI changes during transitions.
3768#[derive(Debug, Clone)]
3769pub struct MultiMonitorManager {
3770    monitors: Vec<MonitorConfig>,
3771    current_monitor_index: usize,
3772}
3773
3774impl MultiMonitorManager {
3775    /// Creates a new `MultiMonitorManager` with a set of displays.
3776    ///
3777    /// # Arguments
3778    /// * `monitors` - The list of active monitors. Must contain at least one monitor.
3779    ///
3780    /// # Contract
3781    /// If the list of monitors is empty, a default 1080p, 60Hz, 1.0x scale monitor is added.
3782    pub fn new(mut monitors: Vec<MonitorConfig>) -> Self {
3783        if monitors.is_empty() {
3784            monitors.push(MonitorConfig {
3785                name: "Default".to_string(),
3786                position: (0, 0),
3787                size: (1920, 1080),
3788                scale_factor: 1.0,
3789                refresh_rate: 60,
3790            });
3791        }
3792        Self {
3793            monitors,
3794            current_monitor_index: 0,
3795        }
3796    }
3797
3798    /// Returns the currently active monitor configuration.
3799    pub fn current_monitor(&self) -> &MonitorConfig {
3800        &self.monitors[self.current_monitor_index]
3801    }
3802
3803    /// Returns all registered monitor configurations.
3804    pub fn monitors(&self) -> &[MonitorConfig] {
3805        &self.monitors
3806    }
3807
3808    /// Determines which monitor a window is on based on its center coordinate.
3809    ///
3810    /// # Arguments
3811    /// * `window_rect` - The spatial bounds of the window represented as `(x, y, width, height)` in physical coordinates.
3812    ///
3813    /// # Contract
3814    /// Selects the monitor that contains the center point of the window. If the center point
3815    /// is outside all monitors, defaults to the closest monitor or the current one.
3816    pub fn update_window_position(&mut self, window_rect: (i32, i32, u32, u32)) -> Option<usize> {
3817        let center_x = window_rect.0 + (window_rect.2 as i32 / 2);
3818        let center_y = window_rect.1 + (window_rect.3 as i32 / 2);
3819
3820        let mut best_index = None;
3821        let mut min_distance = f64::MAX;
3822
3823        for (i, m) in self.monitors.iter().enumerate() {
3824            let left = m.position.0;
3825            let right = m.position.0 + m.size.0 as i32;
3826            let top = m.position.1;
3827            let bottom = m.position.1 + m.size.1 as i32;
3828
3829            if center_x >= left && center_x < right && center_y >= top && center_y < bottom {
3830                self.current_monitor_index = i;
3831                return Some(i);
3832            }
3833
3834            // Calculate distance to center of monitor
3835            let m_center_x = m.position.0 + (m.size.0 as i32 / 2);
3836            let m_center_y = m.position.1 + (m.size.1 as i32 / 2);
3837            let dx = (center_x - m_center_x) as f64;
3838            let dy = (center_y - m_center_y) as f64;
3839            let dist = (dx * dx + dy * dy).sqrt();
3840            if dist < min_distance {
3841                min_distance = dist;
3842                best_index = Some(i);
3843            }
3844        }
3845
3846        if let Some(i) = best_index {
3847            self.current_monitor_index = i;
3848            Some(i)
3849        } else {
3850            None
3851        }
3852    }
3853
3854    /// Dynamically scales logical dimensions to physical dimensions using the active monitor's scale factor.
3855    ///
3856    /// # Arguments
3857    /// * `logical_width` - The logical width to scale.
3858    /// * `logical_height` - The logical height to scale.
3859    ///
3860    /// # Returns
3861    /// The physical dimensions as `(u32, u32)`.
3862    pub fn scale_dimensions(&self, logical_width: f64, logical_height: f64) -> (u32, u32) {
3863        let sf = self.current_monitor().scale_factor;
3864        (
3865            (logical_width * sf).round() as u32,
3866            (logical_height * sf).round() as u32,
3867        )
3868    }
3869
3870    /// Checks if moving between monitors requires a DPI scaling recalculation.
3871    ///
3872    /// # Arguments
3873    /// * `from_index` - The source monitor index.
3874    /// * `to_index` - The target monitor index.
3875    pub fn requires_dpi_adaptation(&self, from_index: usize, to_index: usize) -> bool {
3876        if from_index < self.monitors.len() && to_index < self.monitors.len() {
3877            (self.monitors[from_index].scale_factor - self.monitors[to_index].scale_factor).abs() > f64::EPSILON
3878        } else {
3879            false
3880        }
3881    }
3882}
3883
3884// =============================================================================
3885// P2-40: Visual Regression Tracker
3886// =============================================================================
3887
3888/// P2-40: Native Visual Regression Testing infrastructure.
3889/// Captures and compares frames to detect platform-specific visual differences.
3890#[derive(Debug, Clone)]
3891pub struct VisualRegressionTracker {
3892    /// Path to directory where reference "golden" images are located.
3893    reference_dir: std::path::PathBuf,
3894    /// Absolute threshold difference tolerance per pixel component (0 to 255).
3895    pixel_tolerance: u8,
3896    /// Percentage threshold of allowed mismatched pixels (0.0 to 100.0).
3897    max_mismatched_percentage: f64,
3898}
3899
3900impl VisualRegressionTracker {
3901    /// Creates a new `VisualRegressionTracker` with specified reference folder and tolerances.
3902    pub fn new(reference_dir: std::path::PathBuf, pixel_tolerance: u8, max_mismatched_percentage: f64) -> Self {
3903        Self {
3904            reference_dir,
3905            pixel_tolerance,
3906            max_mismatched_percentage,
3907        }
3908    }
3909
3910    /// Compares a captured PNG byte buffer against a named golden reference file.
3911    ///
3912    /// # Arguments
3913    /// * `test_name` - The identifier of the visual test (e.g. "primary_window_layout").
3914    /// * `captured_png` - The raw bytes of the PNG-encoded frame capture.
3915    ///
3916    /// # Returns
3917    /// `true` if the captured image matches the reference image within tolerances,
3918    /// `false` if they mismatch or if the reference image cannot be found/decoded.
3919    ///
3920    /// # Contract
3921    /// If the reference image file does not exist, this function writes the captured PNG
3922    /// as the new reference (acting in recording mode) and returns `true`.
3923    pub fn verify_frame(&self, test_name: &str, captured_png: &[u8]) -> bool {
3924        let reference_path = self.reference_dir.join(format!("{}.png", test_name));
3925        if !reference_path.exists() {
3926            log::info!("Golden reference for '{}' not found. Recording current capture as reference.", test_name);
3927            if let Some(parent) = reference_path.parent() {
3928                let _ = std::fs::create_dir_all(parent);
3929            }
3930            if let Err(e) = std::fs::write(&reference_path, captured_png) {
3931                log::error!("Failed to write golden image: {}", e);
3932                return false;
3933            }
3934            return true;
3935        }
3936
3937        // Load reference image
3938        let ref_img = match image::load_from_memory(&std::fs::read(&reference_path).unwrap_or_default()) {
3939            Ok(img) => img.to_rgba8(),
3940            Err(e) => {
3941                log::error!("Failed to decode reference image: {}", e);
3942                return false;
3943            }
3944        };
3945
3946        // Load captured image
3947        let cap_img = match image::load_from_memory(captured_png) {
3948            Ok(img) => img.to_rgba8(),
3949            Err(e) => {
3950                log::error!("Failed to decode captured image: {}", e);
3951                return false;
3952            }
3953        };
3954
3955        if ref_img.dimensions() != cap_img.dimensions() {
3956            log::warn!("Dimensions mismatch for test '{}': ref {:?}, cap {:?}", test_name, ref_img.dimensions(), cap_img.dimensions());
3957            return false;
3958        }
3959
3960        let (width, height) = ref_img.dimensions();
3961        let total_pixels = width as f64 * height as f64;
3962        let mut mismatched_pixels = 0;
3963
3964        for (x, y, ref_pixel) in ref_img.enumerate_pixels() {
3965            let cap_pixel = cap_img.get_pixel(x, y);
3966            let mut pixel_differs = false;
3967            for c in 0..4 {
3968                let diff = (ref_pixel[c] as i16 - cap_pixel[c] as i16).abs();
3969                if diff > self.pixel_tolerance as i16 {
3970                    pixel_differs = true;
3971                    break;
3972                }
3973            }
3974            if pixel_differs {
3975                mismatched_pixels += 1;
3976            }
3977        }
3978
3979        let mismatch_pct = (mismatched_pixels as f64 / total_pixels) * 100.0;
3980        if mismatch_pct > self.max_mismatched_percentage {
3981            log::warn!("Visual regression detected in test '{}': {:.2}% mismatched pixels (max allowed {:.2}%)",
3982                test_name, mismatch_pct, self.max_mismatched_percentage);
3983            false
3984        } else {
3985            true
3986        }
3987    }
3988}
3989
3990#[cfg(test)]
3991mod p1_46_47_49_51_tests {
3992    use super::*;
3993
3994    // P2-39: Multi-monitor tests
3995    #[test]
3996    fn test_multi_monitor_manager_basics() {
3997        let m1 = MonitorConfig {
3998            name: "Display 1".to_string(),
3999            position: (0, 0),
4000            size: (1920, 1080),
4001            scale_factor: 1.0,
4002            refresh_rate: 60,
4003        };
4004        let m2 = MonitorConfig {
4005            name: "Display 2".to_string(),
4006            position: (1920, 0),
4007            size: (3840, 2160),
4008            scale_factor: 2.0,
4009            refresh_rate: 120,
4010        };
4011
4012        let mut manager = MultiMonitorManager::new(vec![m1, m2]);
4013        assert_eq!(manager.monitors().len(), 2);
4014        assert_eq!(manager.current_monitor().name, "Display 1");
4015
4016        // Scale dimensions logical to physical
4017        let scaled = manager.scale_dimensions(100.0, 200.0);
4018        assert_eq!(scaled, (100, 200));
4019
4020        // Shift window to second monitor (centered on second monitor)
4021        let idx = manager.update_window_position((1920 + 100, 100, 1000, 1000));
4022        assert_eq!(idx, Some(1));
4023        assert_eq!(manager.current_monitor().name, "Display 2");
4024
4025        let scaled_m2 = manager.scale_dimensions(100.0, 200.0);
4026        assert_eq!(scaled_m2, (200, 400));
4027
4028        // Check DPI adaptation trigger
4029        assert!(manager.requires_dpi_adaptation(0, 1));
4030        assert!(!manager.requires_dpi_adaptation(0, 0));
4031    }
4032
4033    // P2-40: Visual regression tests
4034    #[test]
4035    fn test_visual_regression_tracker_comparison() {
4036        // Create simple mock raw images using image crate
4037        use image::{RgbaImage, ImageFormat};
4038        use std::io::Cursor;
4039
4040        let mut img1 = RgbaImage::new(10, 10);
4041        for p in img1.pixels_mut() {
4042            *p = image::Rgba([255, 0, 0, 255]);
4043        }
4044        let mut png1 = Vec::new();
4045        img1.write_to(&mut Cursor::new(&mut png1), ImageFormat::Png).unwrap();
4046
4047        // Exact match
4048        let temp_dir = std::env::temp_dir().join("cvkg_visual_regression_tests");
4049        let tracker = VisualRegressionTracker::new(temp_dir.clone(), 5, 1.0);
4050
4051        // Recording mode: first call records png1 as the golden reference
4052        let matched = tracker.verify_frame("test_red_rect", &png1);
4053        assert!(matched);
4054
4055        // Second call matches against recorded reference
4056        let matched_again = tracker.verify_frame("test_red_rect", &png1);
4057        assert!(matched_again);
4058
4059        // Slightly different image (within tolerances)
4060        let mut img2 = RgbaImage::new(10, 10);
4061        for (i, p) in img2.pixels_mut().enumerate() {
4062            if i == 0 {
4063                // One pixel slightly off, but within tolerance
4064                *p = image::Rgba([253, 0, 0, 255]);
4065            } else {
4066                *p = image::Rgba([255, 0, 0, 255]);
4067            }
4068        }
4069        let mut png2 = Vec::new();
4070        img2.write_to(&mut Cursor::new(&mut png2), ImageFormat::Png).unwrap();
4071
4072        let matched_tolerated = tracker.verify_frame("test_red_rect", &png2);
4073        assert!(matched_tolerated);
4074
4075        // Very different image (out of tolerances)
4076        let mut img3 = RgbaImage::new(10, 10);
4077        for p in img3.pixels_mut() {
4078            *p = image::Rgba([0, 255, 0, 255]); // Green instead of Red
4079        }
4080        let mut png3 = Vec::new();
4081        img3.write_to(&mut Cursor::new(&mut png3), ImageFormat::Png).unwrap();
4082
4083        let matched_fail = tracker.verify_frame("test_red_rect", &png3);
4084        assert!(!matched_fail);
4085
4086        // Clean up
4087        let _ = std::fs::remove_file(temp_dir.join("test_red_rect.png"));
4088    }
4089
4090    // P1-46: Translation contracts
4091    #[test]
4092    fn translation_contract_registry_has_defaults() {
4093        let reg = TranslationContractRegistry::new();
4094        assert!(reg.find("Button").is_some());
4095        assert!(reg.find("Canvas").is_some());
4096        assert!(reg.find("Unknown").is_none());
4097    }
4098
4099    #[test]
4100    fn button_uses_native_rendering() {
4101        let reg = TranslationContractRegistry::new();
4102        let contract = reg.find("Button").unwrap();
4103        assert_eq!(contract.rendering_mode, RenderingMode::Native);
4104        assert!(contract.native_accessibility);
4105    }
4106
4107    #[test]
4108    fn canvas_uses_custom_rendering() {
4109        let reg = TranslationContractRegistry::new();
4110        let contract = reg.find("Canvas").unwrap();
4111        assert_eq!(contract.rendering_mode, RenderingMode::Custom);
4112    }
4113
4114    // P1-47: Window capabilities
4115    #[test]
4116    fn window_capability_matrix_has_platform() {
4117        let matrix = WindowCapabilityMatrix::for_current_platform();
4118        assert!(!matrix.platform.is_empty());
4119        assert!(!matrix.window_types.is_empty());
4120    }
4121
4122    #[test]
4123    fn macos_has_sheets() {
4124        #[cfg(target_os = "macos")]
4125        {
4126            let matrix = WindowCapabilityMatrix::for_current_platform();
4127            assert!(matrix.sheets);
4128            assert!(matrix.tabbed_windows);
4129        }
4130    }
4131
4132    // P1-49: State sync
4133    #[test]
4134    fn state_sync_registry_has_defaults() {
4135        let reg = StateSyncRegistry::new();
4136        assert!(reg.find("Button").is_some());
4137        assert!(reg.find("TextInput").is_some());
4138    }
4139
4140    #[test]
4141    fn text_input_has_debounce() {
4142        let reg = StateSyncRegistry::new();
4143        let contract = reg.find("TextInput").unwrap();
4144        assert!(contract.debounce);
4145        assert_eq!(contract.debounce_ms, 50);
4146    }
4147
4148    #[test]
4149    fn button_is_bidirectional() {
4150        let reg = StateSyncRegistry::new();
4151        let contract = reg.find("Button").unwrap();
4152        assert_eq!(contract.direction, SyncDirection::Bidirectional);
4153    }
4154
4155    // P1-51: Widget virtualization
4156    #[test]
4157    fn default_virtualization_config() {
4158        let config = WidgetVirtualizationConfig::default();
4159        assert_eq!(config.buffer_size, 5);
4160        assert!(config.recycle_handles);
4161        assert_eq!(config.max_active_handles, 100);
4162    }
4163
4164    // P1-50: Semantic Role Mapping
4165    #[test]
4166    fn semantic_role_registry_has_button_and_text() {
4167        let reg = SemanticRoleRegistry::new();
4168        let button = reg.find(accesskit::Role::Button).unwrap();
4169        assert_eq!(button.mac_ax_role, "AXButton");
4170        assert_eq!(button.win_uia_control_type, "UIA_ButtonControlTypeId");
4171        assert_eq!(button.linux_atk_role, "ATK_ROLE_PUSH_BUTTON");
4172
4173        let text = reg.find(accesskit::Role::TextInput).unwrap();
4174        assert_eq!(text.mac_ax_role, "AXTextField");
4175    }
4176
4177
4178    // =========================================================================
4179    // P2-3: Mutex Poison Recovery Tests
4180    // =========================================================================
4181    // These tests verify that mutex poison is handled gracefully via
4182    // unwrap_or_else(|p| p.into_inner()) instead of panicking.
4183
4184    use std::sync::{Arc, Mutex};
4185    use std::thread;
4186
4187    /// Test that a poisoned mutex can be recovered via unwrap_or_else.
4188    /// This simulates what happens when a thread panics while holding the GPU lock.
4189    #[test]
4190    fn mutex_poison_recovery_via_unwrap_or_else() {
4191        let mutex = Arc::new(Mutex::new(42u32));
4192        let mutex_clone = Arc::clone(&mutex);
4193
4194        // Spawn a thread that panics while holding the lock
4195        let handle = thread::spawn(move || {
4196            let _guard = mutex_clone.lock().unwrap();
4197            panic!("simulated thread panic while holding lock");
4198        });
4199
4200        // Wait for the thread to panic
4201        let _ = handle.join();
4202
4203        // The mutex is now poisoned - but we can still recover the data
4204        let value = mutex.lock().unwrap_or_else(|p| p.into_inner());
4205        assert_eq!(*value, 42, "poisoned mutex should still yield the inner value");
4206    }
4207
4208    /// Test that multiple poison recoveries work correctly.
4209    #[test]
4210    fn mutex_poison_recovery_multiple_times() {
4211        let mutex = Arc::new(Mutex::new(String::from("hello")));
4212
4213        for i in 0..5 {
4214            let m = Arc::clone(&mutex);
4215            let handle = thread::spawn(move || {
4216                let _guard = m.lock().unwrap();
4217                panic!("panic iteration {}", i);
4218            });
4219            let _ = handle.join();
4220        }
4221
4222        // After 5 poison events, we can still recover
4223        let value = mutex.lock().unwrap_or_else(|p| p.into_inner());
4224        assert_eq!(*value, "hello");
4225    }
4226
4227    /// Test that the GPU mutex pattern used in NativeRenderer works correctly.
4228    /// This validates the pattern: self.gpu.lock().unwrap_or_else(|p| p.into_inner())
4229    #[test]
4230    fn gpu_mutex_poison_pattern() {
4231        let gpu = Arc::new(Mutex::new(RendererState { frame_count: 0 }));
4232        let gpu_clone = Arc::clone(&gpu);
4233
4234        // Simulate a render call that panics mid-frame
4235        let handle = thread::spawn(move || {
4236            let mut state = gpu_clone.lock().unwrap();
4237            state.frame_count += 1;
4238            panic!("GPU render panic");
4239        });
4240
4241        let _ = handle.join();
4242
4243        // The NativeRenderer pattern should recover gracefully
4244        let mut state = gpu.lock().unwrap_or_else(|p| p.into_inner());
4245        assert_eq!(state.frame_count, 1);
4246        // Can continue using the renderer after poison recovery
4247        state.frame_count += 1;
4248        assert_eq!(state.frame_count, 2);
4249    }
4250
4251    /// Test that poison recovery doesn't lose data integrity.
4252    #[test]
4253    fn poison_recovery_preserves_data_integrity() {
4254        let data = Arc::new(Mutex::new(vec![1, 2, 3, 4, 5]));
4255        let data_clone = Arc::clone(&data);
4256
4257        let handle = thread::spawn(move || {
4258            let mut guard = data_clone.lock().unwrap();
4259            guard.push(6);
4260            panic!("mid-mutation panic");
4261        });
4262
4263        let _ = handle.join();
4264
4265        // The data should be in a consistent state (push may or may not have completed)
4266        let recovered = data.lock().unwrap_or_else(|p| p.into_inner());
4267        // Either [1,2,3,4,5] or [1,2,3,4,5,6] - both are valid
4268        assert!(recovered.len() >= 5);
4269        assert_eq!(&recovered[..5], &[1, 2, 3, 4, 5]);
4270    }
4271}
4272
4273/// Helper struct for GPU mutex poison tests.
4274struct RendererState {
4275    frame_count: u32,
4276}