1use std::sync::Arc;
2use winit::event::WindowEvent;
3use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
4use winit::window::{Window, WindowId};
5
6use crate::main_loop::AppEvent;
7use cvkg_core::{FocusManager, FocusableId, WindowConfig, WindowHandle, WindowId as CoreWindowId};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum WindowState {
15 Normal,
17 Minimized,
19 Fullscreen,
21 SplitView,
23 Occluded,
25 Hidden,
27}
28
29pub struct WindowStateDetector {
34 state: WindowState,
35 is_key: bool,
36 is_main: bool,
37}
38
39impl WindowStateDetector {
40 pub fn new() -> Self {
42 Self {
43 state: WindowState::Normal,
44 is_key: false,
45 is_main: false,
46 }
47 }
48
49 pub fn state(&self) -> WindowState {
51 self.state
52 }
53
54 pub fn is_key(&self) -> bool {
56 self.is_key
57 }
58
59 pub fn is_main(&self) -> bool {
61 self.is_main
62 }
63
64 pub fn update_from_event(&mut self, event: &WindowEvent) -> Option<WindowState> {
66 let old_state = self.state;
67 match event {
68 WindowEvent::Occluded(true) => {
69 self.state = WindowState::Occluded;
70 }
71 WindowEvent::Focused(focused) => {
72 self.is_key = *focused;
73 if !focused && self.state != WindowState::Minimized {
74 self.state = WindowState::Normal;
75 }
76 }
77 _ => {}
78 };
79 if self.state != old_state {
80 Some(self.state)
81 } else {
82 None
83 }
84 }
85
86 pub fn update_from_window(&mut self, window: &Window) -> Option<WindowState> {
88 let old_state = self.state;
89 if window.is_minimized().unwrap_or(false) {
90 self.state = WindowState::Minimized;
91 } else if window.fullscreen().is_some() {
92 self.state = WindowState::Fullscreen;
93 } else if self.state == WindowState::Minimized || self.state == WindowState::Fullscreen {
94 self.state = WindowState::Normal;
95 }
96 if self.state != old_state {
97 Some(self.state)
98 } else {
99 None
100 }
101 }
102
103 pub fn should_render(&self) -> bool {
105 !matches!(
106 self.state,
107 WindowState::Occluded | WindowState::Minimized | WindowState::Hidden
108 )
109 }
110
111 pub fn control_flow(&self) -> ControlFlow {
113 if self.should_render() {
114 ControlFlow::Poll
115 } else {
116 ControlFlow::Wait
117 }
118 }
119}
120
121impl Default for WindowStateDetector {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127pub struct ResizeHitTest {
129 window_size: winit::dpi::PhysicalSize<u32>,
130 corner_radius: f32,
131 expansion: f32,
132}
133
134impl ResizeHitTest {
135 pub fn new(
137 window_size: winit::dpi::PhysicalSize<u32>,
138 corner_radius: f32,
139 expansion: f32,
140 ) -> Self {
141 Self {
142 window_size,
143 corner_radius,
144 expansion,
145 }
146 }
147
148 pub fn hit_test(&self, pos: winit::dpi::PhysicalPosition<f32>, corner_radius: f32) -> bool {
150 let r = corner_radius + self.expansion;
151 let w = self.window_size.width as f32;
152 let h = self.window_size.height as f32;
153 let px = pos.x;
154 let py = pos.y;
155
156 if px <= r && py <= r {
157 return true;
158 }
159 if px >= w - r && py <= r {
160 return true;
161 }
162 if px <= r && py >= h - r {
163 return true;
164 }
165 if px >= w - r && py >= h - r {
166 return true;
167 }
168 false
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct SafeAreaInsets {
175 pub top: f32,
177 pub bottom: f32,
179 pub left: f32,
181 pub right: f32,
183}
184
185impl SafeAreaInsets {
186 pub fn zero() -> Self {
188 Self {
189 top: 0.0,
190 bottom: 0.0,
191 left: 0.0,
192 right: 0.0,
193 }
194 }
195
196 pub fn for_window_state(state: WindowState) -> Self {
198 if state == WindowState::Fullscreen {
199 return Self::zero();
200 }
201 #[cfg(target_os = "macos")]
202 let top = 24.0;
203 #[cfg(not(target_os = "macos"))]
204 let top = 0.0;
205 Self {
206 top,
207 bottom: 0.0,
208 left: 0.0,
209 right: 0.0,
210 }
211 }
212}
213
214pub struct NativeWindowWrapper {
216 pub(crate) winit_id: WindowId,
217 pub(crate) window: Arc<Window>,
218 pub(crate) proxy: EventLoopProxy<AppEvent>,
219 pub(crate) is_key: Arc<std::sync::atomic::AtomicBool>,
220 pub(crate) is_main: bool,
221}
222
223impl cvkg_core::Window for NativeWindowWrapper {
224 fn close(&self) {
225 let _ = self.proxy.send_event(AppEvent::CloseWindow(self.winit_id));
226 }
227
228 fn set_title(&self, title: &str) {
229 let _ = self
230 .proxy
231 .send_event(AppEvent::SetTitle(self.winit_id, title.to_string()));
232 }
233
234 fn set_size(&self, width: f32, height: f32) {
235 let _ = self
236 .proxy
237 .send_event(AppEvent::SetSize(self.winit_id, width, height));
238 }
239
240 fn is_key(&self) -> bool {
241 self.is_key.load(std::sync::atomic::Ordering::SeqCst)
242 }
243
244 fn is_main(&self) -> bool {
245 self.is_main
246 }
247
248 fn is_visible(&self) -> bool {
249 self.window.is_visible().unwrap_or(false)
250 }
251
252 fn set_visible(&self, visible: bool) {
253 let _ = self
254 .proxy
255 .send_event(AppEvent::SetVisible(self.winit_id, visible));
256 }
257
258 fn bring_to_front(&self) {
259 let _ = self.proxy.send_event(AppEvent::BringToFront(self.winit_id));
260 }
261}
262
263pub struct WindowManager {
265 pub windows: std::collections::HashMap<WindowId, WindowData>,
266 pub window_stack: Vec<WindowId>,
267 pub winit_to_core: std::collections::HashMap<WindowId, CoreWindowId>,
268 pub core_to_winit: std::collections::HashMap<CoreWindowId, WindowId>,
269 pub next_core_id: u64,
270}
271
272impl Default for WindowManager {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278impl WindowManager {
279 pub fn new() -> Self {
280 Self {
281 windows: std::collections::HashMap::new(),
282 window_stack: Vec::new(),
283 winit_to_core: std::collections::HashMap::new(),
284 core_to_winit: std::collections::HashMap::new(),
285 next_core_id: 1,
286 }
287 }
288
289 pub fn create_window(
290 &mut self,
291 event_loop: &ActiveEventLoop,
292 gpu: &Option<Arc<std::sync::Mutex<cvkg_render_gpu::GpuRenderer>>>,
293 proxy: EventLoopProxy<AppEvent>,
294 config: WindowConfig,
295 is_main: bool,
296 view: &impl cvkg_core::View,
297 ) -> WindowHandle {
298 let mut window_attrs = Window::default_attributes()
299 .with_title(&config.title)
300 .with_visible(true)
301 .with_transparent(config.transparent)
302 .with_decorations(config.decorations)
303 .with_inner_size(winit::dpi::LogicalSize::new(config.size.0, config.size.1));
304
305 if let Some(min) = config.min_size {
306 window_attrs =
307 window_attrs.with_min_inner_size(winit::dpi::LogicalSize::new(min.0, min.1));
308 }
309 if let Some(max) = config.max_size {
310 window_attrs =
311 window_attrs.with_max_inner_size(winit::dpi::LogicalSize::new(max.0, max.1));
312 }
313
314 let winit_level = match config.level {
315 cvkg_core::WindowLevel::Normal => winit::window::WindowLevel::Normal,
316 cvkg_core::WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
317 cvkg_core::WindowLevel::PopUpMenu => winit::window::WindowLevel::AlwaysOnTop,
318 };
319 window_attrs = window_attrs.with_window_level(winit_level);
320
321 #[cfg(target_os = "macos")]
322 {
323 use winit::platform::macos::WindowAttributesExtMacOS;
324 window_attrs = window_attrs
325 .with_titlebar_transparent(true)
326 .with_title_hidden(true)
327 .with_fullsize_content_view(true)
328 .with_has_shadow(true);
329 }
330
331 #[cfg(target_os = "windows")]
332 {
333 use winit::platform::windows::WindowAttributesExtWindows;
334 window_attrs = window_attrs.with_undecorated_shadow(true);
335 }
336
337 let window = Arc::new(
338 event_loop
339 .create_window(window_attrs)
340 .expect("failed to create native window"),
341 );
342
343 let winit_id = window.id();
344 let core_id = CoreWindowId(self.next_core_id);
345 self.next_core_id += 1;
346
347 let is_key_focused = Arc::new(std::sync::atomic::AtomicBool::new(true));
348
349 let wrapper = Arc::new(NativeWindowWrapper {
350 winit_id,
351 window: window.clone(),
352 proxy: proxy.clone(),
353 is_key: is_key_focused.clone(),
354 is_main,
355 });
356
357 let handle = WindowHandle::new(core_id, wrapper);
358
359 let vdom = cvkg_vdom::VDom::build(
360 view,
361 cvkg_core::Rect::new(0.0, 0.0, config.size.0, config.size.1),
362 );
363
364 #[cfg(target_os = "linux")]
365 {
366 tracing::info!("[Accessibility] AT-SPI backend available (accesskit_unix)");
367 }
368
369 let accesskit_adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
370 event_loop,
371 &window,
372 proxy.clone(),
373 ));
374
375 let data = WindowData {
376 window: window.clone(),
377 accesskit_adapter,
378 vdom: Some(vdom),
379 cursor_pos: [0.0, 0.0],
380 cursor_velocity: [0.0, 0.0],
381 last_redraw_start: std::time::Instant::now(),
382 frame_history: std::collections::VecDeque::with_capacity(60),
383 frame_count: 0,
384 last_pos: None,
385 needs_cursor_update: false,
386 is_dragging: false,
387 drag_start_pos: [0.0, 0.0],
388 drag_button: 0,
389 drag_threshold: 5.0,
390 active_pointer_target: None,
391 active_pointer_target_type: None,
392 active_pointer_target_key: None,
393 active_pointer_pos: None,
394 active_pointer_precision: 0.0,
395 is_key_focused,
396 is_main,
397 core_id,
398 window_handle: handle.clone(),
399 focus_manager: FocusManager::new(),
400 focused_node_id: None,
401 last_touch_time: None,
402 last_bounds: None,
403 };
404
405 self.windows.insert(winit_id, data);
406 self.window_stack.push(winit_id);
407 self.winit_to_core.insert(winit_id, core_id);
408 self.core_to_winit.insert(core_id, winit_id);
409
410 if let Some(gpu_mutex) = gpu {
411 gpu_mutex
412 .lock()
413 .unwrap_or_else(|p| p.into_inner())
414 .register_window(window.clone());
415 }
416
417 handle
418 }
419
420 pub fn close_window(&mut self, winit_id: WindowId) {
421 self.windows.remove(&winit_id);
422 self.window_stack.retain(|id| *id != winit_id);
423 if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
424 self.core_to_winit.remove(&core_id);
425 }
426 }
427
428 pub fn bring_to_front(&mut self, winit_id: WindowId) {
429 self.window_stack.retain(|id| *id != winit_id);
430 self.window_stack.push(winit_id);
431 if let Some(data) = self.windows.get(&winit_id) {
432 data.window.focus_window();
433 }
434 }
435
436 pub fn window(&self, winit_id: WindowId) -> Option<&WindowData> {
437 self.windows.get(&winit_id)
438 }
439
440 pub fn window_mut(&mut self, winit_id: WindowId) -> Option<&mut WindowData> {
441 self.windows.get_mut(&winit_id)
442 }
443
444 pub fn window_order(&self) -> &[WindowId] {
445 &self.window_stack
446 }
447}
448
449pub struct WindowData {
450 pub(crate) window: Arc<Window>,
451 pub(crate) accesskit_adapter: Option<accesskit_winit::Adapter>,
452 pub(crate) vdom: Option<cvkg_vdom::VDom>,
453 pub(crate) cursor_pos: [f32; 2],
454 pub(crate) cursor_velocity: [f32; 2],
455 pub(crate) last_redraw_start: std::time::Instant,
456 pub(crate) frame_history: std::collections::VecDeque<f32>,
457 pub(crate) frame_count: u64,
458 pub(crate) last_pos: Option<[i32; 2]>,
459 pub(crate) needs_cursor_update: bool,
460 pub(crate) is_dragging: bool,
461 pub(crate) drag_start_pos: [f32; 2],
462 pub(crate) drag_button: u32,
463 pub(crate) drag_threshold: f32,
464 pub(crate) active_pointer_target: Option<cvkg_vdom::NodeId>,
465 pub(crate) active_pointer_target_type: Option<String>,
466 pub(crate) active_pointer_target_key: Option<String>,
467 pub(crate) active_pointer_pos: Option<[f32; 2]>,
468 pub(crate) active_pointer_precision: f32,
469 pub(crate) is_key_focused: Arc<std::sync::atomic::AtomicBool>,
470 pub(crate) is_main: bool,
471 pub(crate) core_id: CoreWindowId,
472 pub(crate) window_handle: WindowHandle,
473 pub(crate) focus_manager: FocusManager,
474 pub(crate) focused_node_id: Option<cvkg_vdom::NodeId>,
475 pub(crate) last_touch_time: Option<std::time::Instant>,
476 pub(crate) last_bounds: Option<cvkg_core::Rect>,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum WindowType {
486 Document,
487 Panel,
488 Popover,
489 Dialog,
490 Tooltip,
491}
492
493#[derive(Debug, Clone)]
495pub struct WindowCapabilityMatrix {
496 pub platform: &'static str,
497 pub window_types: Vec<WindowType>,
498 pub tabbed_windows: bool,
499 pub tiled_windows: bool,
500 pub floating_panels: bool,
501 pub sheets: bool,
502}
503
504impl WindowCapabilityMatrix {
505 pub fn for_current_platform() -> Self {
506 #[cfg(target_os = "macos")]
507 return Self {
508 platform: "macOS",
509 window_types: vec![
510 WindowType::Document,
511 WindowType::Panel,
512 WindowType::Popover,
513 WindowType::Dialog,
514 WindowType::Tooltip,
515 ],
516 tabbed_windows: true,
517 tiled_windows: true,
518 floating_panels: true,
519 sheets: true,
520 };
521
522 #[cfg(target_os = "windows")]
523 return Self {
524 platform: "Windows",
525 window_types: vec![
526 WindowType::Document,
527 WindowType::Panel,
528 WindowType::Dialog,
529 WindowType::Tooltip,
530 ],
531 tabbed_windows: true,
532 tiled_windows: true,
533 floating_panels: true,
534 sheets: false,
535 };
536
537 #[cfg(target_os = "linux")]
538 return Self {
539 platform: "Linux",
540 window_types: vec![
541 WindowType::Document,
542 WindowType::Panel,
543 WindowType::Dialog,
544 WindowType::Tooltip,
545 ],
546 tabbed_windows: false,
547 tiled_windows: true,
548 floating_panels: true,
549 sheets: false,
550 };
551
552 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
553 return Self {
554 platform: "Unknown",
555 window_types: vec![WindowType::Document],
556 tabbed_windows: false,
557 tiled_windows: false,
558 floating_panels: false,
559 sheets: false,
560 };
561 }
562}
563
564#[derive(Debug, Clone)]
566pub struct MonitorConfig {
567 pub name: String,
568 pub position: (i32, i32),
569 pub size: (u32, u32),
570 pub scale_factor: f64,
571 pub refresh_rate: u32,
572}
573
574#[derive(Debug, Clone)]
576pub struct MultiMonitorManager {
577 monitors: Vec<MonitorConfig>,
578 current_monitor_index: usize,
579}
580
581impl MultiMonitorManager {
582 pub fn new(mut monitors: Vec<MonitorConfig>) -> Self {
583 if monitors.is_empty() {
584 monitors.push(MonitorConfig {
585 name: "Default".to_string(),
586 position: (0, 0),
587 size: (1920, 1080),
588 scale_factor: 1.0,
589 refresh_rate: 60,
590 });
591 }
592 Self {
593 monitors,
594 current_monitor_index: 0,
595 }
596 }
597
598 pub fn current_monitor(&self) -> &MonitorConfig {
599 &self.monitors[self.current_monitor_index]
600 }
601
602 pub fn monitors(&self) -> &[MonitorConfig] {
603 &self.monitors
604 }
605
606 pub fn update_window_position(&mut self, window_rect: (i32, i32, u32, u32)) -> Option<usize> {
607 let center_x = window_rect.0 + (window_rect.2 as i32 / 2);
608 let center_y = window_rect.1 + (window_rect.3 as i32 / 2);
609
610 let mut best_index = None;
611 let mut min_distance = f64::MAX;
612
613 for (i, m) in self.monitors.iter().enumerate() {
614 let left = m.position.0;
615 let right = m.position.0 + m.size.0 as i32;
616 let top = m.position.1;
617 let bottom = m.position.1 + m.size.1 as i32;
618
619 if center_x >= left && center_x < right && center_y >= top && center_y < bottom {
620 self.current_monitor_index = i;
621 return Some(i);
622 }
623
624 let m_center_x = m.position.0 + (m.size.0 as i32 / 2);
625 let m_center_y = m.position.1 + (m.size.1 as i32 / 2);
626 let dx = (center_x - m_center_x) as f64;
627 let dy = (center_y - m_center_y) as f64;
628 let dist = (dx * dx + dy * dy).sqrt();
629 if dist < min_distance {
630 min_distance = dist;
631 best_index = Some(i);
632 }
633 }
634
635 if let Some(i) = best_index {
636 self.current_monitor_index = i;
637 Some(i)
638 } else {
639 None
640 }
641 }
642
643 pub fn scale_dimensions(&self, logical_width: f64, logical_height: f64) -> (u32, u32) {
644 let sf = self.current_monitor().scale_factor;
645 (
646 (logical_width * sf).round() as u32,
647 (logical_height * sf).round() as u32,
648 )
649 }
650
651 pub fn requires_dpi_adaptation(&self, from_index: usize, to_index: usize) -> bool {
652 if from_index < self.monitors.len() && to_index < self.monitors.len() {
653 (self.monitors[from_index].scale_factor - self.monitors[to_index].scale_factor).abs()
654 > f64::EPSILON
655 } else {
656 false
657 }
658 }
659}