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::{FrameRenderer, 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::PhysicalSize<u32>, corner_radius: f32) -> bool {
248 let r = corner_radius + self.expansion;
249 let w = self.window_size.width as f32;
250 let h = self.window_size.height as f32;
251 let px = pos.width as f32;
252 let py = pos.height as f32;
253
254 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
328pub struct NativeRenderer {
331 gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
332 delta_time: f32,
333 elapsed_time: f32,
334 berserker_mode: cvkg_core::BerserkerMode,
335 rage: f32,
336 window: Arc<Window>,
337}
338
339#[derive(Debug)]
342pub enum AppEvent {
343 AccessibilityAction(accesskit::ActionRequest),
345 CloseWindow(winit::window::WindowId),
347 SetTitle(winit::window::WindowId, String),
349 SetSize(winit::window::WindowId, f32, f32),
351 SetVisible(winit::window::WindowId, bool),
353 BringToFront(winit::window::WindowId),
355 AccessibilityInitialTreeRequested(winit::window::WindowId),
357}
358
359impl From<accesskit_winit::Event> for AppEvent {
360 fn from(event: accesskit_winit::Event) -> Self {
361 match event.window_event {
362 accesskit_winit::WindowEvent::ActionRequested(req) => {
363 AppEvent::AccessibilityAction(req)
364 }
365 accesskit_winit::WindowEvent::InitialTreeRequested => {
366 AppEvent::AccessibilityInitialTreeRequested(event.window_id)
367 }
368 _ => AppEvent::AccessibilityAction(accesskit::ActionRequest {
369 action: accesskit::Action::Focus,
370 target_node: accesskit::NodeId(0),
371 target_tree: accesskit::TreeId::ROOT,
372 data: None,
373 }),
374 }
375 }
376}
377
378impl NativeRenderer {
379 fn new(
381 window: Arc<Window>,
382 gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
383 delta_time: f32,
384 elapsed_time: f32,
385 berserker_mode: cvkg_core::BerserkerMode,
386 rage: f32,
387 ) -> Self {
388 Self {
389 gpu,
390 delta_time,
391 elapsed_time,
392 berserker_mode,
393 rage,
394 window,
395 }
396 }
397
398 pub fn run<V: cvkg_core::View + 'static>(view: V) {
401 let event_loop = EventLoop::<AppEvent>::with_user_event()
402 .build()
403 .expect("Failed to create event loop");
404 event_loop.set_control_flow(ControlFlow::Wait);
405
406 let mut app = App {
407 view,
408 window_manager: WindowManager::new(),
409 gpu: None,
410 asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
411 proxy: event_loop.create_proxy(),
412 start_time: std::time::Instant::now(),
413 last_frame_time: std::time::Instant::now(),
414 berserker_mode: cvkg_core::BerserkerMode::Normal,
415 rage: 0.0,
416 state_detector: WindowStateDetector::new(),
417 modifiers: winit::keyboard::ModifiersState::default(),
418 audio_engine: None,
419 haptic_engine: Arc::new(VisualHapticEngine::new()),
420 };
421
422 event_loop.run_app(&mut app).expect("Event loop error");
423 }
424}
425
426struct NativeWindowWrapper {
429 winit_id: winit::window::WindowId,
430 window: Arc<winit::window::Window>,
431 proxy: winit::event_loop::EventLoopProxy<AppEvent>,
432 is_key: Arc<std::sync::atomic::AtomicBool>,
433 is_main: bool,
434}
435
436impl cvkg_core::Window for NativeWindowWrapper {
437 fn close(&self) {
439 let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
440 }
441
442 fn set_title(&self, title: &str) {
444 let _ = self
445 .proxy
446 .send_event(AppEvent::SetTitle(self.winit_id, title.to_string()));
447 }
448
449 fn set_size(&self, width: f32, height: f32) {
451 let _ = self
452 .proxy
453 .send_event(AppEvent::SetSize(self.winit_id, width, height));
454 }
455
456 fn is_key(&self) -> bool {
458 self.is_key.load(std::sync::atomic::Ordering::SeqCst)
459 }
460
461 fn is_main(&self) -> bool {
463 self.is_main
464 }
465
466 fn is_visible(&self) -> bool {
468 self.window.is_visible().unwrap_or(false)
469 }
470
471 fn set_visible(&self, visible: bool) {
473 let _ = self
474 .proxy
475 .send_event(AppEvent::SetVisible(self.winit_id, visible));
476 }
477
478 fn bring_to_front(&self) {
480 let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
481 }
482}
483
484pub struct WindowManager {
486 pub windows: std::collections::HashMap<winit::window::WindowId, WindowData>,
488 pub window_stack: Vec<winit::window::WindowId>,
490 pub winit_to_core: std::collections::HashMap<winit::window::WindowId, cvkg_core::WindowId>,
492 pub core_to_winit: std::collections::HashMap<cvkg_core::WindowId, winit::window::WindowId>,
494 pub next_core_id: u64,
496}
497
498impl Default for WindowManager {
499 fn default() -> Self {
500 Self::new()
501 }
502}
503
504impl WindowManager {
505 pub fn new() -> Self {
507 Self {
508 windows: std::collections::HashMap::new(),
509 window_stack: Vec::new(),
510 winit_to_core: std::collections::HashMap::new(),
511 core_to_winit: std::collections::HashMap::new(),
512 next_core_id: 1,
513 }
514 }
515
516 pub fn create_window(
518 &mut self,
519 event_loop: &ActiveEventLoop,
520 gpu: &Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
521 proxy: winit::event_loop::EventLoopProxy<AppEvent>,
522 config: cvkg_core::WindowConfig,
523 is_main: bool,
524 view: &impl cvkg_core::View,
525 ) -> cvkg_core::WindowHandle {
526 let mut window_attrs = Window::default_attributes()
527 .with_title(&config.title)
528 .with_visible(true)
529 .with_transparent(config.transparent)
530 .with_decorations(config.decorations)
531 .with_inner_size(winit::dpi::LogicalSize::new(config.size.0, config.size.1));
532
533 if let Some(min) = config.min_size {
534 window_attrs =
535 window_attrs.with_min_inner_size(winit::dpi::LogicalSize::new(min.0, min.1));
536 }
537 if let Some(max) = config.max_size {
538 window_attrs =
539 window_attrs.with_max_inner_size(winit::dpi::LogicalSize::new(max.0, max.1));
540 }
541
542 let winit_level = match config.level {
543 cvkg_core::WindowLevel::Normal => winit::window::WindowLevel::Normal,
544 cvkg_core::WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
545 cvkg_core::WindowLevel::PopUpMenu => winit::window::WindowLevel::AlwaysOnTop,
546 };
547 window_attrs = window_attrs.with_window_level(winit_level);
548
549 #[cfg(target_os = "macos")]
550 {
551 use winit::platform::macos::WindowAttributesExtMacOS;
552 window_attrs = window_attrs
553 .with_titlebar_transparent(true)
554 .with_title_hidden(true)
555 .with_fullsize_content_view(true)
556 .with_has_shadow(true);
557 }
558
559 let window = Arc::new(
560 event_loop
561 .create_window(window_attrs)
562 .expect("Failed to create window"),
563 );
564
565 let winit_id = window.id();
566 let core_id = cvkg_core::WindowId(self.next_core_id);
567 self.next_core_id += 1;
568
569 let is_key_focused = Arc::new(std::sync::atomic::AtomicBool::new(true));
570
571 let wrapper = Arc::new(NativeWindowWrapper {
572 winit_id,
573 window: window.clone(),
574 proxy: proxy.clone(),
575 is_key: is_key_focused.clone(),
576 is_main,
577 });
578
579 let handle = cvkg_core::WindowHandle::new(core_id, wrapper);
580
581 let vdom = cvkg_vdom::VDom::build(
582 view,
583 cvkg_core::Rect::new(0.0, 0.0, config.size.0, config.size.1),
584 );
585
586 let accesskit_adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
587 event_loop,
588 &window,
589 proxy.clone(),
590 ));
591
592 let data = WindowData {
593 window: window.clone(),
594 accesskit_adapter,
595 vdom: Some(vdom),
596 cursor_pos: [0.0, 0.0],
597 cursor_velocity: [0.0, 0.0],
598 last_redraw_start: std::time::Instant::now(),
599 frame_history: std::collections::VecDeque::with_capacity(60),
600 frame_count: 0,
601 last_pos: None,
602 needs_cursor_update: false,
603 is_dragging: false,
604 drag_start_pos: [0.0, 0.0],
605 drag_button: 0,
606 drag_threshold: 5.0,
607 is_key_focused,
608 is_main,
609 core_id,
610 window_handle: handle.clone(),
611 };
612
613 self.windows.insert(winit_id, data);
614 self.window_stack.push(winit_id);
615 self.winit_to_core.insert(winit_id, core_id);
616 self.core_to_winit.insert(core_id, winit_id);
617
618 if let Some(gpu_mutex) = gpu {
619 gpu_mutex.lock().unwrap().register_window(window.clone());
620 }
621
622 handle
623 }
624
625 pub fn close_window(&mut self, winit_id: winit::window::WindowId) {
627 self.windows.remove(&winit_id);
628 self.window_stack.retain(|id| *id != winit_id);
629 if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
630 self.core_to_winit.remove(&core_id);
631 }
632 }
633
634 pub fn bring_to_front(&mut self, winit_id: winit::window::WindowId) {
636 self.window_stack.retain(|id| *id != winit_id);
637 self.window_stack.push(winit_id);
638 if let Some(data) = self.windows.get(&winit_id) {
639 data.window.focus_window();
640 }
641 }
642
643 pub fn window(&self, winit_id: winit::window::WindowId) -> Option<&WindowData> {
645 self.windows.get(&winit_id)
646 }
647
648 pub fn window_mut(&mut self, winit_id: winit::window::WindowId) -> Option<&mut WindowData> {
650 self.windows.get_mut(&winit_id)
651 }
652
653 pub fn window_order(&self) -> &[winit::window::WindowId] {
655 &self.window_stack
656 }
657}
658
659pub struct WindowData {
660 window: Arc<Window>,
661 accesskit_adapter: Option<accesskit_winit::Adapter>,
662 vdom: Option<cvkg_vdom::VDom>,
663 cursor_pos: [f32; 2],
664 cursor_velocity: [f32; 2],
665 last_redraw_start: std::time::Instant,
667 frame_history: std::collections::VecDeque<f32>,
669 frame_count: u64,
671 last_pos: Option<[i32; 2]>,
673 needs_cursor_update: bool,
676 is_dragging: bool,
679 drag_start_pos: [f32; 2],
681 drag_button: u32,
683 drag_threshold: f32,
685
686 is_key_focused: Arc<std::sync::atomic::AtomicBool>,
688 is_main: bool,
689 core_id: cvkg_core::WindowId,
690 window_handle: cvkg_core::WindowHandle,
691}
692
693struct App<V: cvkg_core::View> {
694 view: V,
695 window_manager: WindowManager,
696 gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
697 #[allow(dead_code)]
698 asset_manager: std::sync::Arc<NativeAssetManager>,
699 proxy: winit::event_loop::EventLoopProxy<AppEvent>,
700 start_time: std::time::Instant,
701 last_frame_time: std::time::Instant,
702 berserker_mode: cvkg_core::BerserkerMode,
703 rage: f32,
704 state_detector: WindowStateDetector,
706 modifiers: winit::keyboard::ModifiersState,
708 audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
710 haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
712}
713
714impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
715 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
716 if self.gpu.is_none() {
717 let a11y_prefs = cvkg_core::AccessibilityPreferences::detect_from_system();
719 cvkg_core::set_accessibility_preferences(a11y_prefs);
720 if a11y_prefs.reduce_motion
721 || a11y_prefs.reduce_transparency
722 || a11y_prefs.increase_contrast
723 {
724 log::info!(
725 "[Native] Accessibility prefs: motion={} transparency={} contrast={}",
726 a11y_prefs.reduce_motion,
727 a11y_prefs.reduce_transparency,
728 a11y_prefs.increase_contrast
729 );
730 }
731
732 let system_theme = cvkg_core::detect_system_theme();
734 log::info!("[Native] System theme detected: {:?}", system_theme);
735
736 self.audio_engine =
738 RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
739
740 self.haptic_engine = Arc::new(VisualHapticEngine::new());
742
743 log::info!("[Native] App instance (resumed): {:p}", self);
744
745 let config = cvkg_core::WindowConfig {
746 title: "CVKG Berserker".to_string(),
747 size: (1280.0, 720.0),
748 min_size: None,
749 max_size: None,
750 resizable: true,
751 transparent: true,
752 decorations: true,
753 level: cvkg_core::WindowLevel::Normal,
754 };
755
756 let handle = self.window_manager.create_window(
757 event_loop,
758 &self.gpu,
759 self.proxy.clone(),
760 config,
761 true, &self.view,
763 );
764
765 let winit_id = self
766 .window_manager
767 .core_to_winit
768 .get(&handle.id)
769 .copied()
770 .expect("Failed to get winit_id");
771 let window = self
772 .window_manager
773 .windows
774 .get(&winit_id)
775 .unwrap()
776 .window
777 .clone();
778
779 let gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
781 self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
782
783 log::info!("[Native] Initialization complete.");
784 window.request_redraw();
785 }
786 }
787
788 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
789 if matches!(cause, winit::event::StartCause::Poll) {
790 } else {
792 log::trace!("[Native] Event Loop Wake: {:?}", cause);
794 }
795 }
796
797 fn device_event(
798 &mut self,
799 _event_loop: &ActiveEventLoop,
800 _device_id: winit::event::DeviceId,
801 event: winit::event::DeviceEvent,
802 ) {
803 if matches!(event, winit::event::DeviceEvent::MouseMotion { .. }) {
804 } else {
806 log::trace!("[Native] DEVICE EVENT: {:?}", event);
809 }
810 }
811
812 fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
813 if !matches!(event, WindowEvent::RedrawRequested)
814 && !matches!(event, WindowEvent::CursorMoved { .. })
815 {
816 log::info!(
817 "[Native] App instance: {:p} | WINDOW EVENT: {:?}",
818 self,
819 event
820 );
821 }
822
823 let gpu_arc = if let Some(g) = &self.gpu {
824 g.clone()
825 } else {
826 log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
827 return;
828 };
829
830 let mut close_window = false;
831 let mut bring_to_front = false;
832 let mut create_new_window = false;
833 let mut quit_all = false;
835
836 {
837 let state = if let Some(s) = self.window_manager.windows.get_mut(&id) {
838 s
839 } else {
840 return;
841 };
842
843 match event {
844 WindowEvent::Moved(pos) => {
845 let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
846 let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
847 let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
848
849 if speed > 0.1 {
850 self.rage = (self.rage + 0.2).min(1.0);
852 log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
853 }
854
855 state.last_pos = Some([pos.x, pos.y]);
856 state.window.request_redraw();
857 }
858 WindowEvent::DroppedFile(path) => {
859 if let Some(vdom) = &state.vdom {
860 vdom.dispatch_event(cvkg_core::Event::FileDrop {
861 x: state.cursor_pos[0],
862 y: state.cursor_pos[1],
863 path: path.to_string_lossy().into_owned(),
864 });
865 }
866 }
867 WindowEvent::CloseRequested => {
868 let close_action = cvkg_core::WindowCloseAction::Allow;
869 match close_action {
870 cvkg_core::WindowCloseAction::Allow
871 | cvkg_core::WindowCloseAction::Confirm => {
872 close_window = true;
873 }
874 cvkg_core::WindowCloseAction::Deny => {
875 log::info!("[Native] Close request denied for window {:?}", id);
876 }
877 }
878 }
879 WindowEvent::Resized(physical_size) => {
880 gpu_arc
881 .lock()
882 .expect("GPU mutex poisoned during resize")
883 .resize(
884 id,
885 physical_size.width,
886 physical_size.height,
887 state.window.scale_factor() as f32,
888 );
889 state.window.request_redraw();
890 }
891 WindowEvent::Focused(focused) => {
892 log::info!("[Native] Window focus changed: {}", focused);
893 state
894 .is_key_focused
895 .store(focused, std::sync::atomic::Ordering::SeqCst);
896 if focused {
897 bring_to_front = true;
898 }
899 }
900 WindowEvent::RedrawRequested => {
901 if state.frame_count % 60 == 0 {
902 log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
903 }
904 let size = state.window.inner_size();
905 let scale = state.window.scale_factor();
906 let logical_size = size.to_logical::<f32>(scale);
907
908 let rect = cvkg_core::Rect {
909 x: 0.0,
910 y: 0.0,
911 width: logical_size.width,
912 height: logical_size.height,
913 };
914
915 let redraw_start = std::time::Instant::now();
918 let last_redraw_start = state.last_redraw_start;
919 state.last_redraw_start = redraw_start;
922
923 let layout_start = std::time::Instant::now();
925 let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
926
927 if state.needs_cursor_update {
929 if let Some(vdom) = &state.vdom {
930 vdom.dispatch_event(cvkg_core::Event::PointerMove {
931 x: state.cursor_pos[0],
932 y: state.cursor_pos[1],
933 proximity_field: 0.0,
934 tilt: None,
935 azimuth: None,
936 pressure: Some(1.0),
937 barrel_rotation: None,
938 pointer_precision: 0.0,
939 });
940 }
941 state.needs_cursor_update = false;
942 }
943 let layout_end = std::time::Instant::now();
944
945 let state_flush_start = std::time::Instant::now();
947 if let Some(prev_vdom) = &mut state.vdom {
948 let patches = prev_vdom.diff(&new_vdom);
949 let mut nodes = Vec::new();
950 for patch in &patches {
951 if let cvkg_vdom::VDomPatch::Create(node)
952 | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
953 {
954 nodes
955 .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
956 } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
957 && let Some(node) = new_vdom.nodes.get(id)
958 {
959 nodes
960 .push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
961 }
962 }
963 if !nodes.is_empty() {
964 if let Some(adapter) = &mut state.accesskit_adapter {
965 adapter.update_if_active(|| accesskit::TreeUpdate {
966 nodes,
967 tree: None,
968 focus: accesskit::NodeId(1),
969 tree_id: accesskit::TreeId::ROOT,
970 });
971 }
972 }
973 prev_vdom.apply_patches(patches);
974 } else {
975 state.vdom = Some(new_vdom);
976 }
977 let state_flush_end = std::time::Instant::now();
978
979 let draw_start = std::time::Instant::now();
981 let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
982 let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
983 let mut gpu = gpu_arc
984 .lock()
985 .expect("GPU mutex poisoned during frame begin");
986 gpu.update_mouse(state.cursor_pos, state.cursor_velocity);
987 let encoder = gpu.begin_frame(id);
988 let mut renderer = NativeRenderer::new(
989 state.window.clone(),
990 gpu_arc.clone(),
991 delta_time,
992 elapsed_time,
993 self.berserker_mode,
994 self.rage,
995 );
996 drop(gpu);
1000 self.view.render(&mut renderer, rect);
1001 let draw_end = std::time::Instant::now();
1002
1003 let gpu_submit_start = std::time::Instant::now();
1005 let mut gpu = gpu_arc
1006 .lock()
1007 .expect("GPU mutex poisoned during frame submit");
1008 gpu.render_frame();
1009 gpu.end_frame(encoder);
1010 let gpu_submit_end = std::time::Instant::now();
1011
1012 let mut telemetry = cvkg_core::TelemetryData::default();
1017 telemetry.input_time_ms =
1018 redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
1019 telemetry.layout_time_ms =
1020 layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
1021 telemetry.state_flush_time_ms = state_flush_end
1022 .duration_since(state_flush_start)
1023 .as_secs_f32()
1024 * 1000.0;
1025 telemetry.draw_time_ms =
1026 draw_end.duration_since(draw_start).as_secs_f32() * 1000.0;
1027 telemetry.gpu_submit_time_ms = gpu_submit_end
1028 .duration_since(gpu_submit_start)
1029 .as_secs_f32()
1030 * 1000.0;
1031
1032 let frame_time_ms =
1034 gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
1035 telemetry.frame_time_ms = frame_time_ms;
1036
1037 log::info!(
1039 "[Native] Frame timings: layout={:.2}ms state={:.2}ms draw={:.2}ms submit={:.2}ms total={:.2}ms",
1040 telemetry.layout_time_ms,
1041 telemetry.state_flush_time_ms,
1042 telemetry.draw_time_ms,
1043 telemetry.gpu_submit_time_ms,
1044 telemetry.frame_time_ms
1045 );
1046
1047 state.frame_history.push_back(frame_time_ms);
1049 if state.frame_history.len() > 100 {
1050 state.frame_history.pop_front();
1051 }
1052
1053 let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
1054 sorted_frames
1055 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1056
1057 if !sorted_frames.is_empty() {
1058 let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
1059 telemetry.p99_frame_time_ms =
1060 sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
1061
1062 let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
1064 let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
1065 / sorted_frames.len() as f32;
1066 telemetry.frame_jitter_ms = variance.sqrt();
1067 }
1068
1069 telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
1075
1076 state.frame_count += 1;
1077
1078 telemetry.berserker_rage = self.rage;
1079 gpu.telemetry = telemetry;
1080 }
1081 WindowEvent::CursorEntered { .. } => {
1082 log::info!("[Native] Cursor ENTERED window");
1083 if let Some(vdom) = &state.vdom {
1084 vdom.dispatch_event(cvkg_core::Event::PointerEnter);
1085 }
1086 state.window.request_redraw();
1087 }
1088 WindowEvent::CursorLeft { .. } => {
1089 log::info!("[Native] Cursor LEFT window");
1090 if let Some(vdom) = &state.vdom {
1091 vdom.dispatch_event(cvkg_core::Event::PointerLeave);
1092 }
1093 state.window.request_redraw();
1094 }
1095 WindowEvent::CursorMoved { position, .. } => {
1096 let scale = state.window.scale_factor();
1097 let logical = position.to_logical::<f32>(scale);
1098 let elapsed = state.last_redraw_start.elapsed().as_secs_f32().max(0.001);
1099 let dx = logical.x - state.cursor_pos[0];
1100 let dy = logical.y - state.cursor_pos[1];
1101 state.cursor_velocity = [dx / elapsed, dy / elapsed];
1102 state.cursor_pos = [logical.x, logical.y];
1103 state.needs_cursor_update = true;
1104 if state.frame_count == 0 {
1107 state.window.request_redraw();
1108 }
1109 }
1110 WindowEvent::MouseInput {
1111 state: mouse_state,
1112 button,
1113 ..
1114 } => {
1115 log::info!(
1116 "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
1117 mouse_state,
1118 button,
1119 state.cursor_pos
1120 );
1121 if let Some(vdom) = &state.vdom {
1122 let btn_id = match button {
1123 winit::event::MouseButton::Left => 0,
1124 winit::event::MouseButton::Right => 2,
1125 winit::event::MouseButton::Middle => 1,
1126 winit::event::MouseButton::Back => 3,
1127 winit::event::MouseButton::Forward => 4,
1128 winit::event::MouseButton::Other(id) => id as u32,
1129 };
1130
1131 match mouse_state {
1132 winit::event::ElementState::Pressed => {
1133 log::info!("[Native] Dispatching PointerDown to VDOM");
1134 vdom.dispatch_event(cvkg_core::Event::PointerDown {
1135 x: state.cursor_pos[0],
1136 y: state.cursor_pos[1],
1137 button: btn_id,
1138 proximity_field: 0.0,
1139 tilt: None,
1140 azimuth: None,
1141 pressure: Some(1.0),
1142 barrel_rotation: None,
1143 pointer_precision: 0.0,
1144 });
1145 }
1146 winit::event::ElementState::Released => {
1147 log::info!("[Native] Dispatching PointerUp to VDOM");
1148 vdom.dispatch_event(cvkg_core::Event::PointerUp {
1149 x: state.cursor_pos[0],
1150 y: state.cursor_pos[1],
1151 button: btn_id,
1152 tilt: None,
1153 azimuth: None,
1154 pressure: Some(0.0),
1155 barrel_rotation: None,
1156 pointer_precision: 0.0,
1157 });
1158 }
1159 }
1160 state.window.request_redraw();
1161 } else {
1162 log::warn!("[Native] Mouse input received but state.vdom is None!");
1163 }
1164 }
1165 WindowEvent::MouseWheel { delta, .. } => {
1166 if let Some(vdom) = &state.vdom {
1167 let (dx, dy) = match delta {
1168 winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
1169 winit::event::MouseScrollDelta::PixelDelta(pos) => {
1170 (pos.x as f32, pos.y as f32)
1171 }
1172 };
1173 vdom.dispatch_event(cvkg_core::Event::PointerWheel {
1174 x: state.cursor_pos[0],
1175 y: state.cursor_pos[1],
1176 delta_x: dx,
1177 delta_y: dy,
1178 pointer_precision: 0.0,
1179 });
1180 state.window.request_redraw();
1181 }
1182 }
1183 WindowEvent::Touch(touch) => {
1187 if let Some(vdom) = &state.vdom {
1188 let scale = state.window.scale_factor();
1189 let logical = touch.location.to_logical::<f32>(scale);
1190 let x = logical.x;
1191 let y = logical.y;
1192 let touch_btn = 0; match touch.phase {
1194 winit::event::TouchPhase::Started => {
1195 log::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
1196 vdom.dispatch_event(cvkg_core::Event::PointerDown {
1197 x,
1198 y,
1199 button: touch_btn,
1200 proximity_field: 0.0,
1201 tilt: None,
1202 azimuth: None,
1203 pressure: Some(
1204 touch.force.map(|f| f.normalized() as f32).unwrap_or(1.0),
1205 ),
1206 barrel_rotation: None,
1207 pointer_precision: 150.0,
1208 });
1209 }
1210 winit::event::TouchPhase::Moved => {
1211 vdom.dispatch_event(cvkg_core::Event::PointerMove {
1212 x,
1213 y,
1214 proximity_field: 0.0,
1215 tilt: None,
1216 azimuth: None,
1217 pressure: Some(
1218 touch.force.map(|f| f.normalized() as f32).unwrap_or(1.0),
1219 ),
1220 barrel_rotation: None,
1221 pointer_precision: 150.0,
1222 });
1223 }
1224 winit::event::TouchPhase::Ended => {
1225 vdom.dispatch_event(cvkg_core::Event::PointerUp {
1226 x,
1227 y,
1228 button: touch_btn,
1229 tilt: None,
1230 azimuth: None,
1231 pressure: Some(0.0),
1232 barrel_rotation: None,
1233 pointer_precision: 150.0,
1234 });
1235 vdom.dispatch_event(cvkg_core::Event::PointerClick {
1237 x,
1238 y,
1239 button: touch_btn,
1240 tilt: None,
1241 azimuth: None,
1242 pressure: Some(0.0),
1243 barrel_rotation: None,
1244 pointer_precision: 150.0,
1245 });
1246 }
1247 winit::event::TouchPhase::Cancelled => {
1248 vdom.dispatch_event(cvkg_core::Event::PointerUp {
1249 x,
1250 y,
1251 button: touch_btn,
1252 tilt: None,
1253 azimuth: None,
1254 pressure: Some(0.0),
1255 barrel_rotation: None,
1256 pointer_precision: 150.0,
1257 });
1258 }
1259 }
1260 state.window.request_redraw();
1261 }
1262 }
1263 WindowEvent::PinchGesture { delta, .. } => {
1267 if let Some(vdom) = &state.vdom {
1268 let scale = 1.0 + delta as f32;
1269 let velocity = delta as f32;
1270 vdom.dispatch_event(cvkg_core::Event::GesturePinch {
1271 center: state.cursor_pos,
1272 scale,
1273 velocity,
1274 phase: cvkg_core::TouchPhase::Moved,
1275 });
1276 }
1277 if let Some(audio) = &self.audio_engine {
1279 audio.play_sound("nav_tick", 0.3);
1280 }
1281 self.haptic_engine
1282 .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
1283 state.window.request_redraw();
1284 }
1285 WindowEvent::RotationGesture { delta, .. } => {
1286 if let Some(vdom) = &state.vdom {
1287 let angle = delta;
1288 vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
1289 direction: [angle.cos(), angle.sin()],
1290 velocity: delta.abs(),
1291 phase: cvkg_core::TouchPhase::Moved,
1292 });
1293 }
1294 state.window.request_redraw();
1295 }
1296 WindowEvent::KeyboardInput { event, .. } => {
1297 if event.state == winit::event::ElementState::Pressed {
1298 if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
1299 let is_cmd = if cfg!(target_os = "macos") {
1303 self.modifiers.super_key()
1304 } else {
1305 self.modifiers.control_key()
1306 };
1307 let is_shift = self.modifiers.shift_key();
1308
1309 if is_cmd {
1310 match code {
1311 winit::keyboard::KeyCode::KeyZ => {
1313 if is_shift {
1314 log::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
1315 let mut redo_action = None;
1316 cvkg_core::update_system_state(|s| {
1317 let mut s = s.clone();
1318 redo_action = s.undo_manager.redo();
1319 s
1320 });
1321 if let Some(action) = redo_action {
1322 action();
1323 }
1324 state.window.request_redraw();
1325 } else {
1326 log::info!("[Native] Shortcut: Undo (Cmd+Z)");
1327 let mut undo_action = None;
1328 cvkg_core::update_system_state(|s| {
1329 let mut s = s.clone();
1330 undo_action = s.undo_manager.undo();
1331 s
1332 });
1333 if let Some(action) = undo_action {
1334 action();
1335 }
1336 state.window.request_redraw();
1337 }
1338 }
1339 winit::keyboard::KeyCode::KeyY
1341 if !cfg!(target_os = "macos") =>
1342 {
1343 log::info!("[Native] Shortcut: Redo (Ctrl+Y)");
1344 let mut redo_action = None;
1345 cvkg_core::update_system_state(|s| {
1346 let mut s = s.clone();
1347 redo_action = s.undo_manager.redo();
1348 s
1349 });
1350 if let Some(action) = redo_action {
1351 action();
1352 }
1353 state.window.request_redraw();
1354 }
1355 winit::keyboard::KeyCode::KeyN => {
1357 log::info!("[Native] Shortcut: New Window (Cmd+N)");
1358 create_new_window = true;
1359 }
1360 winit::keyboard::KeyCode::KeyO => {
1361 log::info!("[Native] Shortcut: Open File (Cmd+O)");
1362 if let Some(vdom) = &state.vdom {
1363 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1364 key: "cmd+o".to_string(),
1365 });
1366 }
1367 state.window.request_redraw();
1368 }
1369 winit::keyboard::KeyCode::KeyS => {
1370 log::info!("[Native] Shortcut: Save (Cmd+S)");
1371 if let Some(vdom) = &state.vdom {
1372 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1373 key: "cmd+s".to_string(),
1374 });
1375 }
1376 state.window.request_redraw();
1377 }
1378 winit::keyboard::KeyCode::KeyW => {
1379 log::info!("[Native] Shortcut: Close Window (Cmd+W)");
1380 close_window = true;
1381 }
1382 winit::keyboard::KeyCode::KeyQ => {
1383 log::info!("[Native] Shortcut: Quit (Cmd+Q)");
1384 quit_all = true;
1386 }
1387 winit::keyboard::KeyCode::KeyC => {
1389 log::info!("[Native] Shortcut: Copy (Cmd+C)");
1390 if let Some(vdom) = &state.vdom {
1391 vdom.dispatch_event(cvkg_core::Event::Copy);
1392 }
1393 state.window.request_redraw();
1394 }
1395 winit::keyboard::KeyCode::KeyV => {
1396 log::info!("[Native] Shortcut: Paste (Cmd+V)");
1397 let text = arboard::Clipboard::new()
1400 .ok()
1401 .and_then(|mut cb| cb.get_text().ok())
1402 .unwrap_or_default();
1403 if let Some(vdom) = &state.vdom {
1404 vdom.dispatch_event(cvkg_core::Event::Paste(text));
1405 }
1406 state.window.request_redraw();
1407 }
1408 winit::keyboard::KeyCode::KeyX => {
1409 log::info!("[Native] Shortcut: Cut (Cmd+X)");
1410 if let Some(vdom) = &state.vdom {
1411 vdom.dispatch_event(cvkg_core::Event::Cut);
1412 }
1413 state.window.request_redraw();
1414 }
1415 winit::keyboard::KeyCode::KeyA => {
1417 log::info!("[Native] Shortcut: Select All (Cmd+A)");
1418 if let Some(vdom) = &state.vdom {
1419 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1420 key: "cmd+a".to_string(),
1421 });
1422 }
1423 state.window.request_redraw();
1424 }
1425 winit::keyboard::KeyCode::KeyF => {
1426 log::info!("[Native] Shortcut: Find (Cmd+F)");
1427 if let Some(vdom) = &state.vdom {
1428 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1429 key: "cmd+f".to_string(),
1430 });
1431 }
1432 state.window.request_redraw();
1433 }
1434 _ => {}
1435 }
1436 }
1437 }
1438 }
1439
1440 if let Some(vdom) = &state.vdom
1441 && let Some(cvkg_event) = convert_keyboard_event(event)
1442 {
1443 vdom.dispatch_event(cvkg_event);
1444 state.window.request_redraw();
1445 }
1446 }
1447
1448 WindowEvent::Ime(ime_event) => {
1449 if let Some(vdom) = &state.vdom
1450 && let Some(cvkg_event) = convert_ime_event(ime_event)
1451 {
1452 vdom.dispatch_event(cvkg_event);
1453 state.window.request_redraw();
1454 }
1455 }
1456 WindowEvent::ModifiersChanged(new_modifiers) => {
1457 self.modifiers = new_modifiers.state();
1458 let shift = self.modifiers.shift_key();
1459 let ctrl = self.modifiers.control_key();
1460 let alt = self.modifiers.alt_key();
1461 let logo = self.modifiers.super_key();
1462 cvkg_core::update_system_state(|st| {
1463 let mut new_st = st.clone();
1464 new_st.modifiers_shift = shift;
1465 new_st.modifiers_ctrl = ctrl;
1466 new_st.modifiers_alt = alt;
1467 new_st.modifiers_logo = logo;
1468 new_st
1469 });
1470 }
1471 _ => {}
1472 }
1473 } if close_window {
1476 self.window_manager.close_window(id);
1477 }
1478 if quit_all {
1479 for wid in self.window_manager.window_order().to_vec() {
1481 self.window_manager.close_window(wid);
1482 }
1483 }
1484 if self.window_manager.windows.is_empty() {
1486 event_loop.exit();
1487 }
1488 if bring_to_front {
1489 self.window_manager.bring_to_front(id);
1490 }
1491 if create_new_window {
1492 self.window_manager.create_window(
1493 event_loop,
1494 &self.gpu,
1495 self.proxy.clone(),
1496 cvkg_core::WindowConfig {
1497 title: "New CVKG Window".to_string(),
1498 size: (800.0, 600.0),
1499 ..Default::default()
1500 },
1501 false, &self.view,
1503 );
1504 }
1505 }
1506
1507 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1508 match event {
1509 AppEvent::AccessibilityAction(request) => {
1510 let node_id = cvkg_vdom::NodeId(request.target_node.0);
1511 let target_state = self.window_manager.windows.values_mut().find(|s| {
1512 s.vdom
1513 .as_ref()
1514 .map_or(false, |v| v.nodes.contains_key(&node_id))
1515 });
1516
1517 if let Some(state) = target_state
1518 && let Some(vdom) = &state.vdom
1519 && let Some(node) = vdom.nodes.get(&node_id)
1520 && request.action == accesskit::Action::Click
1521 {
1522 let event = cvkg_core::Event::PointerClick {
1523 x: node.layout.x + node.layout.width / 2.0,
1524 y: node.layout.y + node.layout.height / 2.0,
1525 button: 0, tilt: None,
1527 azimuth: None,
1528 pressure: Some(1.0),
1529 barrel_rotation: None,
1530 pointer_precision: 0.0,
1531 };
1532 vdom.dispatch_event(event);
1533 }
1534 }
1535 AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
1536 if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
1537 if let Some(vdom) = &state.vdom {
1538 let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
1539 let mut nodes = Vec::new();
1540 for (id, node) in &vdom.nodes {
1541 nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
1542 }
1543 let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
1544 if let Some(adapter) = &mut state.accesskit_adapter {
1545 adapter.update_if_active(|| accesskit::TreeUpdate {
1546 nodes,
1547 tree: Some(tree),
1548 focus: accesskit::NodeId(root_id),
1549 tree_id: accesskit::TreeId::ROOT,
1550 });
1551 }
1552 }
1553 }
1554 }
1555 AppEvent::CloseWindow(winit_id) => {
1556 self.window_manager.close_window(winit_id);
1557 if self.window_manager.windows.is_empty() {
1558 event_loop.exit();
1559 }
1560 }
1561 AppEvent::SetTitle(winit_id, title) => {
1562 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1563 data.window.set_title(&title);
1564 }
1565 }
1566 AppEvent::SetSize(winit_id, width, height) => {
1567 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1568 let _ = data
1569 .window
1570 .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1571 }
1572 }
1573 AppEvent::SetVisible(winit_id, visible) => {
1574 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1575 data.window.set_visible(visible);
1576 }
1577 }
1578 AppEvent::BringToFront(winit_id) => {
1579 self.window_manager.bring_to_front(winit_id);
1580 }
1581 }
1582 }
1583
1584 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1585 self.rage = (self.rage - 0.02).max(0.0);
1587
1588 let now = std::time::Instant::now();
1590 let target_interval = std::time::Duration::from_millis(16);
1591
1592 if now.duration_since(self.last_frame_time) >= target_interval {
1593 if self.rage > 0.01 {
1594 log::debug!("[Native] Heartbeat ticking (rage: {})", self.rage);
1596 }
1597 self.last_frame_time = now;
1598 for window_state in self.window_manager.windows.values() {
1599 window_state.window.request_redraw();
1600 }
1601 event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1602 now + target_interval,
1603 ));
1604 } else {
1605 event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1606 self.last_frame_time + target_interval,
1607 ));
1608 }
1609 }
1610}
1611
1612impl cvkg_core::ElapsedTime for NativeRenderer {
1613 fn delta_time(&self) -> f32 {
1614 self.delta_time
1615 }
1616
1617 fn elapsed_time(&self) -> f32 {
1618 self.elapsed_time
1619 }
1620}
1621
1622impl cvkg_core::Renderer for NativeRenderer {
1623 fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1624 self.gpu
1625 .lock()
1626 .expect("GPU mutex poisoned: fill_rect")
1627 .fill_rect(rect, color);
1628 }
1629 fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
1630 self.gpu
1631 .lock()
1632 .expect("GPU mutex poisoned: fill_rounded_rect")
1633 .fill_rounded_rect(rect, radius, color);
1634 }
1635 fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
1636 self.gpu
1637 .lock()
1638 .expect("GPU mutex poisoned: fill_ellipse")
1639 .fill_ellipse(rect, color);
1640 }
1641 fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1642 self.gpu
1643 .lock()
1644 .expect("GPU mutex poisoned: stroke_rect")
1645 .stroke_rect(rect, color, stroke_width);
1646 }
1647 fn stroke_rounded_rect(
1648 &mut self,
1649 rect: cvkg_core::Rect,
1650 radius: f32,
1651 color: [f32; 4],
1652 stroke_width: f32,
1653 ) {
1654 self.gpu
1655 .lock()
1656 .expect("GPU mutex poisoned: stroke_rounded_rect")
1657 .stroke_rounded_rect(rect, radius, color, stroke_width);
1658 }
1659 fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
1660 self.gpu
1661 .lock()
1662 .expect("GPU mutex poisoned: stroke_ellipse")
1663 .stroke_ellipse(rect, color, stroke_width);
1664 }
1665 fn draw_line(
1666 &mut self,
1667 x1: f32,
1668 y1: f32,
1669 x2: f32,
1670 y2: f32,
1671 color: [f32; 4],
1672 stroke_width: f32,
1673 ) {
1674 self.gpu
1675 .lock()
1676 .expect("GPU mutex poisoned: draw_line")
1677 .draw_line(x1, y1, x2, y2, color, stroke_width);
1678 }
1679
1680 fn fill_glass_rect(&mut self, rect: cvkg_core::Rect, radius: f32, blur_radius: f32) {
1681 self.gpu
1682 .lock()
1683 .expect("GPU mutex poisoned: fill_glass_rect")
1684 .fill_glass_rect(rect, radius, blur_radius);
1685 }
1686
1687 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1688 self.gpu
1689 .lock()
1690 .expect("GPU mutex poisoned: draw_text")
1691 .draw_text(text, x, y, size, color);
1692 }
1693 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1694 self.gpu
1695 .lock()
1696 .expect("GPU mutex poisoned: measure_text")
1697 .measure_text(text, size)
1698 }
1699 fn draw_linear_gradient(
1700 &mut self,
1701 rect: cvkg_core::Rect,
1702 start_color: [f32; 4],
1703 end_color: [f32; 4],
1704 angle: f32,
1705 ) {
1706 self.gpu
1707 .lock()
1708 .expect("GPU mutex poisoned: draw_linear_gradient")
1709 .draw_linear_gradient(rect, start_color, end_color, angle);
1710 }
1711 fn draw_radial_gradient(
1712 &mut self,
1713 rect: cvkg_core::Rect,
1714 inner_color: [f32; 4],
1715 outer_color: [f32; 4],
1716 ) {
1717 self.gpu
1718 .lock()
1719 .expect("GPU mutex poisoned: draw_radial_gradient")
1720 .draw_radial_gradient(rect, inner_color, outer_color);
1721 }
1722 fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
1723 self.gpu
1724 .lock()
1725 .expect("GPU mutex poisoned: draw_texture")
1726 .draw_texture(texture_id, rect);
1727 }
1728 fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
1729 self.gpu
1730 .lock()
1731 .expect("GPU mutex poisoned: draw_image")
1732 .draw_image(image_name, rect);
1733 }
1734 fn load_image(&mut self, name: &str, data: &[u8]) {
1735 self.gpu
1736 .lock()
1737 .expect("GPU mutex poisoned: load_image")
1738 .load_image(name, data);
1739 }
1740 fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
1741 self.gpu
1742 .lock()
1743 .expect("GPU mutex poisoned: push_clip_rect")
1744 .push_clip_rect(rect);
1745 }
1746 fn pop_clip_rect(&mut self) {
1747 self.gpu
1748 .lock()
1749 .expect("GPU mutex poisoned: pop_clip_rect")
1750 .pop_clip_rect();
1751 }
1752 fn push_opacity(&mut self, opacity: f32) {
1753 self.gpu
1754 .lock()
1755 .expect("GPU mutex poisoned: push_opacity")
1756 .push_opacity(opacity);
1757 }
1758 fn draw_3d_cube(&mut self, rect: cvkg_core::Rect, color: [f32; 4], rotation: [f32; 3]) {
1759 self.gpu
1760 .lock()
1761 .expect("GPU mutex poisoned: draw_3d_cube")
1762 .draw_3d_cube(rect, color, rotation);
1763 }
1764 fn pop_opacity(&mut self) {
1765 self.gpu
1766 .lock()
1767 .expect("GPU mutex poisoned: pop_opacity")
1768 .pop_opacity();
1769 }
1770 fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
1771 self.gpu
1772 .lock()
1773 .expect("GPU mutex poisoned: bifrost")
1774 .bifrost(rect, blur, saturation, opacity);
1775 }
1776 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
1777 self.gpu
1778 .lock()
1779 .expect("GPU mutex poisoned: push_mjolnir_slice")
1780 .push_mjolnir_slice(angle, offset);
1781 }
1782 fn pop_mjolnir_slice(&mut self) {
1783 self.gpu
1784 .lock()
1785 .expect("GPU mutex poisoned: pop_mjolnir_slice")
1786 .pop_mjolnir_slice();
1787 }
1788 fn mjolnir_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
1789 self.gpu
1790 .lock()
1791 .expect("GPU mutex poisoned: mjolnir_shatter")
1792 .mjolnir_shatter(rect, pieces, force, color);
1793 }
1794 fn mjolnir_fluid_shatter(
1795 &mut self,
1796 rect: cvkg_core::Rect,
1797 pieces: u32,
1798 force: f32,
1799 color: [f32; 4],
1800 ) {
1801 self.gpu
1802 .lock()
1803 .expect("GPU mutex poisoned: mjolnir_fluid_shatter")
1804 .mjolnir_fluid_shatter(rect, pieces, force, color);
1805 }
1806 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1807 self.gpu
1808 .lock()
1809 .expect("GPU mutex poisoned: draw_mjolnir_bolt")
1810 .draw_mjolnir_bolt(from, to, color);
1811 }
1812 fn gungnir(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32, intensity: f32) {
1813 self.gpu
1814 .lock()
1815 .expect("GPU mutex poisoned: gungnir")
1816 .gungnir(rect, color, radius, intensity);
1817 }
1818 fn mani_glow(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32) {
1819 self.gpu
1820 .lock()
1821 .expect("GPU mutex poisoned: mani_glow")
1822 .mani_glow(rect, color, radius);
1823 }
1824 fn register_handler(
1825 &mut self,
1826 event_type: &str,
1827 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1828 ) {
1829 self.gpu
1830 .lock()
1831 .expect("GPU mutex poisoned: register_handler")
1832 .register_handler(event_type, handler);
1833 }
1834 fn push_vnode(&mut self, rect: cvkg_core::Rect, name: &'static str) {
1835 self.gpu
1836 .lock()
1837 .expect("GPU mutex poisoned: push_vnode")
1838 .push_vnode(rect, name);
1839 }
1840 fn pop_vnode(&mut self) {
1841 self.gpu
1842 .lock()
1843 .expect("GPU mutex poisoned: pop_vnode")
1844 .pop_vnode();
1845 }
1846 fn set_z_index(&mut self, z: f32) {
1850 self.gpu
1851 .lock()
1852 .expect("GPU mutex poisoned: set_z_index")
1853 .set_z_index(z);
1854 }
1855 fn get_z_index(&self) -> f32 {
1856 self.gpu
1857 .lock()
1858 .expect("GPU mutex poisoned: get_z_index")
1859 .get_z_index()
1860 }
1861 fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
1862 self.gpu
1863 .lock()
1864 .expect("GPU mutex poisoned: register_shared_element")
1865 .register_shared_element(id, rect);
1866 }
1867 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1868 self.gpu
1869 .lock()
1870 .expect("GPU mutex poisoned: set_material")
1871 .set_material(material);
1872 }
1873 fn current_material(&self) -> cvkg_core::DrawMaterial {
1874 self.gpu
1875 .lock()
1876 .expect("GPU mutex poisoned: current_material")
1877 .current_material()
1878 }
1879 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1880 self.gpu
1881 .lock()
1882 .expect("GPU mutex poisoned: serialize_svg")
1883 .serialize_svg(name)
1884 }
1885 fn apply_svg_filter(
1886 &mut self,
1887 name: &str,
1888 filter_id: &str,
1889 region: cvkg_core::Rect,
1890 ) -> Result<String, String> {
1891 self.gpu
1892 .lock()
1893 .expect("GPU mutex poisoned: apply_svg_filter")
1894 .apply_svg_filter(name, filter_id, region)
1895 }
1896 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
1897 self.gpu
1898 .lock()
1899 .expect("GPU mutex poisoned: push_shadow")
1900 .push_shadow(radius, color, offset);
1901 }
1902 fn pop_shadow(&mut self) {
1903 self.gpu
1904 .lock()
1905 .expect("GPU mutex poisoned: pop_shadow")
1906 .pop_shadow();
1907 }
1908 fn push_affine(&mut self, transform: [f32; 6]) {
1909 self.gpu
1910 .lock()
1911 .expect("GPU mutex poisoned: push_affine")
1912 .push_affine(transform);
1913 }
1914 fn enter_portal(&mut self, z_index: i32) {
1915 log::warn!(
1918 "Portal rendering (enter_portal) not yet implemented in GPU backend; z_index={}",
1919 z_index
1920 );
1921 }
1922 fn exit_portal(&mut self) {
1923 log::warn!("Portal rendering (exit_portal) not yet implemented in GPU backend");
1925 }
1926 fn viewport_size(&self) -> cvkg_core::Rect {
1927 let size = self.window.inner_size();
1928 let scale = self.window.scale_factor();
1929 let logical = size.to_logical::<f32>(scale);
1930 cvkg_core::Rect::new(0.0, 0.0, logical.width, logical.height)
1931 }
1932 fn announce(&mut self, message: &str, priority: cvkg_core::AnnouncementPriority) {
1933 log::info!("Accessibility announcement [{:?}]: {}", priority, message);
1937 }
1938 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1939 self.gpu
1940 .lock()
1941 .expect("GPU mutex poisoned: load_svg")
1942 .load_svg(name, svg_data);
1943 }
1944 fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
1945 self.gpu
1946 .lock()
1947 .expect("GPU mutex poisoned: draw_svg")
1948 .draw_svg(name, rect, None, 0);
1949 }
1950 fn draw_svg_with_offset(&mut self, name: &str, rect: cvkg_core::Rect, animation_time_offset: f32) {
1951 self.gpu
1952 .lock()
1953 .expect("GPU mutex poisoned: draw_svg_with_offset")
1954 .draw_svg_with_offset(name, rect, None, 0, animation_time_offset);
1955 }
1956 fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1957 self.gpu
1958 .lock()
1959 .expect("GPU mutex poisoned: get_telemetry")
1960 .telemetry
1961 .clone()
1962 }
1963 fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
1964 self.gpu
1965 .lock()
1966 .expect("GPU mutex poisoned: prewarm_vram")
1967 .prewarm_vram(assets);
1968 }
1969 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
1970 self.gpu
1971 .lock()
1972 .expect("GPU mutex poisoned: push_transform")
1973 .push_transform(translation, scale, rotation);
1974 }
1975 fn pop_transform(&mut self) {
1976 self.gpu
1977 .lock()
1978 .expect("GPU mutex poisoned: pop_transform")
1979 .pop_transform();
1980 }
1981
1982 fn set_berserker_mode(&mut self, state: cvkg_core::BerserkerMode) {
1983 self.berserker_mode = state;
1984
1985 if state == cvkg_core::BerserkerMode::GodMode {
1990 log::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
1991 #[cfg(target_os = "linux")]
1992 unsafe {
1993 let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
1994 }
1995 } else {
1996 #[cfg(target_os = "linux")]
1997 unsafe {
1998 let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
1999 }
2000 }
2001
2002 self.gpu
2003 .lock()
2004 .expect("GPU mutex poisoned: set_berserker_mode")
2005 .set_berserker_mode(state);
2006 }
2007
2008 fn set_rage(&mut self, rage: f32) {
2009 self.rage = rage;
2010 self.gpu
2011 .lock()
2012 .expect("GPU mutex poisoned: set_rage")
2013 .set_rage(rage);
2014 }
2015
2016 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
2017 self.gpu
2018 .lock()
2019 .expect("GPU mutex poisoned: memoize")
2020 .memoize(id, data_hash, render_fn);
2021 }
2022 fn request_redraw(&mut self) {
2023 self.window.request_redraw();
2024 }
2025
2026 fn capture_png(&mut self) -> Vec<u8> {
2036 log::info!("CAPTURING_FRAME: Initiating GPU readback...");
2037 let gpu = self.gpu.lock().expect("GPU mutex poisoned: capture_png");
2041 pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
2042 log::error!("GPU frame capture failed: {}", e);
2043 Vec::new() })
2045 }
2046
2047 fn print(&mut self) {
2048 log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
2049 println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
2052 }
2053}
2054
2055fn convert_keyboard_event(event: winit::event::KeyEvent) -> Option<cvkg_core::Event> {
2060 if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
2061 let key_str = format!("{:?}", code);
2062 if event.state == winit::event::ElementState::Pressed {
2063 Some(cvkg_core::Event::KeyDown { key: key_str })
2064 } else {
2065 Some(cvkg_core::Event::KeyUp { key: key_str })
2066 }
2067 } else {
2068 None
2069 }
2070}
2071
2072fn convert_ime_event(event: winit::event::Ime) -> Option<cvkg_core::Event> {
2073 if let winit::event::Ime::Commit(string) = event {
2074 Some(cvkg_core::Event::Ime(string))
2075 } else {
2076 None
2077 }
2078}
2079
2080fn convert_mouse_event(
2081 state: winit::event::ElementState,
2082 position: [f32; 2],
2083 button: u32,
2084) -> cvkg_core::Event {
2085 match state {
2086 winit::event::ElementState::Pressed => cvkg_core::Event::PointerDown {
2087 x: position[0],
2088 y: position[1],
2089 button,
2090 proximity_field: 0.0,
2091 tilt: None,
2092 azimuth: None,
2093 pressure: Some(1.0),
2094 barrel_rotation: None,
2095 pointer_precision: 0.0,
2096 },
2097 winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
2098 x: position[0],
2099 y: position[1],
2100 button,
2101 tilt: None,
2102 azimuth: None,
2103 pressure: Some(0.0),
2104 barrel_rotation: None,
2105 pointer_precision: 0.0,
2106 },
2107 }
2108}
2109
2110struct ShieldWall {
2113 proxy: winit::event_loop::EventLoopProxy<AppEvent>,
2114}
2115
2116impl accesskit::ActionHandler for ShieldWall {
2117 fn do_action(&mut self, request: accesskit::ActionRequest) {
2118 let _ = self
2119 .proxy
2120 .send_event(AppEvent::AccessibilityAction(request));
2121 }
2122}
2123
2124impl accesskit::ActivationHandler for ShieldWall {
2125 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
2126 let mut root = accesskit::Node::new(accesskit::Role::Window);
2127 root.set_label("CVKG Application");
2128
2129 let root_id = accesskit::NodeId(1);
2130 Some(accesskit::TreeUpdate {
2131 nodes: vec![(root_id, root)],
2132 tree: Some(accesskit::Tree::new(root_id)),
2133 focus: root_id,
2134 tree_id: accesskit::TreeId::ROOT,
2135 })
2136 }
2137}
2138
2139impl accesskit::DeactivationHandler for ShieldWall {
2140 fn deactivate_accessibility(&mut self) {}
2141}
2142
2143type AssetCacheMap =
2144 std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
2145
2146pub struct NativeAssetManager {
2152 cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
2153}
2154
2155impl Default for NativeAssetManager {
2156 fn default() -> Self {
2157 Self::new()
2158 }
2159}
2160
2161impl NativeAssetManager {
2162 pub fn new() -> Self {
2164 Self {
2165 cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
2166 std::collections::HashMap::new(),
2167 )),
2168 }
2169 }
2170}
2171
2172impl cvkg_core::AssetManager for NativeAssetManager {
2173 fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
2188 if let Some(state) = self.cache.load().get(url) {
2190 return state.clone();
2191 }
2192
2193 let cache = self.cache.clone();
2194 let key = url.to_string();
2195
2196 let mut we_inserted = false;
2201 self.cache.rcu(|map| {
2202 if map.contains_key(&key) {
2203 (**map).clone()
2205 } else {
2206 we_inserted = true;
2207 let mut m = (**map).clone();
2208 m.insert(key.clone(), cvkg_core::AssetState::Loading);
2209 m
2210 }
2211 });
2212
2213 if we_inserted {
2216 let cache_inner = cache.clone();
2217 let key_inner = key.clone();
2218
2219 std::thread::spawn(move || {
2220 log::debug!("[Native] Asynchronously loading asset: {}", key_inner);
2221 let result = match std::fs::read(&key_inner) {
2222 Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2223 Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2224 };
2225
2226 cache_inner.rcu(move |map| {
2227 let mut m = (**map).clone();
2228 m.insert(key_inner.clone(), result.clone());
2229 m
2230 });
2231 });
2232 }
2233
2234 cvkg_core::AssetState::Loading
2235 }
2236
2237 fn preload_image(&self, url: &str) {
2244 if self.cache.load().contains_key(url) {
2246 return;
2247 }
2248
2249 let cache = self.cache.clone();
2250 let key = url.to_string();
2251
2252 let mut we_inserted = false;
2253 self.cache.rcu(|map| {
2254 if map.contains_key(&key) {
2255 (**map).clone()
2256 } else {
2257 we_inserted = true;
2258 let mut m = (**map).clone();
2259 m.insert(key.clone(), cvkg_core::AssetState::Loading);
2260 m
2261 }
2262 });
2263
2264 if we_inserted {
2265 std::thread::spawn(move || {
2266 log::debug!("[Native] Preloading asset: {}", key);
2267 let result = match std::fs::read(&key) {
2268 Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
2269 Err(e) => cvkg_core::AssetState::Error(e.to_string()),
2270 };
2271
2272 cache.rcu(move |map| {
2273 let mut m = (**map).clone();
2274 m.insert(key.clone(), result.clone());
2275 m
2276 });
2277 });
2278 }
2279 }
2280}
2281
2282#[cfg(test)]
2283mod tests {
2284 use super::*;
2285 use cvkg_core::AssetManager;
2286 use std::io::Write;
2287
2288 #[test]
2293 fn test_native_asset_manager_loading() {
2294 let manager = NativeAssetManager::new();
2295 let temp_path = std::env::temp_dir().join("cvkg_test_asset_loading.png");
2296 let temp_file_path = temp_path.to_str().expect("temp path must be valid UTF-8");
2297 let test_data = b"fake-image-data";
2298
2299 let mut file = std::fs::File::create(temp_file_path).unwrap();
2301 file.write_all(test_data).unwrap();
2302 drop(file);
2303
2304 let mut state = manager.load_image(temp_file_path);
2306
2307 let start = std::time::Instant::now();
2309 while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2310 std::thread::sleep(std::time::Duration::from_millis(10));
2311 state = manager.load_image(temp_file_path);
2312 }
2313
2314 if let cvkg_core::AssetState::Ready(data) = state {
2315 assert_eq!(&*data, test_data);
2316 } else {
2317 let _ = std::fs::remove_file(temp_file_path);
2318 panic!("Expected Ready state, got {:?}", state);
2319 }
2320
2321 let state2 = manager.load_image(temp_file_path);
2323 if let cvkg_core::AssetState::Ready(data) = state2 {
2324 assert_eq!(&*data, test_data);
2325 } else {
2326 let _ = std::fs::remove_file(temp_file_path);
2327 panic!("Expected Ready state (cached), got {:?}", state2);
2328 }
2329
2330 let _ = std::fs::remove_file(temp_file_path);
2331 }
2332
2333 #[test]
2334 fn test_native_asset_manager_error() {
2335 let manager = NativeAssetManager::new();
2336 let path = "non_existent_file_cvkg_test.png";
2337 let mut state = manager.load_image(path);
2338
2339 let start = std::time::Instant::now();
2340 while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
2341 std::thread::sleep(std::time::Duration::from_millis(10));
2342 state = manager.load_image(path);
2343 }
2344
2345 if let cvkg_core::AssetState::Error(_) = state {
2346 } else {
2348 panic!("Expected Error state, got {:?}", state);
2349 }
2350 }
2351
2352 #[test]
2353 fn test_event_conversion() {
2354 let event = convert_mouse_event(winit::event::ElementState::Pressed, [10.0, 20.0], 0);
2356 if let cvkg_core::Event::PointerDown { x, y, button, .. } = event {
2357 assert_eq!(x, 10.0);
2358 assert_eq!(y, 20.0);
2359 assert_eq!(button, 0);
2360 } else {
2361 panic!("Expected PointerDown");
2362 }
2363
2364 let event = convert_ime_event(winit::event::Ime::Commit("hello".to_string()));
2366 if let Some(cvkg_core::Event::Ime(s)) = event {
2367 assert_eq!(s, "hello");
2368 } else {
2369 panic!("Expected Ime event");
2370 }
2371 }
2372}
2373
2374fn load_icon() -> Option<winit::window::Icon> {
2378 let base = std::env::current_dir().unwrap_or_else(|e| {
2382 log::warn!(
2383 "[Native] Failed to get current directory for icon search: {}",
2384 e
2385 );
2386 std::path::PathBuf::new()
2387 });
2388
2389 let mut candidates = vec![
2390 base.join("icon.png"),
2391 base.join("crates/ulfhednar/icons/icon.png"),
2392 base.join("ulfhednar/icons/icon.png"),
2393 base.join("crates/ulfhednar/assets/icon.png"),
2394 base.join("ulfhednar/assets/icon.png"),
2395 base.join("assets/icon.png"),
2396 ];
2397
2398 if let Ok(exe_path) = std::env::current_exe()
2400 && let Some(exe_dir) = exe_path.parent()
2401 {
2402 candidates.push(exe_dir.join("icons/icon.png"));
2403 candidates.push(exe_dir.join("assets/icon.png"));
2404 candidates.push(exe_dir.join("icon.png"));
2405 if let Some(parent) = exe_dir.parent() {
2406 candidates.push(parent.join("icons/icon.png"));
2407 candidates.push(parent.join("assets/icon.png"));
2408 candidates.push(parent.join("icon.png"));
2409 }
2410 }
2411
2412 for path in candidates {
2413 if !path.exists() {
2414 log::debug!("[Native] Icon candidate not found: {:?}", path);
2415 continue;
2416 }
2417
2418 match image::open(&path) {
2419 Ok(img) => {
2420 let rgba = img.to_rgba8();
2421 let (width, height) = rgba.dimensions();
2422 match winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
2423 Ok(icon) => {
2424 log::info!("[Native] Successfully loaded app icon from: {:?}", path);
2425 return Some(icon);
2426 }
2427 Err(e) => {
2428 log::warn!("[Native] Icon format error at {:?}: {}", path, e);
2429 }
2430 }
2431 }
2432 Err(e) => {
2433 log::warn!("[Native] Failed to open icon image at {:?}: {}", path, e);
2434 }
2435 }
2436 }
2437
2438 log::warn!(
2439 "[Native] Failed to find icon.png in any search path (CWD: {:?})",
2440 base
2441 );
2442 None
2443}
2444
2445pub struct RodioAudioEngine {
2453 _stream: rodio::OutputStream,
2454}
2455
2456unsafe impl Send for RodioAudioEngine {}
2460unsafe impl Sync for RodioAudioEngine {}
2461
2462impl RodioAudioEngine {
2463 pub fn new() -> Option<Self> {
2465 match rodio::OutputStreamBuilder::open_default_stream() {
2466 Ok(stream) => {
2467 log::info!("[Native] Audio engine initialized (rodio)");
2468 Some(Self { _stream: stream })
2469 }
2470 Err(e) => {
2471 log::warn!("[Native] Audio init failed (no sound): {}", e);
2472 None
2473 }
2474 }
2475 }
2476}
2477
2478impl cvkg_core::AudioEngine for RodioAudioEngine {
2479 fn play_sound(&self, name: &str, volume: f32) {
2480 let data: &[u8] = match name {
2481 "nav_tick" => cvkg_core::sounds::NAVIGATION_TICK,
2482 "success_chime" => cvkg_core::sounds::SUCCESS_CHIME,
2483 "warning_tone" => cvkg_core::sounds::WARNING_TONE,
2484 _ => {
2485 log::warn!("[Native] Unknown sound: {}", name);
2486 return;
2487 }
2488 };
2489 self.play_buffer(data, volume);
2490 }
2491
2492 fn play_buffer(&self, data: &[u8], _volume: f32) {
2493 use std::io::Cursor;
2494 let cursor = Cursor::new(data.to_vec());
2495 let mixer = self._stream.mixer();
2496 match rodio::play(mixer, cursor) {
2497 Ok(_sink) => {}
2498 Err(e) => log::warn!("[Native] Audio play failed: {}", e),
2499 }
2500 }
2501
2502 fn play_spatial(&self, name: &str, _position: [f32; 3], volume: f32) {
2503 self.play_sound(name, volume);
2505 }
2506}
2507
2508pub struct VisualHapticEngine {
2511 last_impact: std::sync::Mutex<std::time::Instant>,
2512}
2513
2514impl Default for VisualHapticEngine {
2515 fn default() -> Self {
2516 Self::new()
2517 }
2518}
2519
2520impl VisualHapticEngine {
2521 pub fn new() -> Self {
2522 Self {
2523 last_impact: std::sync::Mutex::new(std::time::Instant::now()),
2524 }
2525 }
2526}
2527
2528impl cvkg_core::HapticEngine for VisualHapticEngine {
2529 fn impact(&self, intensity: cvkg_core::HapticIntensity) {
2530 let _ = intensity;
2531 *self.last_impact.lock().unwrap() = std::time::Instant::now();
2532 }
2533 fn selection(&self) {
2534 self.impact(cvkg_core::HapticIntensity::Light);
2535 }
2536 fn success(&self) {
2537 self.impact(cvkg_core::HapticIntensity::Medium);
2538 }
2539 fn warning(&self) {
2540 self.impact(cvkg_core::HapticIntensity::Medium);
2541 }
2542 fn error(&self) {
2543 self.impact(cvkg_core::HapticIntensity::Heavy);
2544 }
2545 fn visual_tick(&self, _intensity: f32) {
2546 *self.last_impact.lock().unwrap() = std::time::Instant::now();
2547 }
2548}