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