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