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::{GpuFramePtrGuard, 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 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!("[Native] Skipping PointerClick (is_dragging=true)");
737 }
738 state.is_dragging = false;
739 state.active_pointer_target = None;
740 state.active_pointer_target_type = None;
741 state.active_pointer_target_key = None;
742 state.active_pointer_pos = None;
743 }
744 }
745 state.window.request_redraw();
746 } else {
747 tracing::warn!("[Native] Mouse input received but state.vdom is None!");
748 }
749 }
750 WindowEvent::MouseWheel { delta, .. } => {
751 if let Some(vdom) = &state.vdom {
752 let (dx, dy) = match delta {
753 winit::event::MouseScrollDelta::LineDelta(x, y) => (x * 10.0, y * 10.0),
754 winit::event::MouseScrollDelta::PixelDelta(pos) => {
755 (pos.x as f32, pos.y as f32)
756 }
757 };
758 vdom.dispatch_event(cvkg_core::Event::PointerWheel {
759 x: state.cursor_pos[0],
760 y: state.cursor_pos[1],
761 delta_x: dx,
762 delta_y: dy,
763 pointer_precision: 0.0,
764 });
765 state.window.request_redraw();
766 }
767 }
768 WindowEvent::Touch(touch) => {
769 state.last_touch_time = Some(std::time::Instant::now());
770 if let Some(vdom) = &state.vdom {
771 let scale = state.window.scale_factor();
772 let logical = touch.location.to_logical::<f32>(scale);
773 let x = logical.x;
774 let y = logical.y;
775 let touch_btn = 0;
776
777 match touch.phase {
778 winit::event::TouchPhase::Started => {
779 tracing::info!("[Native] Dispatching PointerDown (Touch) to VDOM");
780 state.drag_start_pos = [x, y];
781 state.is_dragging = false;
782 state.drag_button = touch_btn;
783 state.active_pointer_pos = Some([x, y]);
784 state.active_pointer_precision = 150.0;
785 state.active_pointer_target =
786 vdom.hit_test(x, y, 150.0).map(|(id, _)| id);
787 if let Some(target_id) = state.active_pointer_target {
788 if let Some(node) = vdom.nodes.get(&target_id) {
789 state.active_pointer_target_type =
790 Some(node.component_type.clone());
791 state.active_pointer_target_key = node.key.clone();
792 }
793 }
794 vdom.dispatch_event(cvkg_core::Event::PointerDown {
795 x,
796 y,
797 button: touch_btn,
798 proximity_field: 0.0,
799 tilt: None,
800 azimuth: None,
801 pressure: Some(
802 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
803 ),
804 barrel_rotation: None,
805 pointer_precision: 150.0,
806 });
807 }
808 winit::event::TouchPhase::Moved => {
809 if !state.is_dragging {
810 let ddx = x - state.drag_start_pos[0];
811 let ddy = y - state.drag_start_pos[1];
812 let dist_sq = ddx * ddx + ddy * ddy;
813 if dist_sq > state.drag_threshold * state.drag_threshold {
814 state.is_dragging = true;
815 }
816 }
817 vdom.dispatch_event(cvkg_core::Event::PointerMove {
818 x,
819 y,
820 proximity_field: 0.0,
821 tilt: None,
822 azimuth: None,
823 pressure: Some(
824 touch.force.map(|f| f.normalized() as f32).unwrap_or(0.5),
825 ),
826 barrel_rotation: None,
827 pointer_precision: 150.0,
828 });
829 }
830 winit::event::TouchPhase::Ended => {
831 let fallback_target = state
832 .active_pointer_pos
833 .and_then(|pos| {
834 vdom.hit_test(
835 pos[0],
836 pos[1],
837 state.active_pointer_precision,
838 )
839 .map(|(id, _)| id)
840 })
841 .or_else(|| {
842 vdom.hit_test(x, y, state.active_pointer_precision)
843 .map(|(id, _)| id)
844 });
845 let target = state
846 .active_pointer_target
847 .filter(|target| {
848 vdom.nodes.get(target).map_or(false, |node| {
849 Some(&node.component_type)
850 == state.active_pointer_target_type.as_ref()
851 && node.key == state.active_pointer_target_key
852 })
853 })
854 .or(fallback_target);
855 let pointer_up = cvkg_core::Event::PointerUp {
856 x,
857 y,
858 button: touch_btn,
859 tilt: None,
860 azimuth: None,
861 pressure: Some(0.0),
862 barrel_rotation: None,
863 pointer_precision: 150.0,
864 };
865 let pointer_click = cvkg_core::Event::PointerClick {
866 x,
867 y,
868 button: touch_btn,
869 tilt: None,
870 azimuth: None,
871 pressure: Some(0.0),
872 barrel_rotation: None,
873 pointer_precision: 150.0,
874 };
875 if let Some(target) = target {
876 vdom.dispatch_event_to_target(target, pointer_up);
877 } else {
878 vdom.dispatch_event(pointer_up);
879 }
880 if !state.is_dragging {
881 if let Some(target) = target {
882 tracing::info!(
883 "[Native] Dispatching PointerClick to VDOM (target={:?})",
884 target
885 );
886 vdom.dispatch_event_to_target(target, pointer_click);
887 } else {
888 tracing::info!(
889 "[Native] Dispatching PointerClick to VDOM (no target, bubbling)"
890 );
891 vdom.dispatch_event(pointer_click);
892 }
893 } else {
894 tracing::info!("[Native] Skipping PointerClick (is_dragging=true)");
895 }
896 state.is_dragging = false;
897 state.active_pointer_target = None;
898 state.active_pointer_target_type = None;
899 state.active_pointer_target_key = None;
900 state.active_pointer_pos = None;
901 }
902 winit::event::TouchPhase::Cancelled => {
903 vdom.dispatch_event(cvkg_core::Event::PointerUp {
904 x,
905 y,
906 button: touch_btn,
907 tilt: None,
908 azimuth: None,
909 pressure: Some(0.0),
910 barrel_rotation: None,
911 pointer_precision: 150.0,
912 });
913 state.active_pointer_target = None;
914 state.active_pointer_pos = None;
915 }
916 }
917 state.window.request_redraw();
918 }
919 }
920 WindowEvent::PinchGesture { delta, .. } => {
921 if let Some(vdom) = &state.vdom {
922 let scale = 1.0 + delta as f32;
923 let velocity = delta as f32;
924 vdom.dispatch_event(cvkg_core::Event::GesturePinch {
925 center: state.cursor_pos,
926 scale,
927 velocity,
928 phase: cvkg_core::TouchPhase::Moved,
929 });
930 }
931 if let Some(audio) = &self.audio_engine {
932 audio.play_sound("nav_tick", 0.3);
933 }
934 self.haptic_engine
935 .visual_tick((delta.abs() as f32 * 5.0).min(1.0));
936 state.window.request_redraw();
937 }
938 WindowEvent::RotationGesture { delta, .. } => {
939 if let Some(vdom) = &state.vdom {
940 let angle = delta;
941 vdom.dispatch_event(cvkg_core::Event::GestureSwipe {
942 direction: [angle.cos(), angle.sin()],
943 velocity: delta.abs(),
944 phase: cvkg_core::TouchPhase::Moved,
945 });
946 }
947 state.window.request_redraw();
948 }
949 WindowEvent::KeyboardInput { event, .. } => {
950 if event.state == winit::event::ElementState::Pressed {
951 if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
952 let is_cmd = if cfg!(target_os = "macos") {
953 self.modifiers.super_key()
954 } else {
955 self.modifiers.control_key()
956 };
957 let is_shift = self.modifiers.shift_key();
958
959 if is_cmd {
960 match code {
961 winit::keyboard::KeyCode::KeyZ => {
962 if is_shift {
963 tracing::info!("[Native] Shortcut: Redo (Cmd+Shift+Z)");
964 let mut redo_action = None;
965 update_system_state(|s| {
966 let mut s = s.clone();
967 redo_action = s.undo_manager.redo();
968 s
969 });
970 if let Some(action) = redo_action {
971 action();
972 }
973 state.window.request_redraw();
974 } else {
975 tracing::info!("[Native] Shortcut: Undo (Cmd+Z)");
976 let mut undo_action = None;
977 update_system_state(|s| {
978 let mut s = s.clone();
979 undo_action = s.undo_manager.undo();
980 s
981 });
982 if let Some(action) = undo_action {
983 action();
984 }
985 state.window.request_redraw();
986 }
987 }
988 winit::keyboard::KeyCode::KeyY
989 if !cfg!(target_os = "macos") =>
990 {
991 tracing::info!("[Native] Shortcut: Redo (Ctrl+Y)");
992 let mut redo_action = None;
993 update_system_state(|s| {
994 let mut s = s.clone();
995 redo_action = s.undo_manager.redo();
996 s
997 });
998 if let Some(action) = redo_action {
999 action();
1000 }
1001 state.window.request_redraw();
1002 }
1003 winit::keyboard::KeyCode::KeyN => {
1004 tracing::info!("[Native] Shortcut: New Window (Cmd+N)");
1005 create_new_window = true;
1006 }
1007 winit::keyboard::KeyCode::KeyO => {
1008 tracing::info!("[Native] Shortcut: Open File (Cmd+O)");
1009 if let Some(vdom) = &state.vdom {
1010 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1011 key: "cmd+o".to_string(),
1012 modifiers: cvkg_core::KeyModifiers::default(),
1013 });
1014 }
1015 state.window.request_redraw();
1016 }
1017 winit::keyboard::KeyCode::KeyS => {
1018 tracing::info!("[Native] Shortcut: Save (Cmd+S)");
1019 if let Some(vdom) = &state.vdom {
1020 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1021 key: "cmd+s".to_string(),
1022 modifiers: cvkg_core::KeyModifiers::default(),
1023 });
1024 }
1025 state.window.request_redraw();
1026 }
1027 winit::keyboard::KeyCode::KeyW => {
1028 tracing::info!("[Native] Shortcut: Close Window (Cmd+W)");
1029 close_window = true;
1030 }
1031 winit::keyboard::KeyCode::KeyQ => {
1032 tracing::info!("[Native] Shortcut: Quit (Cmd+Q)");
1033 quit_all = true;
1034 }
1035 winit::keyboard::KeyCode::KeyC => {
1036 tracing::info!("[Native] Shortcut: Copy (Cmd+C)");
1037 if let Some(vdom) = &state.vdom {
1038 vdom.dispatch_event(cvkg_core::Event::Copy);
1039 }
1040 state.window.request_redraw();
1041 }
1042 winit::keyboard::KeyCode::KeyV => {
1043 tracing::info!("[Native] Shortcut: Paste (Cmd+V)");
1044 let text = arboard::Clipboard::new()
1045 .ok()
1046 .and_then(|mut cb| cb.get_text().ok())
1047 .unwrap_or_default();
1048 if let Some(vdom) = &state.vdom {
1049 vdom.dispatch_event(cvkg_core::Event::Paste(text));
1050 }
1051 state.window.request_redraw();
1052 }
1053 winit::keyboard::KeyCode::KeyX => {
1054 tracing::info!("[Native] Shortcut: Cut (Cmd+X)");
1055 if let Some(vdom) = &state.vdom {
1056 vdom.dispatch_event(cvkg_core::Event::Cut);
1057 }
1058 state.window.request_redraw();
1059 }
1060 winit::keyboard::KeyCode::F11 => {
1061 let is_fullscreen = state.window.fullscreen().is_some();
1062 if is_fullscreen {
1063 state.window.set_fullscreen(None);
1064 tracing::info!("[Native] Fullscreen OFF");
1065 } else {
1066 if let Some(monitor) = state.window.current_monitor() {
1067 if let Some(mode) = monitor.video_modes().next() {
1068 let w = mode.size().width;
1069 let h = mode.size().height;
1070 let rr = mode.refresh_rate_millihertz();
1071 state.window.set_fullscreen(Some(
1072 winit::window::Fullscreen::Exclusive(mode),
1073 ));
1074 tracing::info!(
1075 "[Native] Fullscreen ON (exclusive: {}x{}@{:?}Hz)",
1076 w,
1077 h,
1078 rr
1079 );
1080 }
1081 } else {
1082 state.window.set_fullscreen(Some(
1083 winit::window::Fullscreen::Borderless(None),
1084 ));
1085 tracing::info!("[Native] Fullscreen ON (borderless)");
1086 }
1087 }
1088 state.window.request_redraw();
1089 }
1090 winit::keyboard::KeyCode::KeyA => {
1091 tracing::info!("[Native] Shortcut: Select All (Cmd+A)");
1092 if let Some(vdom) = &state.vdom {
1093 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1094 key: "cmd+a".to_string(),
1095 modifiers: cvkg_core::KeyModifiers::default(),
1096 });
1097 }
1098 state.window.request_redraw();
1099 }
1100 winit::keyboard::KeyCode::KeyF => {
1101 tracing::info!("[Native] Shortcut: Find (Cmd+F)");
1102 if let Some(vdom) = &state.vdom {
1103 vdom.dispatch_event(cvkg_core::Event::KeyDown {
1104 key: "cmd+f".to_string(),
1105 modifiers: cvkg_core::KeyModifiers::default(),
1106 });
1107 }
1108 state.window.request_redraw();
1109 }
1110 winit::keyboard::KeyCode::Tab => {
1111 if is_shift {
1112 if let Some(id) = state.focus_manager.focus_prev() {
1113 if let Ok(node_id) = id.as_str().parse::<u64>() {
1114 state.focused_node_id =
1115 Some(cvkg_core::KvasirId(node_id));
1116 tracing::info!(
1117 "[Native] Focus previous: {:?}",
1118 node_id
1119 );
1120 }
1121 }
1122 } else {
1123 if let Some(id) = state.focus_manager.focus_next() {
1124 if let Ok(node_id) = id.as_str().parse::<u64>() {
1125 state.focused_node_id =
1126 Some(cvkg_core::KvasirId(node_id));
1127 tracing::info!(
1128 "[Native] Focus next: {:?}",
1129 node_id
1130 );
1131 }
1132 }
1133 }
1134 state.window.request_redraw();
1135 }
1136 _ => {}
1137 }
1138 }
1139 }
1140 }
1141
1142 if let Some(vdom) = &state.vdom
1143 && let Some(cvkg_event) = convert_keyboard_event(event, &self.modifiers)
1144 {
1145 vdom.dispatch_event(cvkg_event);
1146 state.window.request_redraw();
1147 }
1148 }
1149 WindowEvent::Ime(ime_event) => {
1150 if let Some(vdom) = &state.vdom
1151 && let Some(cvkg_event) = convert_ime_event(ime_event)
1152 {
1153 vdom.dispatch_event(cvkg_event);
1154 state.window.request_redraw();
1155 }
1156 }
1157 WindowEvent::ModifiersChanged(new_modifiers) => {
1158 self.modifiers = new_modifiers.state();
1159 let shift = self.modifiers.shift_key();
1160 let ctrl = self.modifiers.control_key();
1161 let alt = self.modifiers.alt_key();
1162 let logo = self.modifiers.super_key();
1163 update_system_state(|st| {
1164 let mut new_st = st.clone();
1165 new_st.modifiers_shift = shift;
1166 new_st.modifiers_ctrl = ctrl;
1167 new_st.modifiers_alt = alt;
1168 new_st.modifiers_logo = logo;
1169 new_st
1170 });
1171 }
1172 WindowEvent::ScaleFactorChanged { .. } => {
1173 if let Some(ctx) = self.window_manager.windows.get(&id) {
1174 ctx.window.request_redraw();
1175 }
1176 }
1177 _ => {}
1178 }
1179 }
1180
1181 if close_window {
1182 self.window_manager.close_window(id);
1183 }
1184 if quit_all {
1185 for wid in self.window_manager.window_order().to_vec() {
1186 self.window_manager.close_window(wid);
1187 }
1188 }
1189 if self.window_manager.windows.is_empty() {
1190 event_loop.exit();
1191 }
1192 if bring_to_front {
1193 self.window_manager.bring_to_front(id);
1194 }
1195 if create_new_window {
1196 self.window_manager.create_window(
1197 event_loop,
1198 &self.gpu,
1199 self.proxy.clone(),
1200 WindowConfig {
1201 title: "New CVKG Window".to_string(),
1202 size: (800.0, 600.0),
1203 ..Default::default()
1204 },
1205 false,
1206 &self.view,
1207 );
1208 }
1209 }
1210
1211 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
1212 match event {
1213 AppEvent::AccessibilityAction(request) => {
1214 let node_id = cvkg_core::KvasirId(request.target_node.0);
1215 let target_state = self.window_manager.windows.values_mut().find(|s| {
1216 s.vdom
1217 .as_ref()
1218 .map_or(false, |v| v.nodes.contains_key(&node_id))
1219 });
1220
1221 if let Some(state) = target_state
1222 && let Some(vdom) = &state.vdom
1223 && let Some(node) = vdom.nodes.get(&node_id)
1224 && request.action == accesskit::Action::Click
1225 {
1226 let event = cvkg_core::Event::PointerClick {
1227 x: node.layout.x + node.layout.width / 2.0,
1228 y: node.layout.y + node.layout.height / 2.0,
1229 button: 0,
1230 tilt: None,
1231 azimuth: None,
1232 pressure: Some(1.0),
1233 barrel_rotation: None,
1234 pointer_precision: 0.0,
1235 };
1236 vdom.dispatch_event(event);
1237 }
1238 }
1239 AppEvent::AccessibilityInitialTreeRequested(winit_id) => {
1240 if let Some(state) = self.window_manager.windows.get_mut(&winit_id) {
1241 if let Some(vdom) = &state.vdom {
1242 let root_id = vdom.root.map(|id| id.0).unwrap_or(1);
1243 let mut nodes = Vec::new();
1244 for (id, node) in &vdom.nodes {
1245 nodes.push((accesskit::NodeId(id.0), node.to_accesskit_node()));
1246 }
1247 let tree = accesskit::Tree::new(accesskit::NodeId(root_id));
1248 if let Some(adapter) = &mut state.accesskit_adapter {
1249 adapter.update_if_active(|| accesskit::TreeUpdate {
1250 nodes,
1251 tree: Some(tree),
1252 focus: accesskit::NodeId(root_id),
1253 tree_id: accesskit::TreeId::ROOT,
1254 });
1255 }
1256 }
1257 }
1258 }
1259 AppEvent::CloseWindow(winit_id) => {
1260 self.window_manager.close_window(winit_id);
1261 if self.window_manager.windows.is_empty() {
1262 event_loop.exit();
1263 }
1264 }
1265 AppEvent::SetTitle(winit_id, title) => {
1266 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1267 data.window.set_title(&title);
1268 }
1269 }
1270 AppEvent::SetSize(winit_id, width, height) => {
1271 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1272 let _ = data
1273 .window
1274 .request_inner_size(winit::dpi::LogicalSize::new(width, height));
1275 }
1276 }
1277 AppEvent::SetVisible(winit_id, visible) => {
1278 if let Some(data) = self.window_manager.windows.get(&winit_id) {
1279 data.window.set_visible(visible);
1280 }
1281 }
1282 AppEvent::BringToFront(winit_id) => {
1283 self.window_manager.bring_to_front(winit_id);
1284 }
1285 }
1286 }
1287
1288 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1289 self.rage = (self.rage - 0.02).max(0.0);
1290
1291 let now = std::time::Instant::now();
1292 let target_interval = std::time::Duration::from_micros(8_333);
1293
1294 if now.duration_since(self.last_frame_time) >= target_interval {
1295 self.last_frame_time = now;
1296 let needs_redraw = self.view.changed();
1297 if needs_redraw {
1298 for window_state in self.window_manager.windows.values() {
1299 window_state.window.request_redraw();
1300 }
1301 }
1302 event_loop.set_control_flow(ControlFlow::WaitUntil(now + target_interval));
1303 } else {
1304 event_loop.set_control_flow(ControlFlow::WaitUntil(
1305 self.last_frame_time + target_interval,
1306 ));
1307 }
1308 }
1309}
1310
1311pub struct ShieldWall {
1312 pub(crate) proxy: EventLoopProxy<AppEvent>,
1313}
1314
1315impl accesskit::ActionHandler for ShieldWall {
1316 fn do_action(&mut self, request: accesskit::ActionRequest) {
1317 let _ = self
1318 .proxy
1319 .send_event(AppEvent::AccessibilityAction(request));
1320 }
1321}
1322
1323impl accesskit::ActivationHandler for ShieldWall {
1324 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
1325 let mut root = accesskit::Node::new(accesskit::Role::Window);
1326 root.set_label("CVKG Application");
1327
1328 let root_id = accesskit::NodeId(1);
1329 Some(accesskit::TreeUpdate {
1330 nodes: vec![(root_id, root)],
1331 tree: Some(accesskit::Tree::new(root_id)),
1332 focus: root_id,
1333 tree_id: accesskit::TreeId::ROOT,
1334 })
1335 }
1336}
1337
1338impl accesskit::DeactivationHandler for ShieldWall {
1339 fn deactivate_accessibility(&mut self) {}
1340}