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 crate::main_loop::AppEvent;
7use cvkg_core::{FocusManager, FocusableId, WindowConfig, WindowHandle, WindowId as CoreWindowId};
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;
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/// 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            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// =============================================================================
480// Window Capability Matrix and Multi-Monitor configurations
481// =============================================================================
482
483/// Window type.
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum WindowType {
486    Document,
487    Panel,
488    Popover,
489    Dialog,
490    Tooltip,
491}
492
493/// Window capability matrix per platform.
494#[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/// Monitor configuration.
565#[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/// Manages multi-monitor layouts.
575#[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}