Skip to main content

cvkg_render_native/
window.rs

1use std::sync::Arc;
2use winit::event::WindowEvent;
3use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
4use winit::window::{Window, WindowId};
5
6use cvkg_core::{FocusableId, FocusManager, WindowConfig, WindowHandle, WindowId as CoreWindowId};
7use crate::main_loop::AppEvent;
8
9/// Represents the current state of a window.
10///
11/// Used by [`WindowStateDetector`] to track lifecycle transitions and drive
12/// rendering decisions (e.g., skip frames when occluded or minimized).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum WindowState {
15    /// Window is visible and active.
16    Normal,
17    /// Window is minimized to the Dock or taskbar.
18    Minimized,
19    /// Window is in fullscreen mode.
20    Fullscreen,
21    /// Window is in Split View (side-by-side with another window).
22    SplitView,
23    /// Window is occluded by another window.
24    Occluded,
25    /// Window is hidden (ordered out).
26    Hidden,
27}
28
29/// Tracks the current [`WindowState`] based on incoming winit [`WindowEvent`]s.
30///
31/// The detector maps raw winit events to high-level window states and exposes
32/// helpers for render-loop decisions ([`should_render`], [`control_flow`]).
33pub struct WindowStateDetector {
34    state: WindowState,
35    is_key: bool,
36    is_main: bool,
37}
38
39impl WindowStateDetector {
40    /// Creates a new detector initialized to [`WindowState::Normal`].
41    pub fn new() -> Self {
42        Self {
43            state: WindowState::Normal,
44            is_key: false,
45            is_main: false,
46        }
47    }
48
49    /// Returns the current window state.
50    pub fn state(&self) -> WindowState {
51        self.state
52    }
53
54    /// Returns whether the window is the key (first responder) window.
55    pub fn is_key(&self) -> bool {
56        self.is_key
57    }
58
59    /// Returns whether the window is the main window.
60    pub fn is_main(&self) -> bool {
61        self.is_main
62    }
63
64    /// Updates the internal state based on a winit [`WindowEvent`].
65    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    /// Updates the state by querying the winit `Window` directly.
87    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    /// Returns `true` if the window should render a frame in the current state.
104    pub fn should_render(&self) -> bool {
105        !matches!(
106            self.state,
107            WindowState::Occluded | WindowState::Minimized | WindowState::Hidden
108        )
109    }
110
111    /// Returns the appropriate [`ControlFlow`] for the current state.
112    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
127/// Hit-test helper for resize handles on windows with rounded corners.
128pub struct ResizeHitTest {
129    window_size: winit::dpi::PhysicalSize<u32>,
130    corner_radius: f32,
131    expansion: f32,
132}
133
134impl ResizeHitTest {
135    /// Creates a new hit-test helper.
136    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    /// Tests whether `pos` falls within the expanded resize-hit region.
149    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 as f32;
154        let py = pos.y as f32;
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/// Platform safe area insets (menu bar, notch, etc.).
173#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct SafeAreaInsets {
175    /// Top inset (e.g., menu bar on macOS).
176    pub top: f32,
177    /// Bottom inset (e.g., Dock when at bottom).
178    pub bottom: f32,
179    /// Left inset.
180    pub left: f32,
181    /// Right inset.
182    pub right: f32,
183}
184
185impl SafeAreaInsets {
186    /// Returns zero insets on all sides.
187    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    /// Returns appropriate safe-area insets for a given [`WindowState`].
197    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
214/// Native implementation of the cvkg_core::Window trait.
215pub 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
263/// Dynamic manager for all active native windows and their rendering contexts.
264pub 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            log::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        };
403
404        self.windows.insert(winit_id, data);
405        self.window_stack.push(winit_id);
406        self.winit_to_core.insert(winit_id, core_id);
407        self.core_to_winit.insert(core_id, winit_id);
408
409        if let Some(gpu_mutex) = gpu {
410            gpu_mutex.lock().unwrap_or_else(|p| p.into_inner()).register_window(window.clone());
411        }
412
413        handle
414    }
415
416    pub fn close_window(&mut self, winit_id: WindowId) {
417        self.windows.remove(&winit_id);
418        self.window_stack.retain(|id| *id != winit_id);
419        if let Some(core_id) = self.winit_to_core.remove(&winit_id) {
420            self.core_to_winit.remove(&core_id);
421        }
422    }
423
424    pub fn bring_to_front(&mut self, winit_id: WindowId) {
425        self.window_stack.retain(|id| *id != winit_id);
426        self.window_stack.push(winit_id);
427        if let Some(data) = self.windows.get(&winit_id) {
428            data.window.focus_window();
429        }
430    }
431
432    pub fn window(&self, winit_id: WindowId) -> Option<&WindowData> {
433        self.windows.get(&winit_id)
434    }
435
436    pub fn window_mut(&mut self, winit_id: WindowId) -> Option<&mut WindowData> {
437        self.windows.get_mut(&winit_id)
438    }
439
440    pub fn window_order(&self) -> &[WindowId] {
441        &self.window_stack
442    }
443}
444
445pub struct WindowData {
446    pub(crate) window: Arc<Window>,
447    pub(crate) accesskit_adapter: Option<accesskit_winit::Adapter>,
448    pub(crate) vdom: Option<cvkg_vdom::VDom>,
449    pub(crate) cursor_pos: [f32; 2],
450    pub(crate) cursor_velocity: [f32; 2],
451    pub(crate) last_redraw_start: std::time::Instant,
452    pub(crate) frame_history: std::collections::VecDeque<f32>,
453    pub(crate) frame_count: u64,
454    pub(crate) last_pos: Option<[i32; 2]>,
455    pub(crate) needs_cursor_update: bool,
456    pub(crate) is_dragging: bool,
457    pub(crate) drag_start_pos: [f32; 2],
458    pub(crate) drag_button: u32,
459    pub(crate) drag_threshold: f32,
460    pub(crate) active_pointer_target: Option<cvkg_vdom::NodeId>,
461    pub(crate) active_pointer_target_type: Option<String>,
462    pub(crate) active_pointer_target_key: Option<String>,
463    pub(crate) active_pointer_pos: Option<[f32; 2]>,
464    pub(crate) active_pointer_precision: f32,
465    pub(crate) is_key_focused: Arc<std::sync::atomic::AtomicBool>,
466    pub(crate) is_main: bool,
467    pub(crate) core_id: CoreWindowId,
468    pub(crate) window_handle: WindowHandle,
469    pub(crate) focus_manager: FocusManager,
470    pub(crate) focused_node_id: Option<cvkg_vdom::NodeId>,
471    pub(crate) last_touch_time: Option<std::time::Instant>,
472}
473
474// =============================================================================
475// Window Capability Matrix and Multi-Monitor configurations
476// =============================================================================
477
478/// Window type.
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum WindowType {
481    Document,
482    Panel,
483    Popover,
484    Dialog,
485    Tooltip,
486}
487
488/// Window capability matrix per platform.
489#[derive(Debug, Clone)]
490pub struct WindowCapabilityMatrix {
491    pub platform: &'static str,
492    pub window_types: Vec<WindowType>,
493    pub tabbed_windows: bool,
494    pub tiled_windows: bool,
495    pub floating_panels: bool,
496    pub sheets: bool,
497}
498
499impl WindowCapabilityMatrix {
500    pub fn for_current_platform() -> Self {
501        #[cfg(target_os = "macos")]
502        return Self {
503            platform: "macOS",
504            window_types: vec![
505                WindowType::Document,
506                WindowType::Panel,
507                WindowType::Popover,
508                WindowType::Dialog,
509                WindowType::Tooltip,
510            ],
511            tabbed_windows: true,
512            tiled_windows: true,
513            floating_panels: true,
514            sheets: true,
515        };
516
517        #[cfg(target_os = "windows")]
518        return Self {
519            platform: "Windows",
520            window_types: vec![
521                WindowType::Document,
522                WindowType::Panel,
523                WindowType::Dialog,
524                WindowType::Tooltip,
525            ],
526            tabbed_windows: true,
527            tiled_windows: true,
528            floating_panels: true,
529            sheets: false,
530        };
531
532        #[cfg(target_os = "linux")]
533        return Self {
534            platform: "Linux",
535            window_types: vec![
536                WindowType::Document,
537                WindowType::Panel,
538                WindowType::Dialog,
539                WindowType::Tooltip,
540            ],
541            tabbed_windows: false,
542            tiled_windows: true,
543            floating_panels: true,
544            sheets: false,
545        };
546
547        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
548        return Self {
549            platform: "Unknown",
550            window_types: vec![WindowType::Document],
551            tabbed_windows: false,
552            tiled_windows: false,
553            floating_panels: false,
554            sheets: false,
555        };
556    }
557}
558
559/// Monitor configuration.
560#[derive(Debug, Clone)]
561pub struct MonitorConfig {
562    pub name: String,
563    pub position: (i32, i32),
564    pub size: (u32, u32),
565    pub scale_factor: f64,
566    pub refresh_rate: u32,
567}
568
569/// Manages multi-monitor layouts.
570#[derive(Debug, Clone)]
571pub struct MultiMonitorManager {
572    monitors: Vec<MonitorConfig>,
573    current_monitor_index: usize,
574}
575
576impl MultiMonitorManager {
577    pub fn new(mut monitors: Vec<MonitorConfig>) -> Self {
578        if monitors.is_empty() {
579            monitors.push(MonitorConfig {
580                name: "Default".to_string(),
581                position: (0, 0),
582                size: (1920, 1080),
583                scale_factor: 1.0,
584                refresh_rate: 60,
585            });
586        }
587        Self {
588            monitors,
589            current_monitor_index: 0,
590        }
591    }
592
593    pub fn current_monitor(&self) -> &MonitorConfig {
594        &self.monitors[self.current_monitor_index]
595    }
596
597    pub fn monitors(&self) -> &[MonitorConfig] {
598        &self.monitors
599    }
600
601    pub fn update_window_position(&mut self, window_rect: (i32, i32, u32, u32)) -> Option<usize> {
602        let center_x = window_rect.0 + (window_rect.2 as i32 / 2);
603        let center_y = window_rect.1 + (window_rect.3 as i32 / 2);
604
605        let mut best_index = None;
606        let mut min_distance = f64::MAX;
607
608        for (i, m) in self.monitors.iter().enumerate() {
609            let left = m.position.0;
610            let right = m.position.0 + m.size.0 as i32;
611            let top = m.position.1;
612            let bottom = m.position.1 + m.size.1 as i32;
613
614            if center_x >= left && center_x < right && center_y >= top && center_y < bottom {
615                self.current_monitor_index = i;
616                return Some(i);
617            }
618
619            let m_center_x = m.position.0 + (m.size.0 as i32 / 2);
620            let m_center_y = m.position.1 + (m.size.1 as i32 / 2);
621            let dx = (center_x - m_center_x) as f64;
622            let dy = (center_y - m_center_y) as f64;
623            let dist = (dx * dx + dy * dy).sqrt();
624            if dist < min_distance {
625                min_distance = dist;
626                best_index = Some(i);
627            }
628        }
629
630        if let Some(i) = best_index {
631            self.current_monitor_index = i;
632            Some(i)
633        } else {
634            None
635        }
636    }
637
638    pub fn scale_dimensions(&self, logical_width: f64, logical_height: f64) -> (u32, u32) {
639        let sf = self.current_monitor().scale_factor;
640        (
641            (logical_width * sf).round() as u32,
642            (logical_height * sf).round() as u32,
643        )
644    }
645
646    pub fn requires_dpi_adaptation(&self, from_index: usize, to_index: usize) -> bool {
647        if from_index < self.monitors.len() && to_index < self.monitors.len() {
648            (self.monitors[from_index].scale_factor - self.monitors[to_index].scale_factor).abs() > f64::EPSILON
649        } else {
650            false
651        }
652    }
653}