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 = self
124 .window_manager
125 .core_to_winit
126 .get(&handle.id)
127 .copied()
128 .unwrap_or_else(|| {
129 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 let _guard = unsafe { GpuFramePtrGuard::set(raw) };
438 let render_start = std::time::Instant::now();
439 self.view.render(&mut renderer, content_rect);
440 let render_time = render_start.elapsed().as_secs_f32() * 1000.0;
441 if render_time > 5.0 {
443 tracing::warn!(
444 "[Native] view.render() took {:.2}ms (gpu_lock={:.2}ms, begin_frame={:.2}ms)",
445 render_time,
446 gpu_lock_time,
447 begin_frame_time
448 );
449 }
450 }
451 let cpu_draw_end = std::time::Instant::now();
452 cvkg_core::LayoutCache::clear_layout_budget_deadline();
453
454 self.frame_budget.subsystem_finish(2);
455
456 let gpu_render_start = std::time::Instant::now();
457 gpu.render_frame();
458 let gpu_render_end = std::time::Instant::now();
459
460 gpu.end_frame(encoder);
461 let gpu_submit_end = std::time::Instant::now();
462
463 if state.frame_count % 60 == 0 {
464 let cpu_draw = cpu_draw_end.duration_since(cpu_draw_start);
465 let gpu_render = gpu_render_end.duration_since(gpu_render_start);
466 let gpu_submit = gpu_submit_end.duration_since(gpu_render_end);
467 let total = gpu_submit_end.duration_since(redraw_start);
468 tracing::info!(
469 "[Native] Frame breakdown: cpu_draw={:?} gpu_render={:?} gpu_submit(end_frame)={:?} total={:?}",
470 cpu_draw,
471 gpu_render,
472 gpu_submit,
473 total
474 );
475 }
476
477 let mut telemetry = TelemetryData::default();
478 telemetry.input_time_ms =
479 redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
480 telemetry.layout_time_ms =
481 layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
482 telemetry.state_flush_time_ms = state_flush_end
483 .duration_since(state_flush_start)
484 .as_secs_f32()
485 * 1000.0;
486 telemetry.draw_time_ms =
487 cpu_draw_end.duration_since(cpu_draw_start).as_secs_f32() * 1000.0;
488 telemetry.gpu_submit_time_ms =
489 gpu_submit_end.duration_since(cpu_draw_end).as_secs_f32() * 1000.0;
490
491 let frame_time_ms =
492 gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
493 telemetry.frame_time_ms = frame_time_ms;
494 telemetry.frame_budget_ms = self.frame_budget.total().as_secs_f32() * 1000.0;
495 telemetry.frame_budget_remaining_ms =
496 telemetry.frame_budget_ms - telemetry.frame_time_ms;
497 telemetry.layout_budget_remaining_ms = self
498 .frame_budget
499 .allocations()
500 .get(1)
501 .map(|alloc| {
502 alloc.time_slice.as_secs_f32() * 1000.0 - telemetry.layout_time_ms
503 })
504 .unwrap_or(0.0);
505 telemetry.frame_over_budget = !self.frame_budget.frame_within_budget()
506 || telemetry.frame_budget_remaining_ms < 0.0;
507 telemetry.layout_over_budget = !self.frame_budget.is_within_budget(1)
508 || telemetry.layout_budget_remaining_ms < 0.0;
509
510 if telemetry.frame_over_budget {
512 tracing::warn!(
513 "[Telemetry] Frame budget exceeded by {:.2}ms (frame={:.2}ms budget={:.2}ms)",
514 telemetry.frame_time_ms - telemetry.frame_budget_ms,
515 telemetry.frame_time_ms,
516 telemetry.frame_budget_ms
517 );
518 }
519
520 tracing::info!(
521 "[Native] Frame timings: layout={:.2}ms state={:.2}ms draw={:.2}ms submit={:.2}ms total={:.2}ms",
522 telemetry.layout_time_ms,
523 telemetry.state_flush_time_ms,
524 telemetry.draw_time_ms,
525 telemetry.gpu_submit_time_ms,
526 telemetry.frame_time_ms
527 );
528
529 state.frame_history.push_back(frame_time_ms);
530 if state.frame_history.len() > 100 {
531 state.frame_history.pop_front();
532 }
533
534 let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
535 sorted_frames
536 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
537
538 if !sorted_frames.is_empty() {
539 let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
540 telemetry.p99_frame_time_ms =
541 sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
542
543 let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
544 let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
545 / sorted_frames.len() as f32;
546 telemetry.frame_jitter_ms = variance.sqrt();
547 }
548
549 telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
550 if telemetry.frame_over_budget {
551 tracing::warn!(
552 "[Native] Frame budget exceeded by {:.2}ms (layout remaining {:.2}ms)",
553 -telemetry.frame_budget_remaining_ms,
554 telemetry.layout_budget_remaining_ms
555 );
556 }
557
558 state.frame_count += 1;
559
560 telemetry.berserker_rage = self.rage;
561 gpu.telemetry = telemetry;
562
563 state.window.request_redraw();
564 }
565 WindowEvent::CursorEntered { .. } => {
566 tracing::info!("[Native] Cursor ENTERED window");
567 if let Some(vdom) = &state.vdom {
568 vdom.dispatch_event(cvkg_core::Event::PointerEnter);
569 }
570 state.window.request_redraw();
571 }
572 WindowEvent::CursorLeft { .. } => {
573 tracing::info!("[Native] Cursor LEFT window");
574 if let Some(vdom) = &state.vdom {
575 vdom.dispatch_event(cvkg_core::Event::PointerLeave);
576 }
577 state.window.request_redraw();
578 }
579 WindowEvent::CursorMoved { position, .. } => {
580 let scale = state.window.scale_factor();
581 let logical = position.to_logical::<f32>(scale);
582 let elapsed = state.last_redraw_start.elapsed().as_secs_f32().max(0.001);
583 let dx = logical.x - state.cursor_pos[0];
584 let dy = logical.y - state.cursor_pos[1];
585 state.cursor_velocity = [dx / elapsed, dy / elapsed];
586 state.cursor_pos = [logical.x, logical.y];
587 if !state.is_dragging {
588 let ddx = state.cursor_pos[0] - state.drag_start_pos[0];
589 let ddy = state.cursor_pos[1] - state.drag_start_pos[1];
590 let dist_sq = ddx * ddx + ddy * ddy;
591 if dist_sq > state.drag_threshold * state.drag_threshold {
592 state.is_dragging = true;
593 }
594 }
595 state.needs_cursor_update = true;
596 if state.frame_count == 0 {
597 state.window.request_redraw();
598 }
599 }
600 WindowEvent::MouseInput {
601 state: mouse_state,
602 button,
603 ..
604 } => {
605 tracing::info!(
606 "[Native] MOUSE INPUT: {:?} button={:?} pos={:?}",
607 mouse_state,
608 button,
609 state.cursor_pos
610 );
611 if let Some(touch_time) = state.last_touch_time {
612 if touch_time.elapsed().as_millis() < 500 {
613 tracing::info!("[Native] Ignoring MouseInput (synthesized from Touch)");
614 return;
615 }
616 }
617 if let Some(vdom) = &state.vdom {
618 let btn_id = match button {
619 winit::event::MouseButton::Left => 0,
620 winit::event::MouseButton::Right => 2,
621 winit::event::MouseButton::Middle => 1,
622 winit::event::MouseButton::Back => 3,
623 winit::event::MouseButton::Forward => 4,
624 winit::event::MouseButton::Other(id) => id as u32,
625 };
626
627 match mouse_state {
628 winit::event::ElementState::Pressed => {
629 state.drag_start_pos = state.cursor_pos;
630 state.is_dragging = false;
631 state.drag_button = btn_id;
632 state.active_pointer_pos = Some(state.cursor_pos);
633 state.active_pointer_precision = 0.0;
634 state.active_pointer_target = vdom
635 .hit_test(state.cursor_pos[0], state.cursor_pos[1], 0.0)
636 .map(|(id, _)| id);
637 if let Some(target_id) = state.active_pointer_target {
638 if let Some(node) = vdom.nodes.get(&target_id) {
639 state.active_pointer_target_type =
640 Some(node.component_type.clone());
641 state.active_pointer_target_key = node.key.clone();
642 }
643 }
644 tracing::info!("[Native] Dispatching PointerDown to VDOM");
645 vdom.dispatch_event(cvkg_core::Event::PointerDown {
646 x: state.cursor_pos[0],
647 y: state.cursor_pos[1],
648 button: btn_id,
649 proximity_field: 0.0,
650 tilt: None,
651 azimuth: None,
652 pressure: Some(1.0),
653 barrel_rotation: None,
654 pointer_precision: 0.0,
655 });
656 }
657 winit::event::ElementState::Released => {
658 tracing::info!("[Native] Dispatching PointerUp to VDOM");
659 let fallback_target = state
660 .active_pointer_pos
661 .and_then(|pos| {
662 vdom.hit_test(
663 pos[0],
664 pos[1],
665 state.active_pointer_precision,
666 )
667 .map(|(id, _)| id)
668 })
669 .or_else(|| {
670 vdom.hit_test(
671 state.cursor_pos[0],
672 state.cursor_pos[1],
673 state.active_pointer_precision,
674 )
675 .map(|(id, _)| id)
676 });
677 let target = state
678 .active_pointer_target
679 .filter(|target| {
680 if state.active_pointer_target_key.is_none() {
681 tracing::debug!("[Native] Target verification: key is None, skipping cache");
682 return false;
683 }
684 let verified = vdom.nodes.get(target).map_or(false, |node| {
685 let type_match = Some(&node.component_type) == state.active_pointer_target_type.as_ref();
686 let key_match = node.key == state.active_pointer_target_key;
687 tracing::debug!("[Native] Target verify: id={:?} type={} key={:?} type_match={} key_match={}",
688 target, node.component_type, node.key, type_match, key_match);
689 type_match && key_match
690 });
691 if !verified {
692 tracing::debug!("[Native] Target verification failed for {:?}, using fallback", target);
693 }
694 verified
695 })
696 .or(fallback_target);
697 let pointer_up = cvkg_core::Event::PointerUp {
698 x: state.cursor_pos[0],
699 y: state.cursor_pos[1],
700 button: btn_id,
701 tilt: None,
702 azimuth: None,
703 pressure: Some(0.0),
704 barrel_rotation: None,
705 pointer_precision: 0.0,
706 };
707 let pointer_click = cvkg_core::Event::PointerClick {
708 x: state.cursor_pos[0],
709 y: state.cursor_pos[1],
710 button: btn_id,
711 tilt: None,
712 azimuth: None,
713 pressure: Some(0.0),
714 barrel_rotation: None,
715 pointer_precision: 0.0,
716 };
717 if let Some(target) = target {
718 vdom.dispatch_event_to_target(target, pointer_up);
719 } else {
720 vdom.dispatch_event(pointer_up);
721 }
722 if !state.is_dragging {
723 if let Some(target) = target {
724 tracing::info!(
725 "[Native] Dispatching PointerClick to VDOM (target={:?})",
726 target
727 );
728 vdom.dispatch_event_to_target(target, pointer_click);
729 } else {
730 tracing::info!(
731 "[Native] Dispatching PointerClick to VDOM (no target, bubbling)"
732 );
733 vdom.dispatch_event(pointer_click);
734 }
735 } else {
736 tracing::info!(
737 "[Native] Skipping PointerClick (is_dragging=true)"
738 );
739 }
740 state.is_dragging = false;
741 state.active_pointer_target = None;
742 state.active_pointer_target_type = None;
743 state.active_pointer_target_key = None;
744 state.active_pointer_pos = None;
745 }
746 }
747 state.window.request_redraw();
748 } else {
749 tracing::warn!("[Native] Mouse input received but state.vdom is None!");
750 }
751 }
752 WindowEvent::MouseWheel { delta, .. } => {
753 if let Some(vdom) = &state.vdom {
754 let (dx, dy) = match delta {
755 winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
756 winit::event::MouseScrollDelta::PixelDelta(pos) => {
757 (pos.x as f32, pos.y as f32)
758 }
759 };
760 vdom.dispatch_event(cvkg_core::Event::PointerWheel {
761 x: state.cursor_pos[0],
762 y: state.cursor_pos[1],
763 delta_x: dx,
764 delta_y: dy,
765 pointer_precision: 0.0,
766 });
767 state.window.request_redraw();
768 }
769 }
770 WindowEvent::Touch(touch) => {
771 state.last_touch_time = Some(std::time::Instant::now());
772 if let Some(vdom) = &state.vdom {
773 let scale = state.window.scale_factor();
774 let logical = touch.location.to_logical::<f32>(scale);
775 let x = logical.x;
776 let y = logical.y;
777 let touch_btn = 0;
778
779 match touch.phase {
780 winit::event::TouchPhase::Started => {
781 tracing::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
782 state.drag_start_pos = [x, y];
783 state.is_dragging = false;
784 state.drag_button = touch_btn;
785 state.active_pointer_pos = Some([x, y]);
786 state.active_pointer_precision = 150.0;
787 state.active_pointer_target =
788 vdom.hit_test(x, y, 150.0).map(|(id, _)| id);
789 if let Some(target_id) = state.active_pointer_target {
790 if let Some(node) = vdom.nodes.get(&target_id) {
791 state.active_pointer_target_type =
792 Some(node.component_type.clone());
793 state.active_pointer_target_key = node.key.clone();
794 }
795 }
796 vdom.dispatch_event(cvkg_core::Event::PointerDown {
797 x,
798 y,
799 button: touch_btn,
800 proximity_field: 0.0,
801 tilt: None,
802 azimuth: None,
803 pressure: Some(
804 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
805 ),
806 barrel_rotation: None,
807 pointer_precision: 150.0,
808 });
809 }
810 winit::event::TouchPhase::Moved => {
811 if !state.is_dragging {
812 let ddx = x - state.drag_start_pos[0];
813 let ddy = y - state.drag_start_pos[1];
814 let dist_sq = ddx * ddx + ddy * ddy;
815 if dist_sq > state.drag_threshold * state.drag_threshold {
816 state.is_dragging = true;
817 }
818 }
819 vdom.dispatch_event(cvkg_core::Event::PointerMove {
820 x,
821 y,
822 proximity_field: 0.0,
823 tilt: None,
824 azimuth: None,
825 pressure: Some(
826 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
827 ),
828 barrel_rotation: None,
829 pointer_precision: 150.0,
830 });
831 }
832 winit::event::TouchPhase::Ended => {
833 let fallback_target = state
834 .active_pointer_pos
835 .and_then(|pos| {
836 vdom.hit_test(
837 pos[0],
838 pos[1],
839 state.active_pointer_precision,
840 )
841 .map(|(id, _)| id)
842 })
843 .or_else(|| {
844 vdom.hit_test(x, y, state.active_pointer_precision)
845 .map(|(id, _)| id)
846 });
847 let target = state
848 .active_pointer_target
849 .filter(|target| {
850 vdom.nodes.get(target).map_or(false, |node| {
851 Some(&node.component_type)
852 == state.active_pointer_target_type.as_ref()
853 && node.key == state.active_pointer_target_key
854 })
855 })
856 .or(fallback_target);
857 let pointer_up = cvkg_core::Event::PointerUp {
858 x,
859 y,
860 button: touch_btn,
861 tilt: None,
862 azimuth: None,
863 pressure: Some(0.0),
864 barrel_rotation: None,
865 pointer_precision: 150.0,
866 };
867 let pointer_click = cvkg_core::Event::PointerClick {
868 x,
869 y,
870 button: touch_btn,
871 tilt: None,
872 azimuth: None,
873 pressure: Some(0.0),
874 barrel_rotation: None,
875 pointer_precision: 150.0,
876 };
877 if let Some(target) = target {
878 vdom.dispatch_event_to_target(target, pointer_up);
879 } else {
880 vdom.dispatch_event(pointer_up);
881 }
882 if !state.is_dragging {
883 if let Some(target) = target {
884 tracing::info!(
885 "[Native] Dispatching PointerClick to VDOM (target={:?})",
886 target
887 );
888 vdom.dispatch_event_to_target(target, pointer_click);
889 } else {
890 tracing::info!(
891 "[Native] Dispatching PointerClick to VDOM (no target, bubbling)"
892 );
893 vdom.dispatch_event(pointer_click);
894 }
895 } else {
896 tracing::info!(
897 "[Native] Skipping PointerClick (is_dragging=true)"
898 );
899 }
900 state.is_dragging = false;
901 state.active_pointer_target = None;
902 state.active_pointer_target_type = None;
903 state.active_pointer_target_key = None;
904 state.active_pointer_pos = None;
905 }
906 winit::event::TouchPhase::Cancelled => {
907 vdom.dispatch_event(cvkg_core::Event::PointerUp {
908 x,
909 y,
910 button: touch_btn,
911 tilt: None,
912 azimuth: None,
913 pressure: Some(0.0),
914 barrel_rotation: None,
915 pointer_precision: 150.0,
916 });
917 state.active_pointer_target = None;
918 state.active_pointer_pos = None;
919 }
920 }
921 state.window.request_redraw();
922 }
923 }
924 WindowEvent::PinchGesture { delta, .. } => {
925 if let Some(vdom) = &state.vdom {
926 let scale = 1.0 + delta as f32;
927 let velocity = delta as f32;
928 vdom.dispatch_event(cvkg_core::Event::GesturePinch {
929 center: state.cursor_pos,
930 scale,
931 velocity,
932 phase: cvkg_core::TouchPhase::Moved,
933 });
934 }
935 if let Some(audio) = &self.audio_engine {
936 audio.play_sound("nav_tick", 0.3);
937 }
938 self.haptic_engine
939 .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
940 state.window.request_redraw();
941 }
942 WindowEvent::RotationGesture { delta, .. } => {
943 if let Some(vdom) = &state.vdom {
944 let angle = delta;
945 vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
946 direction: [angle.cos(), angle.sin()],
947 velocity: delta.abs(),
948 phase: cvkg_core::TouchPhase::Moved,
949 });
950 }
951 state.window.request_redraw();
952 }
953 WindowEvent::KeyboardInput { event, .. } => {
954 if event.state == winit::event::ElementState::Pressed {
955 if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
956 let is_cmd = if cfg!(target_os = "macos") {
957 self.modifiers.super_key()
958 } else {
959 self.modifiers.control_key()
960 };
961 let is_shift = self.modifiers.shift_key();
962
963 if is_cmd {
964 match code {
965 winit::keyboard::KeyCode::KeyZ => {
966 if is_shift {
967 tracing::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
968 let mut redo_action = None;
969 update_system_state(|s| {
970 let mut s = s.clone();
971 redo_action = s.undo_manager.redo();
972 s
973 });
974 if let Some(action) = redo_action {
975 action();
976 }
977 state.window.request_redraw();
978 } else {
979 tracing::info!("[Native] Shortcut: Undo (Cmd+Z)");
980 let mut undo_action = None;
981 update_system_state(|s| {
982 let mut s = s.clone();
983 undo_action = s.undo_manager.undo();
984 s
985 });
986 if let Some(action) = undo_action {
987 action();
988 }
989 state.window.request_redraw();
990 }
991 }
992 winit::keyboard::KeyCode::KeyY
993 if !cfg!(target_os = "macos") =>
994 {
995 tracing::info!("[Native] Shortcut: Redo (Ctrl+Y)");
996 let mut redo_action = None;
997 update_system_state(|s| {
998 let mut s = s.clone();
999 redo_action = s.undo_manager.redo();
1000 s
1001 });
1002 if let Some(action) = redo_action {
1003 action();
1004 }
1005 state.window.request_redraw();
1006 }
1007 winit::keyboard::KeyCode::KeyN => {
1008 tracing::info!("[Native] Shortcut: New Window (Cmd+N)");
1009 create_new_window = true;
1010 }
1011 winit::keyboard::KeyCode::KeyO => {
1012 tracing::info!("[Native] Shortcut: Open File (Cmd+O)");
1013 if let Some(vdom) = &state.vdom {
1014 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1015 key: "cmd+o".to_string(),
1016 modifiers: cvkg_core::KeyModifiers::default(),
1017 });
1018 }
1019 state.window.request_redraw();
1020 }
1021 winit::keyboard::KeyCode::KeyS => {
1022 tracing::info!("[Native] Shortcut: Save (Cmd+S)");
1023 if let Some(vdom) = &state.vdom {
1024 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1025 key: "cmd+s".to_string(),
1026 modifiers: cvkg_core::KeyModifiers::default(),
1027 });
1028 }
1029 state.window.request_redraw();
1030 }
1031 winit::keyboard::KeyCode::KeyW => {
1032 tracing::info!("[Native] Shortcut: Close Window (Cmd+W)");
1033 close_window = true;
1034 }
1035 winit::keyboard::KeyCode::KeyQ => {
1036 tracing::info!("[Native] Shortcut: Quit (Cmd+Q)");
1037 quit_all = true;
1038 }
1039 winit::keyboard::KeyCode::KeyC => {
1040 tracing::info!("[Native] Shortcut: Copy (Cmd+C)");
1041 if let Some(vdom) = &state.vdom {
1042 vdom.dispatch_event(cvkg_core::Event::Copy);
1043 }
1044 state.window.request_redraw();
1045 }
1046 winit::keyboard::KeyCode::KeyV => {
1047 tracing::info!("[Native] Shortcut: Paste (Cmd+V)");
1048 let text = arboard::Clipboard::new()
1049 .ok()
1050 .and_then(|mut cb| cb.get_text().ok())
1051 .unwrap_or_default();
1052 if let Some(vdom) = &state.vdom {
1053 vdom.dispatch_event(cvkg_core::Event::Paste(text));
1054 }
1055 state.window.request_redraw();
1056 }
1057 winit::keyboard::KeyCode::KeyX => {
1058 tracing::info!("[Native] Shortcut: Cut (Cmd+X)");
1059 if let Some(vdom) = &state.vdom {
1060 vdom.dispatch_event(cvkg_core::Event::Cut);
1061 }
1062 state.window.request_redraw();
1063 }
1064 winit::keyboard::KeyCode::F11 => {
1065 let is_fullscreen = state.window.fullscreen().is_some();
1066 if is_fullscreen {
1067 state.window.set_fullscreen(None);
1068 tracing::info!("[Native] Fullscreen OFF");
1069 } else {
1070 if let Some(monitor) = state.window.current_monitor() {
1071 if let Some(mode) = monitor.video_modes().next() {
1072 let w = mode.size().width;
1073 let h = mode.size().height;
1074 let rr = mode.refresh_rate_millihertz();
1075 state.window.set_fullscreen(Some(
1076 winit::window::Fullscreen::Exclusive(mode),
1077 ));
1078 tracing::info!(
1079 "[Native] Fullscreen ON (exclusive: {}x{}@{:?}Hz)",
1080 w,
1081 h,
1082 rr
1083 );
1084 }
1085 } else {
1086 state.window.set_fullscreen(Some(
1087 winit::window::Fullscreen::Borderless(None),
1088 ));
1089 tracing::info!(
1090 "[Native] Fullscreen ON (borderless)"
1091 );
1092 }
1093 }
1094 state.window.request_redraw();
1095 }
1096 winit::keyboard::KeyCode::KeyA => {
1097 tracing::info!("[Native] Shortcut: Select All (Cmd+A)");
1098 if let Some(vdom) = &state.vdom {
1099 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1100 key: "cmd+a".to_string(),
1101 modifiers: cvkg_core::KeyModifiers::default(),
1102 });
1103 }
1104 state.window.request_redraw();
1105 }
1106 winit::keyboard::KeyCode::KeyF => {
1107 tracing::info!("[Native] Shortcut: Find (Cmd+F)");
1108 if let Some(vdom) = &state.vdom {
1109 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1110 key: "cmd+f".to_string(),
1111 modifiers: cvkg_core::KeyModifiers::default(),
1112 });
1113 }
1114 state.window.request_redraw();
1115 }
1116 winit::keyboard::KeyCode::Tab => {
1117 if is_shift {
1118 if let Some(id) = state.focus_manager.focus_prev() {
1119 if let Ok(node_id) = id.as_str().parse::<u64>() {
1120 state.focused_node_id =
1121 Some(cvkg_core::KvasirId(node_id));
1122 tracing::info!(
1123 "[Native] Focus previous: {:?}",
1124 node_id
1125 );
1126 }
1127 }
1128 } else {
1129 if let Some(id) = state.focus_manager.focus_next() {
1130 if let Ok(node_id) = id.as_str().parse::<u64>() {
1131 state.focused_node_id =
1132 Some(cvkg_core::KvasirId(node_id));
1133 tracing::info!(
1134 "[Native] Focus next: {:?}",
1135 node_id
1136 );
1137 }
1138 }
1139 }
1140 state.window.request_redraw();
1141 }
1142 _ => {}
1143 }
1144 }
1145 }
1146 }
1147
1148 if let Some(vdom) = &state.vdom
1149 && let Some(cvkg_event) = convert_keyboard_event(event, &self.modifiers)
1150 {
1151 vdom.dispatch_event(cvkg_event);
1152 state.window.request_redraw();
1153 }
1154 }
1155 WindowEvent::Ime(ime_event) => {
1156 if let Some(vdom) = &state.vdom
1157 && let Some(cvkg_event) = convert_ime_event(ime_event)
1158 {
1159 vdom.dispatch_event(cvkg_event);
1160 state.window.request_redraw();
1161 }
1162 }
1163 WindowEvent::ModifiersChanged(new_modifiers) => {
1164 self.modifiers = new_modifiers.state();
1165 let shift = self.modifiers.shift_key();
1166 let ctrl = self.modifiers.control_key();
1167 let alt = self.modifiers.alt_key();
1168 let logo = self.modifiers.super_key();
1169 update_system_state(|st| {
1170 let mut new_st = st.clone();
1171 new_st.modifiers_shift = shift;
1172 new_st.modifiers_ctrl = ctrl;
1173 new_st.modifiers_alt = alt;
1174 new_st.modifiers_logo = logo;
1175 new_st
1176 });
1177 }
1178 WindowEvent::ScaleFactorChanged { .. } => {
1179 if let Some(ctx) = self.window_manager.windows.get(&id) {
1180 ctx.window.request_redraw();
1181 }
1182 }
1183 _ => {}
1184 }
1185 }
1186
1187 if close_window {
1188 self.window_manager.close_window(id);
1189 }
1190 if quit_all {
1191 for wid in self.window_manager.window_order().to_vec() {
1192 self.window_manager.close_window(wid);
1193 }
1194 }
1195 if self.window_manager.windows.is_empty() {
1196 event_loop.exit();
1197 }
1198 if bring_to_front {
1199 self.window_manager.bring_to_front(id);
1200 }
1201 if create_new_window {
1202 self.window_manager.create_window(
1203 event_loop,
1204 &self.gpu,
1205 self.proxy.clone(),
1206 WindowConfig {
1207 title: "New CVKG Window".to_string(),
1208 size: (800.0, 600.0),
1209 ..Default::default()
1210 },
1211 false,
1212 &self.view,
1213 );
1214 }
1215 }
1216
1217 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1218 match event {
1219 AppEvent::AccessibilityAction(request) => {
1220 let node_id = cvkg_core::KvasirId(request.target_node.0);
1221 let target_state = self.window_manager.windows.values_mut().find(|s| {
1222 s.vdom
1223 .as_ref()
1224 .map_or(false, |v| v.nodes.contains_key(&node_id))
1225 });
1226
1227 if let Some(state) = target_state
1228 && let Some(vdom) = &state.vdom
1229 && let Some(node) = vdom.nodes.get(&node_id)
1230 && request.action == accesskit::Action::Click
1231 {
1232 let event = cvkg_core::Event::PointerClick {
1233 x: node.layout.x + node.layout.width / 2.0,
1234 y: node.layout.y + node.layout.height / 2.0,
1235 button: 0,
1236 tilt: None,
1237 azimuth: None,
1238 pressure: Some(1.0),
1239 barrel_rotation: None,
1240 pointer_precision: 0.0,
1241 };
1242 vdom.dispatch_event(event);
1243 }
1244 }
1245 AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
1246 if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
1247 if let Some(vdom) = &state.vdom {
1248 let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
1249 let mut nodes = Vec::new();
1250 for (id, node) in &vdom.nodes {
1251 nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
1252 }
1253 let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
1254 if let Some(adapter) = &mut state.accesskit_adapter {
1255 adapter.update_if_active(|| accesskit::TreeUpdate {
1256 nodes,
1257 tree: Some(tree),
1258 focus: accesskit::NodeId(root_id),
1259 tree_id: accesskit::TreeId::ROOT,
1260 });
1261 }
1262 }
1263 }
1264 }
1265 AppEvent::CloseWindow(winit_id) => {
1266 self.window_manager.close_window(winit_id);
1267 if self.window_manager.windows.is_empty() {
1268 event_loop.exit();
1269 }
1270 }
1271 AppEvent::SetTitle(winit_id, title) => {
1272 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1273 data.window.set_title(&title);
1274 }
1275 }
1276 AppEvent::SetSize(winit_id, width, height) => {
1277 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1278 let _ = data
1279 .window
1280 .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1281 }
1282 }
1283 AppEvent::SetVisible(winit_id, visible) => {
1284 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1285 data.window.set_visible(visible);
1286 }
1287 }
1288 AppEvent::BringToFront(winit_id) => {
1289 self.window_manager.bring_to_front(winit_id);
1290 }
1291 }
1292 }
1293
1294 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1295 self.rage = (self.rage - 0.02).max(0.0);
1296
1297 let now = std::time::Instant::now();
1298 let target_interval = std::time::Duration::from_micros(8_333);
1299
1300 if now.duration_since(self.last_frame_time) >= target_interval {
1301 self.last_frame_time = now;
1302 let needs_redraw = self.view.changed();
1303 if needs_redraw {
1304 for window_state in self.window_manager.windows.values() {
1305 window_state.window.request_redraw();
1306 }
1307 }
1308 event_loop.set_control_flow(ControlFlow::WaitUntil(now + target_interval));
1309 } else {
1310 event_loop.set_control_flow(ControlFlow::WaitUntil(
1311 self.last_frame_time + target_interval,
1312 ));
1313 }
1314 }
1315}
1316
1317pub struct ShieldWall {
1318 pub(crate) proxy: EventLoopProxy<AppEvent>,
1319}
1320
1321impl accesskit::ActionHandler for ShieldWall {
1322 fn do_action(&mut self, request: accesskit::ActionRequest) {
1323 let _ = self
1324 .proxy
1325 .send_event(AppEvent::AccessibilityAction(request));
1326 }
1327}
1328
1329impl accesskit::ActivationHandler for ShieldWall {
1330 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
1331 let mut root = accesskit::Node::new(accesskit::Role::Window);
1332 root.set_label("CVKG Application");
1333
1334 let root_id = accesskit::NodeId(1);
1335 Some(accesskit::TreeUpdate {
1336 nodes: vec![(root_id, root)],
1337 tree: Some(accesskit::Tree::new(root_id)),
1338 focus: root_id,
1339 tree_id: accesskit::TreeId::ROOT,
1340 })
1341 }
1342}
1343
1344impl accesskit::DeactivationHandler for ShieldWall {
1345 fn deactivate_accessibility(&mut self) {}
1346}