1use std::sync::Arc;
2use winit::application::ApplicationHandler;
3use winit::event::{DeviceEvent, DeviceId, WindowEvent};
4use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
5use winit::window::{Window, WindowId};
6
7use crate::asset_manager::NativeAssetManager;
8use crate::audio::{RodioAudioEngine, VisualHapticEngine};
9use crate::events::{convert_ime_event, convert_keyboard_event};
10use crate::renderer::{GPU_FRAME_PTR, NativeRenderer};
11use crate::window::{SafeAreaInsets, WindowManager, WindowState, WindowStateDetector};
12use cvkg_core::{
13 AccessibilityPreferences, ColorTheme, FocusableId, FrameBudgetTracker, FrameRenderer,
14 RenderIntensityMode, Renderer, TelemetryData, View, WindowConfig, detect_system_theme,
15 set_accessibility_preferences, update_system_state,
16};
17
18#[derive(Debug)]
21pub enum AppEvent {
22 AccessibilityAction(accesskit::ActionRequest),
24 CloseWindow(WindowId),
26 SetTitle(WindowId, String),
28 SetSize(WindowId, f32, f32),
30 SetVisible(WindowId, bool),
32 BringToFront(WindowId),
34 AccessibilityInitialTreeRequested(WindowId),
36}
37
38impl From<accesskit_winit::Event> for AppEvent {
39 fn from(event: accesskit_winit::Event) -> Self {
40 match event.window_event {
41 accesskit_winit::WindowEvent::ActionRequested(req) => {
42 AppEvent::AccessibilityAction(req)
43 }
44 accesskit_winit::WindowEvent::InitialTreeRequested => {
45 AppEvent::AccessibilityInitialTreeRequested(event.window_id)
46 }
47 _ => AppEvent::AccessibilityAction(accesskit::ActionRequest {
48 action: accesskit::Action::Focus,
49 target_node: accesskit::NodeId(0),
50 target_tree: accesskit::TreeId::ROOT,
51 data: None,
52 }),
53 }
54 }
55}
56
57pub struct App<V: View> {
58 pub(crate) view: V,
59 pub(crate) window_manager: WindowManager,
60 pub(crate) gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::GpuRenderer>>>,
61 #[allow(dead_code)]
62 pub(crate) asset_manager: std::sync::Arc<NativeAssetManager>,
63 pub(crate) proxy: EventLoopProxy<AppEvent>,
64 pub(crate) start_time: std::time::Instant,
65 pub(crate) last_frame_time: std::time::Instant,
66 pub(crate) berserker_mode: RenderIntensityMode,
67 pub(crate) rage: f32,
68 pub(crate) state_detector: WindowStateDetector,
69 pub(crate) frame_budget: FrameBudgetTracker,
70 pub(crate) modifiers: winit::keyboard::ModifiersState,
71 pub(crate) audio_engine: Option<Arc<dyn cvkg_core::AudioEngine>>,
72 pub(crate) haptic_engine: Arc<dyn cvkg_core::HapticEngine>,
73 pub(crate) pending_prewarm: Option<Vec<(String, Vec<u8>)>>,
74}
75
76impl<V: View + 'static> ApplicationHandler<AppEvent> for App<V> {
77 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
78 if self.gpu.is_none() {
79 let a11y_prefs = AccessibilityPreferences::detect_from_system();
80 set_accessibility_preferences(a11y_prefs);
81 if a11y_prefs.reduce_motion
82 || a11y_prefs.reduce_transparency
83 || a11y_prefs.increase_contrast
84 {
85 log::info!(
86 "[Native] Accessibility prefs: motion={} transparency={} contrast={}",
87 a11y_prefs.reduce_motion,
88 a11y_prefs.reduce_transparency,
89 a11y_prefs.increase_contrast
90 );
91 }
92
93 let system_theme = detect_system_theme();
94 log::info!("[Native] System theme detected: {:?}", system_theme);
95
96 self.audio_engine =
97 RodioAudioEngine::new().map(|e| Arc::new(e) as Arc<dyn cvkg_core::AudioEngine>);
98
99 self.haptic_engine = Arc::new(VisualHapticEngine::new());
100
101 log::info!("[Native] App instance (resumed): {:p}", self);
102
103 let config = WindowConfig {
104 title: "CVKG Gallery".to_string(),
105 size: (1280.0, 720.0),
106 min_size: None,
107 max_size: None,
108 resizable: true,
109 transparent: true,
110 decorations: true,
111 level: cvkg_core::WindowLevel::Normal,
112 };
113
114 let handle = self.window_manager.create_window(
115 event_loop,
116 &self.gpu,
117 self.proxy.clone(),
118 config,
119 true, &self.view,
121 );
122
123 let winit_id = self
124 .window_manager
125 .core_to_winit
126 .get(&handle.id)
127 .copied()
128 .unwrap_or_else(|| {
129 log::error!("[Native] winit_id not found for window handle: window may have been destroyed");
130 std::process::exit(1);
131 });
132 let window = self
133 .window_manager
134 .windows
135 .get(&winit_id)
136 .unwrap()
137 .window
138 .clone();
139
140 let mut gpu = pollster::block_on(cvkg_render_gpu::GpuRenderer::forge(window.clone()));
141
142 static PREFETCH_LABELS: &[(&str, f32)] = &[
143 ("File", 13.0),
144 ("Edit", 13.0),
145 ("View", 13.0),
146 ("Window", 13.0),
147 ("Help", 13.0),
148 ("Gallery", 14.0),
149 ("Rage", 12.0),
150 ("FPS", 12.0),
151 ("Frame", 12.0),
152 ("Draw", 12.0),
153 ("Layout", 12.0),
154 ("Submit", 12.0),
155 ("Browser", 12.0),
156 ("Chat", 12.0),
157 ("Code", 12.0),
158 ("Terminal", 12.0),
159 ];
160 gpu.prewarm_text_cache(PREFETCH_LABELS);
161
162 self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
163
164 log::info!("[Native] Initialization complete.");
165 window.request_redraw();
166 }
167 }
168
169 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
170 if !matches!(cause, winit::event::StartCause::Poll) {
171 log::trace!("[Native] Event Loop Wake: {:?}", cause);
172 }
173 }
174
175 fn device_event(
176 &mut self,
177 _event_loop: &ActiveEventLoop,
178 _device_id: DeviceId,
179 event: DeviceEvent,
180 ) {
181 if !matches!(event, DeviceEvent::MouseMotion { .. }) {
182 log::trace!("[Native] DEVICE EVENT: {:?}", event);
183 }
184 }
185
186 fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
187 if !matches!(event, WindowEvent::RedrawRequested)
188 && !matches!(event, WindowEvent::CursorMoved { .. })
189 {
190 log::info!(
191 "[Native] App instance: {:p} | WINDOW EVENT: {:?}",
192 self,
193 event
194 );
195 }
196
197 let gpu_arc = if let Some(g) = &self.gpu {
198 g.clone()
199 } else {
200 log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
201 return;
202 };
203
204 let mut close_window = false;
205 let mut bring_to_front = false;
206 let mut create_new_window = false;
207 let mut quit_all = false;
208
209 {
210 let state = if let Some(s) = self.window_manager.windows.get_mut(&id) {
211 s
212 } else {
213 return;
214 };
215
216 match event {
217 WindowEvent::Moved(pos) => {
218 let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
219 let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
220 let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
221
222 if speed > 0.1 {
223 self.rage = (self.rage + 0.2).min(1.0);
224 log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
225 }
226
227 state.last_pos = Some([pos.x, pos.y]);
228 state.window.request_redraw();
229 }
230 WindowEvent::DroppedFile(path) => {
231 if let Some(vdom) = &state.vdom {
232 vdom.dispatch_event(cvkg_core::Event::FileDrop {
233 x: state.cursor_pos[0],
234 y: state.cursor_pos[1],
235 path: path.to_string_lossy().into_owned(),
236 });
237 }
238 }
239 WindowEvent::CloseRequested => {
240 close_window = true;
241 }
242 WindowEvent::Resized(physical_size) => {
243 gpu_arc.lock().unwrap_or_else(|p| p.into_inner()).resize(
244 id,
245 physical_size.width,
246 physical_size.height,
247 state.window.scale_factor() as f32,
248 );
249 state.window.request_redraw();
250 }
251 WindowEvent::Focused(focused) => {
252 log::info!("[Native] Window focus changed: {}", focused);
253 state
254 .is_key_focused
255 .store(focused, std::sync::atomic::Ordering::SeqCst);
256 if focused {
257 bring_to_front = true;
258 }
259 }
260 WindowEvent::RedrawRequested => {
261 if state.frame_count % 60 == 0 {
262 log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
263 }
264 let size = state.window.inner_size();
265 let scale = state.window.scale_factor();
266 let logical_size = size.to_logical::<f32>(scale);
267
268 let rect = cvkg_core::Rect {
269 x: 0.0,
270 y: 0.0,
271 width: logical_size.width,
272 height: logical_size.height,
273 };
274
275 let redraw_start = std::time::Instant::now();
276 let last_redraw_start = state.last_redraw_start;
277 state.last_redraw_start = redraw_start;
278 self.frame_budget.new_frame();
279
280 let layout_start = std::time::Instant::now();
281 let view_changed = self.view.changed();
282
283 let bounds_changed = state.last_bounds.map_or(true, |b| b != rect);
284 let new_vdom: Option<cvkg_vdom::VDom> = if view_changed || bounds_changed {
285 state.last_bounds = Some(rect);
286 let vdom_start = std::time::Instant::now();
287 let vdom = cvkg_vdom::VDom::build(&self.view, rect);
288 let vdom_elapsed = vdom_start.elapsed();
289 if vdom_elapsed > std::time::Duration::from_millis(1) {
290 log::warn!(
291 "[Native] VDom::build took {:?} ({} nodes)",
292 vdom_elapsed,
293 vdom.nodes.len()
294 );
295 }
296 Some(vdom)
297 } else {
298 None
299 };
300
301 if state.needs_cursor_update {
302 if let Some(vdom) = &state.vdom {
303 vdom.dispatch_event(cvkg_core::Event::PointerMove {
304 x: state.cursor_pos[0],
305 y: state.cursor_pos[1],
306 proximity_field: 0.0,
307 tilt: None,
308 azimuth: None,
309 pressure: Some(1.0),
310 barrel_rotation: None,
311 pointer_precision: 0.0,
312 });
313 }
314 state.needs_cursor_update = false;
315 }
316 let layout_end = std::time::Instant::now();
317 self.frame_budget.subsystem_finish(1);
318
319 let state_flush_start = std::time::Instant::now();
320 #[allow(unused_assignments)]
321 let mut diff_patches = None;
322 match (new_vdom, &mut state.vdom) {
323 (Some(new_vdom), Some(prev_vdom)) => {
324 let diff_start = std::time::Instant::now();
325 let patches = prev_vdom.diff(&new_vdom);
326 let diff_elapsed = diff_start.elapsed();
327 if diff_elapsed > std::time::Duration::from_millis(1) {
328 log::warn!(
329 "[Native] VDom::diff took {:?} ({} patches)",
330 diff_elapsed,
331 patches.len()
332 );
333 }
334 diff_patches = Some(patches);
335 let patches = diff_patches.as_deref().unwrap_or_default();
337 let mut nodes = Vec::new();
338 for patch in patches {
339 if let cvkg_vdom::VDomPatch::Create(node)
340 | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
341 {
342 nodes.push((
343 accesskit::NodeId(node.id.0),
344 node.to_accesskit_node(),
345 ));
346 } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
347 && let Some(node) = new_vdom.nodes.get(id)
348 {
349 nodes.push((
350 accesskit::NodeId(node.id.0),
351 node.to_accesskit_node(),
352 ));
353 } else if let cvkg_vdom::VDomPatch::Remove(id) = patch {
354 state
355 .focus_manager
356 .unregister(&FocusableId::from(id.0.to_string()));
357 }
358 }
359 let focused_id = state
360 .focused_node_id
361 .map(|id| accesskit::NodeId(id.0))
362 .unwrap_or(accesskit::NodeId(1));
363 for patch in diff_patches.as_deref().unwrap_or_default() {
364 if let cvkg_vdom::VDomPatch::Create(node)
365 | cvkg_vdom::VDomPatch::Replace { node, .. } = patch
366 {
367 if node.is_focusable() {
368 state.focus_manager.register(node.id.0.to_string());
369 }
370 }
371 }
372 if !nodes.is_empty() {
373 if let Some(adapter) = &mut state.accesskit_adapter {
374 adapter.update_if_active(|| accesskit::TreeUpdate {
375 nodes,
376 tree: None,
377 focus: focused_id,
378 tree_id: accesskit::TreeId::ROOT,
379 });
380 }
381 }
382 prev_vdom.apply_patches(diff_patches.unwrap_or_default());
383 state.vdom = Some(new_vdom);
384 }
385 (Some(new_vdom), None) => {
386 state.vdom = Some(new_vdom);
387 }
388 (None, _) => {}
389 }
390 let state_flush_end = std::time::Instant::now();
391 self.frame_budget.subsystem_finish(0);
392
393 let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
394 let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
395
396 let safe_area = SafeAreaInsets::for_window_state(self.state_detector.state());
397 let content_rect = cvkg_core::Rect {
398 x: safe_area.left,
399 y: safe_area.top,
400 width: rect.width - safe_area.left - safe_area.right,
401 height: rect.height - safe_area.top - safe_area.bottom,
402 };
403 let layout_deadline =
404 std::time::Instant::now() + self.frame_budget.allocations()[1].time_slice;
405 cvkg_core::LayoutCache::set_layout_budget_deadline(Some(layout_deadline));
406
407 let mut renderer = NativeRenderer::new(
408 state.window.clone(),
409 gpu_arc.clone(),
410 delta_time,
411 elapsed_time,
412 self.berserker_mode,
413 self.rage,
414 );
415
416 let cpu_draw_start = std::time::Instant::now();
417 let mut gpu = gpu_arc.lock().unwrap_or_else(|p| p.into_inner());
418 let gpu_lock_time = cpu_draw_start.elapsed().as_secs_f32() * 1000.0;
419
420 gpu.update_mouse(state.cursor_pos, state.cursor_velocity);
421
422 if let Some(assets) = self.pending_prewarm.take() {
423 log::info!(
424 "[Native] Pre-warming {} assets on first frame",
425 assets.len()
426 );
427 gpu.prewarm_vram(assets);
428 }
429
430 let encoder = gpu.begin_frame(id);
431 let begin_frame_time =
432 cpu_draw_start.elapsed().as_secs_f32() * 1000.0 - gpu_lock_time;
433
434 {
435 let raw: *mut cvkg_render_gpu::GpuRenderer = &mut *gpu;
436 GPU_FRAME_PTR.with(|ptr| ptr.set(raw));
437 let render_start = std::time::Instant::now();
438 self.view.render(&mut renderer, content_rect);
439 let render_time = render_start.elapsed().as_secs_f32() * 1000.0;
440 GPU_FRAME_PTR.with(|ptr| ptr.set(std::ptr::null_mut()));
441 if render_time > 5.0 {
442 log::warn!(
443 "[Native] view.render() took {:.2}ms (gpu_lock={:.2}ms, begin_frame={:.2}ms)",
444 render_time,
445 gpu_lock_time,
446 begin_frame_time
447 );
448 }
449 }
450 let cpu_draw_end = std::time::Instant::now();
451 cvkg_core::LayoutCache::clear_layout_budget_deadline();
452
453 self.frame_budget.subsystem_finish(2);
454
455 let gpu_render_start = std::time::Instant::now();
456 gpu.render_frame();
457 let gpu_render_end = std::time::Instant::now();
458
459 gpu.end_frame(encoder);
460 let gpu_submit_end = std::time::Instant::now();
461
462 if state.frame_count % 60 == 0 {
463 let cpu_draw = cpu_draw_end.duration_since(cpu_draw_start);
464 let gpu_render = gpu_render_end.duration_since(gpu_render_start);
465 let gpu_submit = gpu_submit_end.duration_since(gpu_render_end);
466 let total = gpu_submit_end.duration_since(redraw_start);
467 log::info!(
468 "[Native] Frame breakdown: cpu_draw={:?} gpu_render={:?} gpu_submit(end_frame)={:?} total={:?}",
469 cpu_draw,
470 gpu_render,
471 gpu_submit,
472 total
473 );
474 }
475
476 let mut telemetry = TelemetryData::default();
477 telemetry.input_time_ms =
478 redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
479 telemetry.layout_time_ms =
480 layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
481 telemetry.state_flush_time_ms = state_flush_end
482 .duration_since(state_flush_start)
483 .as_secs_f32()
484 * 1000.0;
485 telemetry.draw_time_ms =
486 cpu_draw_end.duration_since(cpu_draw_start).as_secs_f32() * 1000.0;
487 telemetry.gpu_submit_time_ms =
488 gpu_submit_end.duration_since(cpu_draw_end).as_secs_f32() * 1000.0;
489
490 let frame_time_ms =
491 gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
492 telemetry.frame_time_ms = frame_time_ms;
493 telemetry.frame_budget_ms = self.frame_budget.total().as_secs_f32() * 1000.0;
494 telemetry.frame_budget_remaining_ms =
495 telemetry.frame_budget_ms - telemetry.frame_time_ms;
496 telemetry.layout_budget_remaining_ms = self
497 .frame_budget
498 .allocations()
499 .get(1)
500 .map(|alloc| {
501 alloc.time_slice.as_secs_f32() * 1000.0 - telemetry.layout_time_ms
502 })
503 .unwrap_or(0.0);
504 telemetry.frame_over_budget = !self.frame_budget.frame_within_budget()
505 || telemetry.frame_budget_remaining_ms < 0.0;
506 telemetry.layout_over_budget = !self.frame_budget.is_within_budget(1)
507 || telemetry.layout_budget_remaining_ms < 0.0;
508
509 log::info!(
510 "[Native] Frame timings: layout={:.2}ms state={:.2}ms draw={:.2}ms submit={:.2}ms total={:.2}ms",
511 telemetry.layout_time_ms,
512 telemetry.state_flush_time_ms,
513 telemetry.draw_time_ms,
514 telemetry.gpu_submit_time_ms,
515 telemetry.frame_time_ms
516 );
517
518 state.frame_history.push_back(frame_time_ms);
519 if state.frame_history.len() > 100 {
520 state.frame_history.pop_front();
521 }
522
523 let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
524 sorted_frames
525 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
526
527 if !sorted_frames.is_empty() {
528 let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
529 telemetry.p99_frame_time_ms =
530 sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
531
532 let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
533 let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
534 / sorted_frames.len() as f32;
535 telemetry.frame_jitter_ms = variance.sqrt();
536 }
537
538 telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
539 if telemetry.frame_over_budget {
540 log::warn!(
541 "[Native] Frame budget exceeded by {:.2}ms (layout remaining {:.2}ms)",
542 -telemetry.frame_budget_remaining_ms,
543 telemetry.layout_budget_remaining_ms
544 );
545 }
546
547 state.frame_count += 1;
548
549 telemetry.berserker_rage = self.rage;
550 gpu.telemetry = telemetry;
551
552 state.window.request_redraw();
553 }
554 WindowEvent::CursorEntered { .. } => {
555 log::info!("[Native] Cursor ENTERED window");
556 if let Some(vdom) = &state.vdom {
557 vdom.dispatch_event(cvkg_core::Event::PointerEnter);
558 }
559 state.window.request_redraw();
560 }
561 WindowEvent::CursorLeft { .. } => {
562 log::info!("[Native] Cursor LEFT window");
563 if let Some(vdom) = &state.vdom {
564 vdom.dispatch_event(cvkg_core::Event::PointerLeave);
565 }
566 state.window.request_redraw();
567 }
568 WindowEvent::CursorMoved { position, .. } => {
569 let scale = state.window.scale_factor();
570 let logical = position.to_logical::<f32>(scale);
571 let elapsed = state.last_redraw_start.elapsed().as_secs_f32().max(0.001);
572 let dx = logical.x - state.cursor_pos[0];
573 let dy = logical.y - state.cursor_pos[1];
574 state.cursor_velocity = [dx / elapsed, dy / elapsed];
575 state.cursor_pos = [logical.x, logical.y];
576 if !state.is_dragging {
577 let ddx = state.cursor_pos[0] - state.drag_start_pos[0];
578 let ddy = state.cursor_pos[1] - state.drag_start_pos[1];
579 let dist_sq = ddx * ddx + ddy * ddy;
580 if dist_sq > state.drag_threshold * state.drag_threshold {
581 state.is_dragging = true;
582 }
583 }
584 state.needs_cursor_update = true;
585 if state.frame_count == 0 {
586 state.window.request_redraw();
587 }
588 }
589 WindowEvent::MouseInput {
590 state: mouse_state,
591 button,
592 ..
593 } => {
594 log::info!(
595 "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
596 mouse_state,
597 button,
598 state.cursor_pos
599 );
600 if let Some(touch_time) = state.last_touch_time {
601 if touch_time.elapsed().as_millis() < 500 {
602 log::info!("[Native] Ignoring MouseInput (synthesized from Touch)");
603 return;
604 }
605 }
606 if let Some(vdom) = &state.vdom {
607 let btn_id = match button {
608 winit::event::MouseButton::Left => 0,
609 winit::event::MouseButton::Right => 2,
610 winit::event::MouseButton::Middle => 1,
611 winit::event::MouseButton::Back => 3,
612 winit::event::MouseButton::Forward => 4,
613 winit::event::MouseButton::Other(id) => id as u32,
614 };
615
616 match mouse_state {
617 winit::event::ElementState::Pressed => {
618 state.drag_start_pos = state.cursor_pos;
619 state.is_dragging = false;
620 state.drag_button = btn_id;
621 state.active_pointer_pos = Some(state.cursor_pos);
622 state.active_pointer_precision = 0.0;
623 state.active_pointer_target = vdom
624 .hit_test(state.cursor_pos[0], state.cursor_pos[1], 0.0)
625 .map(|(id, _)| id);
626 if let Some(target_id) = state.active_pointer_target {
627 if let Some(node) = vdom.nodes.get(&target_id) {
628 state.active_pointer_target_type =
629 Some(node.component_type.clone());
630 state.active_pointer_target_key = node.key.clone();
631 }
632 }
633 log::info!("[Native] Dispatching PointerDown to VDOM");
634 vdom.dispatch_event(cvkg_core::Event::PointerDown {
635 x: state.cursor_pos[0],
636 y: state.cursor_pos[1],
637 button: btn_id,
638 proximity_field: 0.0,
639 tilt: None,
640 azimuth: None,
641 pressure: Some(1.0),
642 barrel_rotation: None,
643 pointer_precision: 0.0,
644 });
645 }
646 winit::event::ElementState::Released => {
647 log::info!("[Native] Dispatching PointerUp to VDOM");
648 let fallback_target = state
649 .active_pointer_pos
650 .and_then(|pos| {
651 vdom.hit_test(
652 pos[0],
653 pos[1],
654 state.active_pointer_precision,
655 )
656 .map(|(id, _)| id)
657 })
658 .or_else(|| {
659 vdom.hit_test(
660 state.cursor_pos[0],
661 state.cursor_pos[1],
662 state.active_pointer_precision,
663 )
664 .map(|(id, _)| id)
665 });
666 let target = state
667 .active_pointer_target
668 .filter(|target| {
669 if state.active_pointer_target_key.is_none() {
670 log::debug!("[Native] Target verification: key is None, skipping cache");
671 return false;
672 }
673 let verified = vdom.nodes.get(target).map_or(false, |node| {
674 let type_match = Some(&node.component_type) == state.active_pointer_target_type.as_ref();
675 let key_match = node.key == state.active_pointer_target_key;
676 log::debug!("[Native] Target verify: id={:?} type={} key={:?} type_match={} key_match={}",
677 target, node.component_type, node.key, type_match, key_match);
678 type_match && key_match
679 });
680 if !verified {
681 log::debug!("[Native] Target verification failed for {:?}, using fallback", target);
682 }
683 verified
684 })
685 .or(fallback_target);
686 let pointer_up = cvkg_core::Event::PointerUp {
687 x: state.cursor_pos[0],
688 y: state.cursor_pos[1],
689 button: btn_id,
690 tilt: None,
691 azimuth: None,
692 pressure: Some(0.0),
693 barrel_rotation: None,
694 pointer_precision: 0.0,
695 };
696 let pointer_click = cvkg_core::Event::PointerClick {
697 x: state.cursor_pos[0],
698 y: state.cursor_pos[1],
699 button: btn_id,
700 tilt: None,
701 azimuth: None,
702 pressure: Some(0.0),
703 barrel_rotation: None,
704 pointer_precision: 0.0,
705 };
706 if let Some(target) = target {
707 vdom.dispatch_event_to_target(target, pointer_up);
708 } else {
709 vdom.dispatch_event(pointer_up);
710 }
711 if !state.is_dragging {
712 if let Some(target) = target {
713 log::info!(
714 "[Native] Dispatching PointerClick to VDOM (target={:?})",
715 target
716 );
717 vdom.dispatch_event_to_target(target, pointer_click);
718 } else {
719 log::info!(
720 "[Native] Dispatching PointerClick to VDOM (no target, bubbling)"
721 );
722 vdom.dispatch_event(pointer_click);
723 }
724 } else {
725 log::info!("[Native] Skipping PointerClick (is_dragging=true)");
726 }
727 state.is_dragging = false;
728 state.active_pointer_target = None;
729 state.active_pointer_target_type = None;
730 state.active_pointer_target_key = None;
731 state.active_pointer_pos = None;
732 }
733 }
734 state.window.request_redraw();
735 } else {
736 log::warn!("[Native] Mouse input received but state.vdom is None!");
737 }
738 }
739 WindowEvent::MouseWheel { delta, .. } => {
740 if let Some(vdom) = &state.vdom {
741 let (dx, dy) = match delta {
742 winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
743 winit::event::MouseScrollDelta::PixelDelta(pos) => {
744 (pos.x as f32, pos.y as f32)
745 }
746 };
747 vdom.dispatch_event(cvkg_core::Event::PointerWheel {
748 x: state.cursor_pos[0],
749 y: state.cursor_pos[1],
750 delta_x: dx,
751 delta_y: dy,
752 pointer_precision: 0.0,
753 });
754 state.window.request_redraw();
755 }
756 }
757 WindowEvent::Touch(touch) => {
758 state.last_touch_time = Some(std::time::Instant::now());
759 if let Some(vdom) = &state.vdom {
760 let scale = state.window.scale_factor();
761 let logical = touch.location.to_logical::<f32>(scale);
762 let x = logical.x;
763 let y = logical.y;
764 let touch_btn = 0;
765
766 match touch.phase {
767 winit::event::TouchPhase::Started => {
768 log::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
769 state.drag_start_pos = [x, y];
770 state.is_dragging = false;
771 state.drag_button = touch_btn;
772 state.active_pointer_pos = Some([x, y]);
773 state.active_pointer_precision = 150.0;
774 state.active_pointer_target =
775 vdom.hit_test(x, y, 150.0).map(|(id, _)| id);
776 if let Some(target_id) = state.active_pointer_target {
777 if let Some(node) = vdom.nodes.get(&target_id) {
778 state.active_pointer_target_type =
779 Some(node.component_type.clone());
780 state.active_pointer_target_key = node.key.clone();
781 }
782 }
783 vdom.dispatch_event(cvkg_core::Event::PointerDown {
784 x,
785 y,
786 button: touch_btn,
787 proximity_field: 0.0,
788 tilt: None,
789 azimuth: None,
790 pressure: Some(
791 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
792 ),
793 barrel_rotation: None,
794 pointer_precision: 150.0,
795 });
796 }
797 winit::event::TouchPhase::Moved => {
798 if !state.is_dragging {
799 let ddx = x - state.drag_start_pos[0];
800 let ddy = y - state.drag_start_pos[1];
801 let dist_sq = ddx * ddx + ddy * ddy;
802 if dist_sq > state.drag_threshold * state.drag_threshold {
803 state.is_dragging = true;
804 }
805 }
806 vdom.dispatch_event(cvkg_core::Event::PointerMove {
807 x,
808 y,
809 proximity_field: 0.0,
810 tilt: None,
811 azimuth: None,
812 pressure: Some(
813 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
814 ),
815 barrel_rotation: None,
816 pointer_precision: 150.0,
817 });
818 }
819 winit::event::TouchPhase::Ended => {
820 let fallback_target = state
821 .active_pointer_pos
822 .and_then(|pos| {
823 vdom.hit_test(
824 pos[0],
825 pos[1],
826 state.active_pointer_precision,
827 )
828 .map(|(id, _)| id)
829 })
830 .or_else(|| {
831 vdom.hit_test(x, y, state.active_pointer_precision)
832 .map(|(id, _)| id)
833 });
834 let target = state
835 .active_pointer_target
836 .filter(|target| {
837 vdom.nodes.get(target).map_or(false, |node| {
838 Some(&node.component_type)
839 == state.active_pointer_target_type.as_ref()
840 && node.key == state.active_pointer_target_key
841 })
842 })
843 .or(fallback_target);
844 let pointer_up = cvkg_core::Event::PointerUp {
845 x,
846 y,
847 button: touch_btn,
848 tilt: None,
849 azimuth: None,
850 pressure: Some(0.0),
851 barrel_rotation: None,
852 pointer_precision: 150.0,
853 };
854 let pointer_click = cvkg_core::Event::PointerClick {
855 x,
856 y,
857 button: touch_btn,
858 tilt: None,
859 azimuth: None,
860 pressure: Some(0.0),
861 barrel_rotation: None,
862 pointer_precision: 150.0,
863 };
864 if let Some(target) = target {
865 vdom.dispatch_event_to_target(target, pointer_up);
866 } else {
867 vdom.dispatch_event(pointer_up);
868 }
869 if !state.is_dragging {
870 if let Some(target) = target {
871 log::info!(
872 "[Native] Dispatching PointerClick to VDOM (target={:?})",
873 target
874 );
875 vdom.dispatch_event_to_target(target, pointer_click);
876 } else {
877 log::info!(
878 "[Native] Dispatching PointerClick to VDOM (no target, bubbling)"
879 );
880 vdom.dispatch_event(pointer_click);
881 }
882 } else {
883 log::info!("[Native] Skipping PointerClick (is_dragging=true)");
884 }
885 state.is_dragging = false;
886 state.active_pointer_target = None;
887 state.active_pointer_target_type = None;
888 state.active_pointer_target_key = None;
889 state.active_pointer_pos = None;
890 }
891 winit::event::TouchPhase::Cancelled => {
892 vdom.dispatch_event(cvkg_core::Event::PointerUp {
893 x,
894 y,
895 button: touch_btn,
896 tilt: None,
897 azimuth: None,
898 pressure: Some(0.0),
899 barrel_rotation: None,
900 pointer_precision: 150.0,
901 });
902 state.active_pointer_target = None;
903 state.active_pointer_pos = None;
904 }
905 }
906 state.window.request_redraw();
907 }
908 }
909 WindowEvent::PinchGesture { delta, .. } => {
910 if let Some(vdom) = &state.vdom {
911 let scale = 1.0 + delta as f32;
912 let velocity = delta as f32;
913 vdom.dispatch_event(cvkg_core::Event::GesturePinch {
914 center: state.cursor_pos,
915 scale,
916 velocity,
917 phase: cvkg_core::TouchPhase::Moved,
918 });
919 }
920 if let Some(audio) = &self.audio_engine {
921 audio.play_sound("nav_tick", 0.3);
922 }
923 self.haptic_engine
924 .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
925 state.window.request_redraw();
926 }
927 WindowEvent::RotationGesture { delta, .. } => {
928 if let Some(vdom) = &state.vdom {
929 let angle = delta;
930 vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
931 direction: [angle.cos(), angle.sin()],
932 velocity: delta.abs(),
933 phase: cvkg_core::TouchPhase::Moved,
934 });
935 }
936 state.window.request_redraw();
937 }
938 WindowEvent::KeyboardInput { event, .. } => {
939 if event.state == winit::event::ElementState::Pressed {
940 if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
941 let is_cmd = if cfg!(target_os = "macos") {
942 self.modifiers.super_key()
943 } else {
944 self.modifiers.control_key()
945 };
946 let is_shift = self.modifiers.shift_key();
947
948 if is_cmd {
949 match code {
950 winit::keyboard::KeyCode::KeyZ => {
951 if is_shift {
952 log::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
953 let mut redo_action = None;
954 update_system_state(|s| {
955 let mut s = s.clone();
956 redo_action = s.undo_manager.redo();
957 s
958 });
959 if let Some(action) = redo_action {
960 action();
961 }
962 state.window.request_redraw();
963 } else {
964 log::info!("[Native] Shortcut: Undo (Cmd+Z)");
965 let mut undo_action = None;
966 update_system_state(|s| {
967 let mut s = s.clone();
968 undo_action = s.undo_manager.undo();
969 s
970 });
971 if let Some(action) = undo_action {
972 action();
973 }
974 state.window.request_redraw();
975 }
976 }
977 winit::keyboard::KeyCode::KeyY
978 if !cfg!(target_os = "macos") =>
979 {
980 log::info!("[Native] Shortcut: Redo (Ctrl+Y)");
981 let mut redo_action = None;
982 update_system_state(|s| {
983 let mut s = s.clone();
984 redo_action = s.undo_manager.redo();
985 s
986 });
987 if let Some(action) = redo_action {
988 action();
989 }
990 state.window.request_redraw();
991 }
992 winit::keyboard::KeyCode::KeyN => {
993 log::info!("[Native] Shortcut: New Window (Cmd+N)");
994 create_new_window = true;
995 }
996 winit::keyboard::KeyCode::KeyO => {
997 log::info!("[Native] Shortcut: Open File (Cmd+O)");
998 if let Some(vdom) = &state.vdom {
999 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1000 key: "cmd+o".to_string(),
1001 modifiers: cvkg_core::KeyModifiers::default(),
1002 });
1003 }
1004 state.window.request_redraw();
1005 }
1006 winit::keyboard::KeyCode::KeyS => {
1007 log::info!("[Native] Shortcut: Save (Cmd+S)");
1008 if let Some(vdom) = &state.vdom {
1009 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1010 key: "cmd+s".to_string(),
1011 modifiers: cvkg_core::KeyModifiers::default(),
1012 });
1013 }
1014 state.window.request_redraw();
1015 }
1016 winit::keyboard::KeyCode::KeyW => {
1017 log::info!("[Native] Shortcut: Close Window (Cmd+W)");
1018 close_window = true;
1019 }
1020 winit::keyboard::KeyCode::KeyQ => {
1021 log::info!("[Native] Shortcut: Quit (Cmd+Q)");
1022 quit_all = true;
1023 }
1024 winit::keyboard::KeyCode::KeyC => {
1025 log::info!("[Native] Shortcut: Copy (Cmd+C)");
1026 if let Some(vdom) = &state.vdom {
1027 vdom.dispatch_event(cvkg_core::Event::Copy);
1028 }
1029 state.window.request_redraw();
1030 }
1031 winit::keyboard::KeyCode::KeyV => {
1032 log::info!("[Native] Shortcut: Paste (Cmd+V)");
1033 let text = arboard::Clipboard::new()
1034 .ok()
1035 .and_then(|mut cb| cb.get_text().ok())
1036 .unwrap_or_default();
1037 if let Some(vdom) = &state.vdom {
1038 vdom.dispatch_event(cvkg_core::Event::Paste(text));
1039 }
1040 state.window.request_redraw();
1041 }
1042 winit::keyboard::KeyCode::KeyX => {
1043 log::info!("[Native] Shortcut: Cut (Cmd+X)");
1044 if let Some(vdom) = &state.vdom {
1045 vdom.dispatch_event(cvkg_core::Event::Cut);
1046 }
1047 state.window.request_redraw();
1048 }
1049 winit::keyboard::KeyCode::F11 => {
1050 let is_fullscreen = state.window.fullscreen().is_some();
1051 if is_fullscreen {
1052 state.window.set_fullscreen(None);
1053 log::info!("[Native] Fullscreen OFF");
1054 } else {
1055 if let Some(monitor) = state.window.current_monitor() {
1056 if let Some(mode) = monitor.video_modes().next() {
1057 let w = mode.size().width;
1058 let h = mode.size().height;
1059 let rr = mode.refresh_rate_millihertz();
1060 state.window.set_fullscreen(Some(
1061 winit::window::Fullscreen::Exclusive(mode),
1062 ));
1063 log::info!(
1064 "[Native] Fullscreen ON (exclusive: {}x{}@{:?}Hz)",
1065 w,
1066 h,
1067 rr
1068 );
1069 }
1070 } else {
1071 state.window.set_fullscreen(Some(
1072 winit::window::Fullscreen::Borderless(None),
1073 ));
1074 log::info!("[Native] Fullscreen ON (borderless)");
1075 }
1076 }
1077 state.window.request_redraw();
1078 }
1079 winit::keyboard::KeyCode::KeyA => {
1080 log::info!("[Native] Shortcut: Select All (Cmd+A)");
1081 if let Some(vdom) = &state.vdom {
1082 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1083 key: "cmd+a".to_string(),
1084 modifiers: cvkg_core::KeyModifiers::default(),
1085 });
1086 }
1087 state.window.request_redraw();
1088 }
1089 winit::keyboard::KeyCode::KeyF => {
1090 log::info!("[Native] Shortcut: Find (Cmd+F)");
1091 if let Some(vdom) = &state.vdom {
1092 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1093 key: "cmd+f".to_string(),
1094 modifiers: cvkg_core::KeyModifiers::default(),
1095 });
1096 }
1097 state.window.request_redraw();
1098 }
1099 winit::keyboard::KeyCode::Tab => {
1100 if is_shift {
1101 if let Some(id) = state.focus_manager.focus_prev() {
1102 if let Ok(node_id) = id.as_str().parse::<u64>() {
1103 state.focused_node_id =
1104 Some(cvkg_core::KvasirId(node_id));
1105 log::info!(
1106 "[Native] Focus previous: {:?}",
1107 node_id
1108 );
1109 }
1110 }
1111 } else {
1112 if let Some(id) = state.focus_manager.focus_next() {
1113 if let Ok(node_id) = id.as_str().parse::<u64>() {
1114 state.focused_node_id =
1115 Some(cvkg_core::KvasirId(node_id));
1116 log::info!(
1117 "[Native] Focus next: {:?}",
1118 node_id
1119 );
1120 }
1121 }
1122 }
1123 state.window.request_redraw();
1124 }
1125 _ => {}
1126 }
1127 }
1128 }
1129 }
1130
1131 if let Some(vdom) = &state.vdom
1132 && let Some(cvkg_event) = convert_keyboard_event(event, &self.modifiers)
1133 {
1134 vdom.dispatch_event(cvkg_event);
1135 state.window.request_redraw();
1136 }
1137 }
1138 WindowEvent::Ime(ime_event) => {
1139 if let Some(vdom) = &state.vdom
1140 && let Some(cvkg_event) = convert_ime_event(ime_event)
1141 {
1142 vdom.dispatch_event(cvkg_event);
1143 state.window.request_redraw();
1144 }
1145 }
1146 WindowEvent::ModifiersChanged(new_modifiers) => {
1147 self.modifiers = new_modifiers.state();
1148 let shift = self.modifiers.shift_key();
1149 let ctrl = self.modifiers.control_key();
1150 let alt = self.modifiers.alt_key();
1151 let logo = self.modifiers.super_key();
1152 update_system_state(|st| {
1153 let mut new_st = st.clone();
1154 new_st.modifiers_shift = shift;
1155 new_st.modifiers_ctrl = ctrl;
1156 new_st.modifiers_alt = alt;
1157 new_st.modifiers_logo = logo;
1158 new_st
1159 });
1160 }
1161 WindowEvent::ScaleFactorChanged { .. } => {
1162 if let Some(ctx) = self.window_manager.windows.get(&id) {
1163 ctx.window.request_redraw();
1164 }
1165 }
1166 _ => {}
1167 }
1168 }
1169
1170 if close_window {
1171 self.window_manager.close_window(id);
1172 }
1173 if quit_all {
1174 for wid in self.window_manager.window_order().to_vec() {
1175 self.window_manager.close_window(wid);
1176 }
1177 }
1178 if self.window_manager.windows.is_empty() {
1179 event_loop.exit();
1180 }
1181 if bring_to_front {
1182 self.window_manager.bring_to_front(id);
1183 }
1184 if create_new_window {
1185 self.window_manager.create_window(
1186 event_loop,
1187 &self.gpu,
1188 self.proxy.clone(),
1189 WindowConfig {
1190 title: "New CVKG Window".to_string(),
1191 size: (800.0, 600.0),
1192 ..Default::default()
1193 },
1194 false,
1195 &self.view,
1196 );
1197 }
1198 }
1199
1200 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1201 match event {
1202 AppEvent::AccessibilityAction(request) => {
1203 let node_id = cvkg_core::KvasirId(request.target_node.0);
1204 let target_state = self.window_manager.windows.values_mut().find(|s| {
1205 s.vdom
1206 .as_ref()
1207 .map_or(false, |v| v.nodes.contains_key(&node_id))
1208 });
1209
1210 if let Some(state) = target_state
1211 && let Some(vdom) = &state.vdom
1212 && let Some(node) = vdom.nodes.get(&node_id)
1213 && request.action == accesskit::Action::Click
1214 {
1215 let event = cvkg_core::Event::PointerClick {
1216 x: node.layout.x + node.layout.width / 2.0,
1217 y: node.layout.y + node.layout.height / 2.0,
1218 button: 0,
1219 tilt: None,
1220 azimuth: None,
1221 pressure: Some(1.0),
1222 barrel_rotation: None,
1223 pointer_precision: 0.0,
1224 };
1225 vdom.dispatch_event(event);
1226 }
1227 }
1228 AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
1229 if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
1230 if let Some(vdom) = &state.vdom {
1231 let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
1232 let mut nodes = Vec::new();
1233 for (id, node) in &vdom.nodes {
1234 nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
1235 }
1236 let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
1237 if let Some(adapter) = &mut state.accesskit_adapter {
1238 adapter.update_if_active(|| accesskit::TreeUpdate {
1239 nodes,
1240 tree: Some(tree),
1241 focus: accesskit::NodeId(root_id),
1242 tree_id: accesskit::TreeId::ROOT,
1243 });
1244 }
1245 }
1246 }
1247 }
1248 AppEvent::CloseWindow(winit_id) => {
1249 self.window_manager.close_window(winit_id);
1250 if self.window_manager.windows.is_empty() {
1251 event_loop.exit();
1252 }
1253 }
1254 AppEvent::SetTitle(winit_id, title) => {
1255 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1256 data.window.set_title(&title);
1257 }
1258 }
1259 AppEvent::SetSize(winit_id, width, height) => {
1260 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1261 let _ = data
1262 .window
1263 .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1264 }
1265 }
1266 AppEvent::SetVisible(winit_id, visible) => {
1267 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1268 data.window.set_visible(visible);
1269 }
1270 }
1271 AppEvent::BringToFront(winit_id) => {
1272 self.window_manager.bring_to_front(winit_id);
1273 }
1274 }
1275 }
1276
1277 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1278 self.rage = (self.rage - 0.02).max(0.0);
1279
1280 let now = std::time::Instant::now();
1281 let target_interval = std::time::Duration::from_micros(8_333);
1282
1283 if now.duration_since(self.last_frame_time) >= target_interval {
1284 self.last_frame_time = now;
1285 let needs_redraw = self.view.changed();
1286 if needs_redraw {
1287 for window_state in self.window_manager.windows.values() {
1288 window_state.window.request_redraw();
1289 }
1290 }
1291 event_loop.set_control_flow(ControlFlow::WaitUntil(now + target_interval));
1292 } else {
1293 event_loop.set_control_flow(ControlFlow::WaitUntil(
1294 self.last_frame_time + target_interval,
1295 ));
1296 }
1297 }
1298}
1299
1300pub struct ShieldWall {
1301 pub(crate) proxy: EventLoopProxy<AppEvent>,
1302}
1303
1304impl accesskit::ActionHandler for ShieldWall {
1305 fn do_action(&mut self, request: accesskit::ActionRequest) {
1306 let _ = self
1307 .proxy
1308 .send_event(AppEvent::AccessibilityAction(request));
1309 }
1310}
1311
1312impl accesskit::ActivationHandler for ShieldWall {
1313 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
1314 let mut root = accesskit::Node::new(accesskit::Role::Window);
1315 root.set_label("CVKG Application");
1316
1317 let root_id = accesskit::NodeId(1);
1318 Some(accesskit::TreeUpdate {
1319 nodes: vec![(root_id, root)],
1320 tree: Some(accesskit::Tree::new(root_id)),
1321 focus: root_id,
1322 tree_id: accesskit::TreeId::ROOT,
1323 })
1324 }
1325}
1326
1327impl accesskit::DeactivationHandler for ShieldWall {
1328 fn deactivate_accessibility(&mut self) {}
1329}