1#![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
37use cvkg_core::{FocusableId, FrameRenderer, KvasirId, RenderStateSnapshot, Renderer};
43use image;
44use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum WindowState {
59 Normal,
61 Minimized,
63 Fullscreen,
65 SplitView,
67 Occluded,
69 Hidden,
71}
72
73pub struct WindowStateDetector {
87 state: WindowState,
88 is_key: bool,
89 is_main: bool,
90}
91
92impl WindowStateDetector {
93 pub fn new() -> Self {
95 Self {
96 state: WindowState::Normal,
97 is_key: false,
98 is_main: false,
99 }
100 }
101
102 pub fn state(&self) -> WindowState {
104 self.state
105 }
106
107 pub fn is_key(&self) -> bool {
109 self.is_key
110 }
111
112 pub fn is_main(&self) -> bool {
114 self.is_main
115 }
116
117 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 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 self.state = WindowState::Normal;
168 }
169 if self.state != old_state {
170 Some(self.state)
171 } else {
172 None
173 }
174 }
175
176 pub fn should_render(&self) -> bool {
181 !matches!(
182 self.state,
183 WindowState::Occluded | WindowState::Minimized | WindowState::Hidden
184 )
185 }
186
187 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
206pub struct ResizeHitTest {
213 window_size: winit::dpi::PhysicalSize<u32>,
215 corner_radius: f32,
217 expansion: f32,
219}
220
221impl ResizeHitTest {
222 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 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 if px <= r && py <= r {
256 return true;
257 }
258
259 if px >= w - r && py <= r {
261 return true;
262 }
263
264 if px <= r && py >= h - r {
266 return true;
267 }
268
269 if px >= w - r && py >= h - r {
271 return true;
272 }
273
274 false
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq)]
282pub struct SafeAreaInsets {
283 pub top: f32,
285 pub bottom: f32,
287 pub left: f32,
289 pub right: f32,
291}
292
293impl SafeAreaInsets {
294 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 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
328thread_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
337pub 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 #[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 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 #[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 GpuRefShared::Ptr(unsafe { &*raw })
382 } else {
383 GpuRefShared::Guard(self.gpu.lock().unwrap_or_else(|p| p.into_inner()))
384 }
385 })
386 }
387}
388
389enum 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
414enum 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#[derive(Debug)]
433pub enum AppEvent {
434 AccessibilityAction(accesskit::ActionRequest),
436 CloseWindow(winit::window::WindowId),
438 SetTitle(winit::window::WindowId, String),
440 SetSize(winit::window::WindowId, f32, f32),
442 SetVisible(winit::window::WindowId, bool),
444 BringToFront(winit::window::WindowId),
446 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 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 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 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
530struct 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 fn close(&self) {
543 let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
544 }
545
546 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 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 fn is_key(&self) -> bool {
562 self.is_key.load(std::sync::atomic::Ordering::SeqCst)
563 }
564
565 fn is_main(&self) -> bool {
567 self.is_main
568 }
569
570 fn is_visible(&self) -> bool {
572 self.window.is_visible().unwrap_or(false)
573 }
574
575 fn set_visible(&self, visible: bool) {
577 let _ = self
578 .proxy
579 .send_event(AppEvent::SetVisible(self.winit_id, visible));
580 }
581
582 fn bring_to_front(&self) {
584 let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
585 }
586}
587
588pub struct WindowManager {
590 pub windows: std::collections::HashMap<winit::window::WindowId, WindowData>,
592 pub window_stack: Vec<winit::window::WindowId>,
594 pub winit_to_core: std::collections::HashMap<winit::window::WindowId, cvkg_core::WindowId>,
596 pub core_to_winit: std::collections::HashMap<cvkg_core::WindowId, winit::window::WindowId>,
598 pub next_core_id: u64,
600}
601
602impl Default for WindowManager {
603 fn default() -> Self {
604 Self::new()
605 }
606}
607
608impl WindowManager {
609 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 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 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 #[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 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 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 pub fn window(&self, winit_id: winit::window::WindowId) -> Option<&WindowData> {
774 self.windows.get(&winit_id)
775 }
776
777 pub fn window_mut(&mut self, winit_id: winit::window::WindowId) -> Option<&mut WindowData> {
779 self.windows.get_mut(&winit_id)
780 }
781
782 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 last_redraw_start: std::time::Instant,
796 frame_history: std::collections::VecDeque<f32>,
798 frame_count: u64,
800 last_pos: Option<[i32; 2]>,
802 needs_cursor_update: bool,
805 is_dragging: bool,
808 drag_start_pos: [f32; 2],
810 drag_button: u32,
812 drag_threshold: f32,
814 active_pointer_target: Option<cvkg_vdom::NodeId>,
816 active_pointer_target_type: Option<String>,
818 active_pointer_target_key: Option<String>,
820 active_pointer_pos: Option<[f32; 2]>,
822 active_pointer_precision: f32,
824
825 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_manager: cvkg_core::FocusManager,
833 focused_node_id: Option<cvkg_vdom::NodeId>,
834
835 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 state_detector: WindowStateDetector,
852 frame_budget: cvkg_core::FrameBudgetTracker,
854 modifiers: winit::keyboard::ModifiersState,
856 audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
858 haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
860 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 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 let system_theme = cvkg_core::detect_system_theme();
884 log::info!("[Native] System theme detected: {:?}", system_theme);
885
886 self.audio_engine =
888 RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
889
890 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, &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 let mut gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
931
932 static PREFETCH_LABELS: &[(&str, f32)] = &[
937 ("File", 13.0),
939 ("Edit", 13.0),
940 ("View", 13.0),
941 ("Window", 13.0),
942 ("Help", 13.0),
943 ("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 } else {
969 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 } else {
983 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 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 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 let redraw_start = std::time::Instant::now();
1095 let last_redraw_start = state.last_redraw_start;
1096 state.last_redraw_start = redraw_start;
1099 self.frame_budget.new_frame();
1100
1101 let layout_start = std::time::Instant::now();
1103 let view_changed = self.view.changed();
1104
1105 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 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 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 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 }
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 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 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 gpu.update_mouse(state.cursor_pos, state.cursor_velocity);
1234
1235 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 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 {
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 let gpu_render_start = std::time::Instant::now();
1264 gpu.render_frame();
1265 let gpu_render_end = std::time::Instant::now();
1266
1267 gpu.end_frame(encoder);
1270 let gpu_submit_end = std::time::Instant::now();
1271
1272 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 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 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::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 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 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 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 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 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 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 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 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 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 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 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 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; match touch.phase {
1601 winit::event::TouchPhase::Started => {
1602 log::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
1603 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 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 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 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 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 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 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 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 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 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 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 quit_all = true;
1858 }
1859 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 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 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 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 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 let _ = scale_factor;
1991 if let Some(ctx) = self.window_manager.windows.get(&id) {
1992 ctx.window.request_redraw();
1993 }
1994 }
1995 _ => {}
1996 }
1997 } if close_window {
2000 self.window_manager.close_window(id);
2001 }
2002 if quit_all {
2003 for wid in self.window_manager.window_order().to_vec() {
2005 self.window_manager.close_window(wid);
2006 }
2007 }
2008 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, &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, 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 self.rage = (self.rage - 0.02).max(0.0);
2111
2112 let now = std::time::Instant::now();
2115 let target_interval = std::time::Duration::from_micros(8_333); if now.duration_since(self.last_frame_time) >= target_interval {
2118 self.last_frame_time = now;
2119 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 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 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 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 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 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 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 fn text_scale_factor(&self) -> f32 {
2438 self.gpu_ref_shared()
2439 .text_scale_factor()
2440 }
2441
2442 fn is_over_budget(&self) -> bool {
2447 self.gpu_ref_shared()
2448 .is_over_budget()
2449 }
2450
2451 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 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2465 self.gpu_ref()
2466 .measure_text(text, size)
2467 }
2468
2469 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 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 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 fn set_theme(&mut self, theme: cvkg_core::ColorTheme) {
2514 self.gpu_ref()
2515 .set_theme(theme);
2516 }
2517
2518 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
2523 self.gpu_ref()
2524 .trigger_shatter_event(origin, force);
2525 }
2526
2527 fn set_fireball_pos(&mut self, pos: [f32; 2]) {
2532 self.gpu_ref()
2533 .set_fireball_pos(pos);
2534 }
2535
2536 fn set_scene(&mut self, scene: &str) {
2541 self.gpu_ref()
2542 .set_scene(scene);
2543 }
2544
2545 fn set_scene_preset(&mut self, preset: u32) {
2550 self.gpu_ref()
2551 .set_scene_preset(preset);
2552 }
2553
2554 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 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 fn capture_png(&mut self) -> Vec<u8> {
2629 log::info!("CAPTURING_FRAME: Initiating GPU readback...");
2630 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() })
2638 }
2639
2640 fn print(&mut self) {
2641 log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
2642 println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
2645 }
2646}
2647
2648fn 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
2709struct 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
2745pub 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 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 fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
2787 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 let mut we_inserted = false;
2800 self.cache.rcu(|map| {
2801 if map.contains_key(&key) {
2802 (**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 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 fn preload_image(&self, url: &str) {
2843 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 #[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 let mut file = std::fs::File::create(temp_file_path).unwrap();
3023 file.write_all(test_data).unwrap();
3024 drop(file);
3025
3026 let mut state = manager.load_image(temp_file_path);
3028
3029 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 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 } else {
3070 panic!("Expected Error state, got {:?}", state);
3071 }
3072 }
3073
3074 #[test]
3075 fn test_event_conversion() {
3076 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 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
3223fn load_icon() -> Option<winit::window::Icon> {
3227 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 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
3294pub struct RodioAudioEngine {
3302 _stream: rodio::OutputStream,
3303}
3304
3305unsafe impl Send for RodioAudioEngine {}
3309unsafe impl Sync for RodioAudioEngine {}
3310
3311impl RodioAudioEngine {
3312 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 self.play_sound(name, volume);
3354 }
3355}
3356
3357pub 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#[derive(Debug, Clone)]
3409pub struct TranslationContract {
3410 pub cvkg_type: &'static str,
3412 pub platform_type: &'static str,
3414 pub rendering_mode: RenderingMode,
3416 pub native_accessibility: bool,
3418}
3419
3420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3422pub enum RenderingMode {
3423 Native,
3425 Custom,
3427 Hybrid,
3429}
3430
3431pub 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 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#[derive(Debug, Clone)]
3486pub struct WindowCapabilityMatrix {
3487 pub platform: &'static str,
3489 pub window_types: Vec<WindowType>,
3491 pub tabbed_windows: bool,
3493 pub tiled_windows: bool,
3495 pub floating_panels: bool,
3497 pub sheets: bool,
3499}
3500
3501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3503pub enum WindowType {
3504 Document,
3505 Panel,
3506 Popover,
3507 Dialog,
3508 Tooltip,
3509}
3510
3511impl WindowCapabilityMatrix {
3512 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3578pub enum SyncDirection {
3579 CvkgToNative,
3581 NativeToCvkg,
3583 Bidirectional,
3585}
3586
3587#[derive(Debug, Clone)]
3589pub struct StateSyncContract {
3590 pub widget_type: &'static str,
3592 pub direction: SyncDirection,
3594 pub debounce: bool,
3596 pub debounce_ms: u64,
3598}
3599
3600pub 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#[derive(Debug, Clone, Copy)]
3654pub struct WidgetVirtualizationConfig {
3655 pub buffer_size: usize,
3657 pub recycle_handles: bool,
3659 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3680pub struct SemanticRoleMapping {
3681 pub role: accesskit::Role,
3683 pub mac_ax_role: &'static str,
3685 pub win_uia_control_type: &'static str,
3687 pub linux_atk_role: &'static str,
3689}
3690
3691pub 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 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#[derive(Debug, Clone)]
3753pub struct MonitorConfig {
3754 pub name: String,
3756 pub position: (i32, i32),
3758 pub size: (u32, u32),
3760 pub scale_factor: f64,
3762 pub refresh_rate: u32,
3764}
3765
3766#[derive(Debug, Clone)]
3769pub struct MultiMonitorManager {
3770 monitors: Vec<MonitorConfig>,
3771 current_monitor_index: usize,
3772}
3773
3774impl MultiMonitorManager {
3775 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 pub fn current_monitor(&self) -> &MonitorConfig {
3800 &self.monitors[self.current_monitor_index]
3801 }
3802
3803 pub fn monitors(&self) -> &[MonitorConfig] {
3805 &self.monitors
3806 }
3807
3808 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 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 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 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#[derive(Debug, Clone)]
3891pub struct VisualRegressionTracker {
3892 reference_dir: std::path::PathBuf,
3894 pixel_tolerance: u8,
3896 max_mismatched_percentage: f64,
3898}
3899
3900impl VisualRegressionTracker {
3901 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 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 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 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 #[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 let scaled = manager.scale_dimensions(100.0, 200.0);
4018 assert_eq!(scaled, (100, 200));
4019
4020 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 assert!(manager.requires_dpi_adaptation(0, 1));
4030 assert!(!manager.requires_dpi_adaptation(0, 0));
4031 }
4032
4033 #[test]
4035 fn test_visual_regression_tracker_comparison() {
4036 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 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 let matched = tracker.verify_frame("test_red_rect", &png1);
4053 assert!(matched);
4054
4055 let matched_again = tracker.verify_frame("test_red_rect", &png1);
4057 assert!(matched_again);
4058
4059 let mut img2 = RgbaImage::new(10, 10);
4061 for (i, p) in img2.pixels_mut().enumerate() {
4062 if i == 0 {
4063 *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 let mut img3 = RgbaImage::new(10, 10);
4077 for p in img3.pixels_mut() {
4078 *p = image::Rgba([0, 255, 0, 255]); }
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 let _ = std::fs::remove_file(temp_dir.join("test_red_rect.png"));
4088 }
4089
4090 #[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 #[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 #[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 #[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 #[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 use std::sync::{Arc, Mutex};
4185 use std::thread;
4186
4187 #[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 let handle = thread::spawn(move || {
4196 let _guard = mutex_clone.lock().unwrap();
4197 panic!("simulated thread panic while holding lock");
4198 });
4199
4200 let _ = handle.join();
4202
4203 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]
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 let value = mutex.lock().unwrap_or_else(|p| p.into_inner());
4224 assert_eq!(*value, "hello");
4225 }
4226
4227 #[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 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 let mut state = gpu.lock().unwrap_or_else(|p| p.into_inner());
4245 assert_eq!(state.frame_count, 1);
4246 state.frame_count += 1;
4248 assert_eq!(state.frame_count, 2);
4249 }
4250
4251 #[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 let recovered = data.lock().unwrap_or_else(|p| p.into_inner());
4267 assert!(recovered.len() >= 5);
4269 assert_eq!(&recovered[..5], &[1, 2, 3, 4, 5]);
4270 }
4271}
4272
4273struct RendererState {
4275 frame_count: u32,
4276}