Skip to main content

lingxia_windows_contract/
lib.rs

1//! Windows host UI contract shared by the Rust Windows SDK pieces.
2//!
3//! This crate intentionally contains no Win32 window implementation. The
4//! implementation belongs to `lingxia-windows-sdk`.
5//!
6//! The crate is Windows-only; off-Windows it compiles to nothing so a
7//! `cargo *(--workspace)` on other hosts neither pulls the `windows` crate
8//! nor lints Win32 contracts that can't exist there.
9#![cfg(windows)]
10
11use std::any::Any;
12use std::collections::HashMap;
13use std::sync::{Arc, Mutex, OnceLock};
14
15use lingxia_webview::{WebTag, WebViewError};
16use windows::Win32::Foundation::{HWND, RECT};
17use windows::Win32::Graphics::Gdi::HDC;
18
19type StdResult<T, E = WebViewError> = std::result::Result<T, E>;
20
21pub type HostWindowCreatedHandler = Arc<dyn Fn(isize) + Send + Sync>;
22pub type CloseHandler = Arc<dyn Fn() + Send + Sync>;
23pub type ChromeEventHandler = Arc<dyn Fn(WindowsChromeCommand) + Send + Sync>;
24pub type WebViewVisibilityHandler = Arc<dyn Fn(&WebTag, bool) + Send + Sync>;
25pub type WindowsHostPanelInputHandler = Arc<dyn Fn(WindowsHostPanelKeyEvent) -> bool + Send + Sync>;
26
27static DEFAULT_WINDOW_SIZE: OnceLock<(i32, i32)> = OnceLock::new();
28static BACKEND: OnceLock<Arc<dyn WindowsHostBackend>> = OnceLock::new();
29static CLOSE_HANDLERS: OnceLock<Mutex<HashMap<String, CloseHandler>>> = OnceLock::new();
30static CHROME_HANDLERS: OnceLock<Mutex<HashMap<String, ChromeEventHandler>>> = OnceLock::new();
31static VISIBILITY_HANDLER: OnceLock<Mutex<Option<WebViewVisibilityHandler>>> = OnceLock::new();
32static HOST_WINDOW_CREATED_HANDLERS: OnceLock<Mutex<Vec<HostWindowCreatedHandler>>> =
33    OnceLock::new();
34static HOST_PANEL_INPUT_HANDLERS: OnceLock<Mutex<HashMap<String, WindowsHostPanelInputHandler>>> =
35    OnceLock::new();
36static WINDOW_LAYOUTS: OnceLock<Mutex<HashMap<String, WindowsWindowLayout>>> = OnceLock::new();
37static WINDOWS_CHROME_RENDERER: OnceLock<Mutex<Option<Arc<dyn WindowsChromeRenderer>>>> =
38    OnceLock::new();
39static ASIDE_PANEL_TABS: OnceLock<Mutex<HashMap<String, Vec<WindowsAsidePanelTab>>>> =
40    OnceLock::new();
41static ASIDE_PANEL_EVENT_HANDLER: OnceLock<Mutex<Option<WindowsAsidePanelEventHandler>>> =
42    OnceLock::new();
43
44/// One tab in a docked aside slot.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct WindowsAsidePanelTab {
47    pub surface_id: String,
48    pub title: String,
49    pub active: bool,
50}
51
52/// Chrome events from a docked aside slot, routed back to the owner of the
53/// addressed browser, lxapp, or native slot.
54#[derive(Debug, Clone)]
55pub enum WindowsAsidePanelEvent {
56    TabClick {
57        panel_id: String,
58        surface_id: String,
59    },
60    TabClose {
61        panel_id: String,
62        surface_id: String,
63    },
64    /// Put the whole slot away without closing anything in it.
65    Collapse {
66        panel_id: String,
67    },
68    NavBack {
69        panel_id: String,
70    },
71    NavForward {
72        panel_id: String,
73    },
74    NavReload {
75        panel_id: String,
76    },
77}
78
79pub type WindowsAsidePanelEventHandler = Arc<dyn Fn(WindowsAsidePanelEvent) + Send + Sync>;
80
81/// Stable panel id of the shared aside browser panel (one per window).
82pub const ASIDE_BROWSER_PANEL_ID: &str = "lx.aside-browser";
83/// Stable panel id of the shared lxapp aside slot (one per window).
84pub const ASIDE_LXAPP_PANEL_ID: &str = "lx.aside-lxapp";
85
86/// Publishes the tab strip of an aside browser panel; an empty list removes
87/// it (the panel then falls back to non-tabbed chrome).
88pub fn set_aside_panel_tabs(panel_id: &str, tabs: Vec<WindowsAsidePanelTab>) {
89    let registry = ASIDE_PANEL_TABS.get_or_init(|| Mutex::new(HashMap::new()));
90    if let Ok(mut registry) = registry.lock() {
91        if tabs.is_empty() {
92            registry.remove(panel_id);
93        } else {
94            registry.insert(panel_id.to_string(), tabs);
95        }
96    }
97}
98
99pub fn aside_panel_tabs(panel_id: &str) -> Vec<WindowsAsidePanelTab> {
100    ASIDE_PANEL_TABS
101        .get()
102        .and_then(|registry| registry.lock().ok())
103        .and_then(|registry| registry.get(panel_id).cloned())
104        .unwrap_or_default()
105}
106
107pub fn set_windows_aside_panel_event_handler(handler: WindowsAsidePanelEventHandler) {
108    let slot = ASIDE_PANEL_EVENT_HANDLER.get_or_init(|| Mutex::new(None));
109    if let Ok(mut slot) = slot.lock() {
110        *slot = Some(handler);
111    }
112}
113
114/// Routes a chrome event to the aside-panel handler; `false` when none is
115/// installed.
116pub fn dispatch_windows_aside_panel_event(event: WindowsAsidePanelEvent) -> bool {
117    let handler = ASIDE_PANEL_EVENT_HANDLER
118        .get()
119        .and_then(|slot| slot.lock().ok())
120        .and_then(|slot| slot.clone());
121    let Some(handler) = handler else {
122        return false;
123    };
124    handler(event);
125    true
126}
127
128fn unsupported_operation<T>(operation: &str) -> StdResult<T> {
129    Err(WebViewError::WebView(format!(
130        "Windows host backend does not support {operation}"
131    )))
132}
133
134/// Host callbacks implemented by the window owner.
135///
136/// Every hook has a conservative default so a custom host can opt into only the
137/// capabilities it actually orchestrates. For example, a host that wants the
138/// SDK-managed native view components usually starts with
139/// `find_webview_content_window` and `post_to_window_thread`, then adds panel or
140/// shell integration as needed.
141pub trait WindowsHostBackend: Send + Sync {
142    fn show_webview_as_panel(
143        &self,
144        _webtag: &WebTag,
145        _title: &str,
146        _panel_id: &str,
147    ) -> StdResult<()> {
148        unsupported_operation("show_webview_as_panel")
149    }
150
151    fn show_webview_as_adaptive_panel(
152        &self,
153        _webtag: &WebTag,
154        _title: &str,
155        _panel_id: &str,
156        _position: WindowsPanelPosition,
157        _preferred_size: Option<i32>,
158    ) -> StdResult<()> {
159        unsupported_operation("show_webview_as_adaptive_panel")
160    }
161
162    fn show_webview_as_overlay_panel(
163        &self,
164        _webtag: &WebTag,
165        _title: &str,
166        _panel_id: &str,
167        _position: WindowsPanelPosition,
168    ) -> StdResult<()> {
169        unsupported_operation("show_webview_as_overlay_panel")
170    }
171
172    fn present_webview_in_active_group(&self, _webtag: &WebTag) -> StdResult<()> {
173        unsupported_operation("present_webview_in_active_group")
174    }
175
176    fn active_host_window_is_device_framed(&self) -> bool {
177        false
178    }
179
180    fn active_host_window_webtag_key(&self) -> Option<String> {
181        None
182    }
183
184    fn present_webview_as_group_main(&self, _webtag: &WebTag, _group_key: String) -> StdResult<()> {
185        unsupported_operation("present_webview_as_group_main")
186    }
187
188    fn present_webview_as_overlay(
189        &self,
190        _webtag: &WebTag,
191        _width: f64,
192        _height: f64,
193        _width_ratio: f64,
194        _height_ratio: f64,
195        _position: u8,
196    ) -> StdResult<()> {
197        unsupported_operation("present_webview_as_overlay")
198    }
199
200    fn configure_webview_surface_interaction(
201        &self,
202        _webtag: &WebTag,
203        _close_button: bool,
204        _dismiss_on_outside: bool,
205        _modal: bool,
206    ) -> StdResult<()> {
207        unsupported_operation("configure_webview_surface_interaction")
208    }
209
210    fn resize_host_window_content(
211        &self,
212        _webtag: &WebTag,
213        _width: i32,
214        _height: i32,
215    ) -> StdResult<()> {
216        unsupported_operation("resize_host_window_content")
217    }
218
219    fn restore_presented_group_main(&self) -> StdResult<()> {
220        unsupported_operation("restore_presented_group_main")
221    }
222
223    fn show_interactive_host_panel(
224        &self,
225        _panel_id: &str,
226        _title: &str,
227        _body: &str,
228        _position: WindowsPanelPosition,
229    ) -> StdResult<()> {
230        unsupported_operation("show_interactive_host_panel")
231    }
232
233    fn hide_host_panel(&self, _panel_id: &str) -> StdResult<()> {
234        unsupported_operation("hide_host_panel")
235    }
236
237    fn update_host_panel_body(&self, _panel_id: &str, _body: &str) -> StdResult<()> {
238        unsupported_operation("update_host_panel_body")
239    }
240
241    fn set_host_panel_tabs(&self, _panel_id: &str, _tabs: Vec<WindowsHostPanelTab>) -> bool {
242        false
243    }
244
245    fn set_host_panel_maximized(&self, _panel_id: &str, _maximized: bool) -> bool {
246        false
247    }
248
249    fn invalidate_host_panel(&self, _panel_id: &str) -> bool {
250        false
251    }
252
253    fn is_panel_visible(&self, _panel_id: &str) -> bool {
254        false
255    }
256
257    fn find_webview_content_window(&self, _webtag: &WebTag) -> Option<WindowsWebViewContentWindow> {
258        None
259    }
260
261    fn webview_window_snapshot(&self, _webtag: &WebTag) -> StdResult<WindowsWebViewWindowSnapshot> {
262        unsupported_operation("webview_window_snapshot")
263    }
264
265    fn show_webview_window(
266        &self,
267        _webtag: &WebTag,
268        _title: &str,
269        _activate: bool,
270    ) -> StdResult<()> {
271        unsupported_operation("show_webview_window")
272    }
273
274    fn show_webview_window_with_content_size(
275        &self,
276        _webtag: &WebTag,
277        _title: &str,
278        _activate: bool,
279        _width: Option<i32>,
280        _height: Option<i32>,
281    ) -> StdResult<()> {
282        unsupported_operation("show_webview_window_with_content_size")
283    }
284
285    /// `full_chrome` runs the page to the window edge while the system keeps
286    /// minimize, maximize, resize, and drag.
287    fn show_webview_window_with_chrome(
288        &self,
289        _webtag: &WebTag,
290        _title: &str,
291        _activate: bool,
292        _width: Option<i32>,
293        _height: Option<i32>,
294        _full_chrome: bool,
295    ) -> StdResult<()> {
296        unsupported_operation("show_webview_window_with_chrome")
297    }
298
299    fn navigate_webview_window(
300        &self,
301        _webtag: &WebTag,
302        _title: &str,
303        _activate: bool,
304        _animation: WindowsNavAnimation,
305    ) -> StdResult<()> {
306        unsupported_operation("navigate_webview_window")
307    }
308
309    fn hide_webview_window(&self, _webtag: &WebTag) -> StdResult<()> {
310        unsupported_operation("hide_webview_window")
311    }
312
313    fn request_host_window_layout(&self, _window: WindowsHostWindow) -> bool {
314        false
315    }
316
317    fn active_content_screen_rect(&self) -> Option<WindowsContentRect> {
318        None
319    }
320
321    fn post_to_window_thread(&self, _window: isize, _callback: Box<dyn FnOnce() + Send>) -> bool {
322        false
323    }
324
325    fn sync_webview_window_layout(&self, _webtag: &WebTag) {}
326
327    /// Repaints an aside panel's chrome after a tab-strip change that leaves
328    /// the attached layout untouched (e.g. an inactive tab closed).
329    fn refresh_aside_panel(&self, _panel_id: &str) {}
330}
331
332pub fn refresh_aside_panel(panel_id: &str) {
333    if let Ok(backend) = backend() {
334        backend.refresh_aside_panel(panel_id);
335    }
336}
337
338pub fn set_windows_host_backend(backend: Arc<dyn WindowsHostBackend>) {
339    if BACKEND.set(backend).is_err() {
340        log::warn!("Windows host backend is already installed; ignoring replacement");
341    }
342}
343
344fn backend() -> StdResult<&'static Arc<dyn WindowsHostBackend>> {
345    BACKEND
346        .get()
347        .ok_or_else(|| WebViewError::WebView("Windows host backend is not installed".to_string()))
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
351/// A rectangle of a host window whose pixels the compositor owns, not GDI.
352///
353/// `BitBlt` cannot see DirectComposition content, so anything drawn that way —
354/// WebView2 surfaces, the terminal grid — has to hand its pixels to the
355/// screenshot path or it is simply missing from every capture.
356pub struct WindowsSurfaceCapture {
357    /// Top-left in the host window's client coordinates.
358    pub x: i32,
359    pub y: i32,
360    pub width: u32,
361    pub height: u32,
362    /// Row-major BGRA, `width * height * 4` bytes.
363    pub pixels: Vec<u8>,
364}
365
366type SurfaceCaptureProvider = fn(usize) -> Vec<WindowsSurfaceCapture>;
367
368static SURFACE_CAPTURE_PROVIDERS: OnceLock<Mutex<Vec<SurfaceCaptureProvider>>> = OnceLock::new();
369
370fn surface_capture_providers() -> &'static Mutex<Vec<SurfaceCaptureProvider>> {
371    SURFACE_CAPTURE_PROVIDERS.get_or_init(|| Mutex::new(Vec::new()))
372}
373
374/// Offer composited pixels to screenshots. Called once per renderer.
375pub fn register_surface_capture_provider(provider: SurfaceCaptureProvider) {
376    if let Ok(mut providers) = surface_capture_providers().lock() {
377        providers.push(provider);
378    }
379}
380
381/// Every composited rectangle in `window_id`, for the screenshot path.
382pub fn surface_captures(window_id: usize) -> Vec<WindowsSurfaceCapture> {
383    let providers = match surface_capture_providers().lock() {
384        Ok(providers) => providers.clone(),
385        Err(_) => return Vec::new(),
386    };
387    providers
388        .into_iter()
389        .flat_map(|provider| provider(window_id))
390        .collect()
391}
392
393pub struct WindowsWebViewWindowSnapshot {
394    pub window_id: usize,
395    pub webtag_key: String,
396    pub visible: bool,
397    pub window_left: i32,
398    pub window_top: i32,
399    pub window_width: i32,
400    pub window_height: i32,
401    pub content_left: i32,
402    pub content_top: i32,
403    pub content_width: u32,
404    pub content_height: u32,
405    /// Composition-clip corner radii `[tl, tr, br, bl]` of the live surface
406    /// (zeros for windowed hosting), so screenshot compositing can reproduce
407    /// the on-screen rounding.
408    pub content_corner_radii: [i32; 4],
409}
410
411#[derive(Debug, Clone, Copy, PartialEq)]
412pub struct WindowsWebViewContentWindow {
413    pub window: isize,
414    pub content_left: i32,
415    pub content_top: i32,
416    pub content_width: i32,
417    pub content_height: i32,
418    pub scale: f64,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
422pub enum WindowsPanelPosition {
423    Left,
424    #[default]
425    Right,
426    Top,
427    Bottom,
428}
429
430/// The page-transition animation a `navigate` should play, mirroring the
431/// `AnimationType` the core computes from the JS navigation verb (forward slide
432/// for `navigateTo`, backward slide for `navigateBack`, none for
433/// `redirectTo`/`switchTab`/`reLaunch`). Kept as a contract-local enum so this
434/// crate needs no dependency on `lingxia-platform`.
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
436pub enum WindowsNavAnimation {
437    #[default]
438    None,
439    Forward,
440    Backward,
441}
442
443#[derive(Clone, Default)]
444pub struct WindowsWindowLayout {
445    payload: Option<Arc<dyn Any + Send + Sync>>,
446}
447
448impl std::fmt::Debug for WindowsWindowLayout {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        f.debug_struct("WindowsWindowLayout")
451            .field("has_payload", &self.payload.is_some())
452            .finish()
453    }
454}
455
456impl WindowsWindowLayout {
457    pub fn new<T>(payload: T) -> Self
458    where
459        T: Any + Send + Sync + 'static,
460    {
461        Self {
462            payload: Some(Arc::new(payload)),
463        }
464    }
465
466    pub fn empty() -> Self {
467        Self::default()
468    }
469
470    pub fn is_empty(&self) -> bool {
471        self.payload.is_none()
472    }
473
474    pub fn downcast_ref<T>(&self) -> Option<&T>
475    where
476        T: Any + 'static,
477    {
478        self.payload.as_deref()?.downcast_ref::<T>()
479    }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq)]
483pub struct WindowsHostPanelTab {
484    pub id: u64,
485    pub title: String,
486    pub active: bool,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct WindowsHostPanelContent {
491    pub title: Option<String>,
492    pub body: Option<String>,
493    pub tabs: Vec<WindowsHostPanelTab>,
494    pub maximized: bool,
495    /// Whether the panel header exposes its expand/restore control. Native
496    /// main workspaces fill the workspace by definition, so only asides show
497    /// this affordance.
498    pub show_maximize: bool,
499}
500
501#[derive(Debug, Clone, PartialEq)]
502pub struct WindowsChromePanel {
503    pub panel_id: String,
504    pub webtag_key: String,
505    pub title: String,
506    pub rect: RECT,
507    /// Top-band slice (aligned with the main navbar baseline) where a browser
508    /// aside paints its address bar; `None` for panels with no band header.
509    pub header_rect: Option<RECT>,
510    /// Gutter between this panel and the neighboring workspace region. It is
511    /// both the resize hit target and the exposed first-layer shell surface.
512    pub resize_handle: Option<RECT>,
513    pub host_content: Option<WindowsHostPanelContent>,
514    pub docked: bool,
515    /// Covers the main workspace without reserving split space.
516    pub overlay: bool,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
520pub struct WindowsChromePanelLayoutInput {
521    pub panel_id: String,
522    pub webtag_key: String,
523    pub position: WindowsPanelPosition,
524    pub requested_size: Option<i32>,
525    pub docked: bool,
526    /// Cover the host workspace without consuming main layout space.
527    pub overlay: bool,
528    pub maximized: bool,
529}
530
531#[derive(Debug, Clone, PartialEq)]
532pub struct WindowsChromePanelLayout {
533    pub panel_id: String,
534    pub webtag_key: String,
535    pub rect: RECT,
536    /// Top-band slice for a browser aside's address bar (see
537    /// [`WindowsChromePanel::header_rect`]); `None` when the panel has none.
538    pub header_rect: Option<RECT>,
539    pub resize_handle: Option<RECT>,
540    /// Covers the main workspace without reserving split space.
541    pub overlay: bool,
542}
543
544#[derive(Debug, Clone, PartialEq)]
545pub struct WindowsChromeAttachedLayout {
546    /// Full main region after aside arbitration, including main-owned chrome.
547    pub main_region: RECT,
548    /// Main WebView viewport after reserving its navigation bar.
549    pub main: RECT,
550    pub panels: Vec<WindowsChromePanelLayout>,
551}
552
553#[derive(Debug, Clone, PartialEq)]
554pub struct WindowsChromeAttachedState {
555    pub main_region: RECT,
556    pub main: RECT,
557    pub panels: Vec<WindowsChromePanel>,
558}
559
560#[derive(Debug, Clone)]
561pub struct WindowsChromeState {
562    pub hwnd: HWND,
563    pub client: RECT,
564    pub layout: WindowsWindowLayout,
565    pub attached: Option<WindowsChromeAttachedState>,
566    pub frame_button_hover: Option<WindowsFrameButton>,
567    pub frame_button_pressed: Option<WindowsFrameButton>,
568    /// Client-space cursor position while over this window's chrome; drives
569    /// hover feedback (frame buttons keep their dedicated state above).
570    pub cursor: Option<(i32, i32)>,
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub enum WindowsFrameButton {
575    Minimize,
576    Maximize,
577    Close,
578}
579
580#[derive(Debug, Clone, PartialEq)]
581pub struct WindowsChromeCommand {
582    pub id: String,
583    pub payload: serde_json::Value,
584    pub focus: Option<String>,
585    pub double_click: Option<Box<WindowsChromeCommand>>,
586    pub include_screen_position: bool,
587}
588
589impl WindowsChromeCommand {
590    pub fn new(id: impl Into<String>) -> Self {
591        Self {
592            id: id.into(),
593            payload: serde_json::Value::Null,
594            focus: None,
595            double_click: None,
596            include_screen_position: false,
597        }
598    }
599
600    pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
601        self.payload = payload;
602        self
603    }
604
605    pub fn with_focus(mut self, surface_id: impl Into<String>) -> Self {
606        self.focus = Some(surface_id.into());
607        self
608    }
609
610    pub fn with_double_click(mut self, command: WindowsChromeCommand) -> Self {
611        self.double_click = Some(Box::new(command));
612        self
613    }
614
615    pub fn with_screen_position(mut self) -> Self {
616        self.include_screen_position = true;
617        self
618    }
619}
620
621#[derive(Debug, Clone, PartialEq)]
622pub enum WindowsChromeHit {
623    Caption,
624    FrameButton(WindowsFrameButton),
625    Focusable {
626        id: String,
627        context_menu: Option<WindowsChromeCommand>,
628        /// Optional command invoked on left-button-down in addition to
629        /// focusing the surface (e.g. focusing the terminal pane under the
630        /// cursor). Carries the click's screen position when requested.
631        click_command: Option<WindowsChromeCommand>,
632    },
633    Command(WindowsChromeCommand),
634    CommandWithContext {
635        command: WindowsChromeCommand,
636        context_menu: WindowsChromeCommand,
637    },
638    Chrome,
639}
640
641pub trait WindowsChromeRenderer: Send + Sync {
642    fn content_rect(&self, client: RECT, layout: &WindowsWindowLayout) -> RECT;
643
644    fn attached_layout(
645        &self,
646        client: RECT,
647        layout: &WindowsWindowLayout,
648        panels: &[WindowsChromePanelLayoutInput],
649    ) -> Option<WindowsChromeAttachedLayout> {
650        let _ = (client, layout, panels);
651        None
652    }
653
654    fn paint(&self, hdc: HDC, state: &WindowsChromeState);
655
656    fn paint_region(&self, hdc: HDC, state: &WindowsChromeState, invalid: RECT) {
657        let _ = invalid;
658        self.paint(hdc, state);
659    }
660
661    fn hit_test(&self, state: &WindowsChromeState, point: (i32, i32)) -> Option<WindowsChromeHit>;
662
663    fn frame_button_rect(
664        &self,
665        state: &WindowsChromeState,
666        button: WindowsFrameButton,
667    ) -> Option<RECT> {
668        let _ = (state, button);
669        None
670    }
671
672    /// Bounding rect of the hover-highlightable element under `point`; the
673    /// host invalidates the rects the cursor enters/leaves so hover feedback
674    /// repaints exactly the affected element.
675    fn hover_rect(&self, state: &WindowsChromeState, point: (i32, i32)) -> Option<RECT> {
676        let _ = (state, point);
677        None
678    }
679
680    /// Translate a wheel gesture over owner-drawn chrome into a runtime
681    /// command. Returning `None` lets the host forward the gesture normally.
682    fn mouse_wheel(
683        &self,
684        state: &WindowsChromeState,
685        point: (i32, i32),
686        delta: i16,
687    ) -> Option<WindowsChromeCommand> {
688        let _ = (state, point, delta);
689        None
690    }
691}
692
693#[derive(Debug, Clone, Copy, PartialEq, Eq)]
694pub struct WindowsHostPanelKeyEvent {
695    pub vk: u32,
696    pub ctrl: bool,
697    pub shift: bool,
698    pub alt: bool,
699    pub character: Option<char>,
700}
701
702#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub struct WindowsHostWindow {
704    pub window: isize,
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
708pub struct WindowsContentRect {
709    pub host_window: isize,
710    pub left: i32,
711    pub top: i32,
712    pub width: i32,
713    pub height: i32,
714    pub dpi: u32,
715}
716
717pub fn set_default_window_size(width: i32, height: i32) {
718    if width > 0 && height > 0 {
719        let _ = DEFAULT_WINDOW_SIZE.set((width, height));
720    }
721}
722
723pub fn default_window_size() -> (i32, i32) {
724    DEFAULT_WINDOW_SIZE.get().copied().unwrap_or((1024, 768))
725}
726
727pub fn set_windows_chrome_renderer(renderer: Arc<dyn WindowsChromeRenderer>) {
728    let slot = WINDOWS_CHROME_RENDERER.get_or_init(|| Mutex::new(None));
729    if let Ok(mut slot) = slot.lock() {
730        *slot = Some(renderer);
731    }
732}
733
734pub fn windows_chrome_renderer() -> Option<Arc<dyn WindowsChromeRenderer>> {
735    WINDOWS_CHROME_RENDERER
736        .get()
737        .and_then(|renderer| renderer.lock().ok())
738        .and_then(|renderer| renderer.clone())
739}
740
741pub fn set_webview_close_handler(webtag: &WebTag, handler: CloseHandler) {
742    let handlers = CLOSE_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
743    if let Ok(mut handlers) = handlers.lock() {
744        handlers.insert(webtag.key().to_string(), handler);
745    }
746}
747
748pub fn webview_close_handler(webtag_key: &str) -> Option<CloseHandler> {
749    CLOSE_HANDLERS
750        .get()
751        .and_then(|handlers| handlers.lock().ok())
752        .and_then(|handlers| handlers.get(webtag_key).cloned())
753}
754
755pub fn set_webview_visibility_handler(handler: WebViewVisibilityHandler) {
756    let slot = VISIBILITY_HANDLER.get_or_init(|| Mutex::new(None));
757    if let Ok(mut slot) = slot.lock() {
758        *slot = Some(handler);
759    }
760}
761
762pub fn webview_visibility_handler() -> Option<WebViewVisibilityHandler> {
763    VISIBILITY_HANDLER
764        .get()
765        .and_then(|slot| slot.lock().ok())
766        .and_then(|slot| slot.clone())
767}
768
769pub fn set_webview_chrome_event_handler(webtag: &WebTag, handler: ChromeEventHandler) {
770    let handlers = CHROME_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
771    if let Ok(mut handlers) = handlers.lock() {
772        handlers.insert(webtag.key().to_string(), handler);
773    }
774}
775
776pub fn webview_chrome_event_handler(webtag_key: &str) -> Option<ChromeEventHandler> {
777    CHROME_HANDLERS
778        .get()
779        .and_then(|handlers| handlers.lock().ok())
780        .and_then(|handlers| handlers.get(webtag_key).cloned())
781}
782
783pub fn add_host_window_created_handler(handler: HostWindowCreatedHandler) {
784    let handlers = HOST_WINDOW_CREATED_HANDLERS.get_or_init(|| Mutex::new(Vec::new()));
785    if let Ok(mut handlers) = handlers.lock() {
786        handlers.push(handler);
787    }
788}
789
790pub fn host_window_created_handlers() -> Vec<HostWindowCreatedHandler> {
791    HOST_WINDOW_CREATED_HANDLERS
792        .get()
793        .and_then(|state| state.lock().ok())
794        .map(|state| state.clone())
795        .unwrap_or_default()
796}
797
798pub fn set_host_panel_input_handler(panel_id: &str, handler: WindowsHostPanelInputHandler) {
799    let handlers = HOST_PANEL_INPUT_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
800    if let Ok(mut handlers) = handlers.lock() {
801        handlers.insert(panel_id.to_string(), handler);
802    }
803}
804
805pub fn clear_host_panel_input_handler(panel_id: &str) {
806    if let Some(handlers) = HOST_PANEL_INPUT_HANDLERS.get()
807        && let Ok(mut handlers) = handlers.lock()
808    {
809        handlers.remove(panel_id);
810    }
811}
812
813pub fn host_panel_input_handler(panel_id: &str) -> Option<WindowsHostPanelInputHandler> {
814    HOST_PANEL_INPUT_HANDLERS
815        .get()
816        .and_then(|handlers| handlers.lock().ok())
817        .and_then(|handlers| handlers.get(panel_id).cloned())
818}
819
820pub fn set_webview_window_layout(webtag: &WebTag, layout: WindowsWindowLayout) -> StdResult<()> {
821    let layouts = WINDOW_LAYOUTS.get_or_init(|| Mutex::new(HashMap::new()));
822    if let Ok(mut layouts) = layouts.lock() {
823        layouts.insert(webtag.key().to_string(), layout);
824    }
825    if let Ok(backend) = backend() {
826        backend.sync_webview_window_layout(webtag);
827    }
828    Ok(())
829}
830
831pub fn current_window_layout(webtag_key: &str) -> WindowsWindowLayout {
832    WINDOW_LAYOUTS
833        .get()
834        .and_then(|layouts| layouts.lock().ok())
835        .and_then(|layouts| layouts.get(webtag_key).cloned())
836        .unwrap_or_default()
837}
838
839pub fn cleanup_webview_state(webtag_key: &str) {
840    if let Some(handlers) = CLOSE_HANDLERS.get()
841        && let Ok(mut handlers) = handlers.lock()
842    {
843        handlers.remove(webtag_key);
844    }
845    if let Some(handlers) = CHROME_HANDLERS.get()
846        && let Ok(mut handlers) = handlers.lock()
847    {
848        handlers.remove(webtag_key);
849    }
850    if let Some(layouts) = WINDOW_LAYOUTS.get()
851        && let Ok(mut layouts) = layouts.lock()
852    {
853        layouts.remove(webtag_key);
854    }
855}
856
857pub fn show_webview_as_panel(webtag: &WebTag, title: &str, panel_id: &str) -> StdResult<()> {
858    backend()?.show_webview_as_panel(webtag, title, panel_id)
859}
860
861pub fn show_webview_as_adaptive_panel(
862    webtag: &WebTag,
863    title: &str,
864    panel_id: &str,
865    position: WindowsPanelPosition,
866    preferred_size: Option<i32>,
867) -> StdResult<()> {
868    backend()?.show_webview_as_adaptive_panel(webtag, title, panel_id, position, preferred_size)
869}
870
871pub fn show_webview_as_overlay_panel(
872    webtag: &WebTag,
873    title: &str,
874    panel_id: &str,
875    position: WindowsPanelPosition,
876) -> StdResult<()> {
877    backend()?.show_webview_as_overlay_panel(webtag, title, panel_id, position)
878}
879
880pub fn present_webview_in_active_group(webtag: &WebTag) -> StdResult<()> {
881    backend()?.present_webview_in_active_group(webtag)
882}
883
884pub fn active_host_window_is_device_framed() -> bool {
885    backend()
886        .map(|backend| backend.active_host_window_is_device_framed())
887        .unwrap_or(false)
888}
889
890pub fn active_host_window_webtag_key() -> Option<String> {
891    backend()
892        .ok()
893        .and_then(|backend| backend.active_host_window_webtag_key())
894}
895
896pub fn present_webview_as_group_main(webtag: &WebTag, group_key: String) -> StdResult<()> {
897    backend()?.present_webview_as_group_main(webtag, group_key)
898}
899
900pub fn present_webview_as_overlay(
901    webtag: &WebTag,
902    width: f64,
903    height: f64,
904    width_ratio: f64,
905    height_ratio: f64,
906    position: u8,
907) -> StdResult<()> {
908    backend()?.present_webview_as_overlay(
909        webtag,
910        width,
911        height,
912        width_ratio,
913        height_ratio,
914        position,
915    )
916}
917
918pub fn configure_webview_surface_interaction(
919    webtag: &WebTag,
920    close_button: bool,
921    dismiss_on_outside: bool,
922    modal: bool,
923) -> StdResult<()> {
924    backend()?.configure_webview_surface_interaction(
925        webtag,
926        close_button,
927        dismiss_on_outside,
928        modal,
929    )
930}
931
932pub fn resize_host_window_content(webtag: &WebTag, width: i32, height: i32) -> StdResult<()> {
933    backend()?.resize_host_window_content(webtag, width, height)
934}
935
936pub fn restore_presented_group_main() -> StdResult<()> {
937    backend()?.restore_presented_group_main()
938}
939
940pub fn show_interactive_host_panel(
941    panel_id: &str,
942    title: &str,
943    body: &str,
944    position: WindowsPanelPosition,
945) -> StdResult<()> {
946    backend()?.show_interactive_host_panel(panel_id, title, body, position)
947}
948
949pub fn hide_host_panel(panel_id: &str) -> StdResult<()> {
950    backend()?.hide_host_panel(panel_id)
951}
952
953pub fn update_host_panel_body(panel_id: &str, body: &str) -> StdResult<()> {
954    backend()?.update_host_panel_body(panel_id, body)
955}
956
957pub fn set_host_panel_tabs(panel_id: &str, tabs: Vec<WindowsHostPanelTab>) -> bool {
958    backend()
959        .map(|backend| backend.set_host_panel_tabs(panel_id, tabs))
960        .unwrap_or(false)
961}
962
963pub fn set_host_panel_maximized(panel_id: &str, maximized: bool) -> bool {
964    backend()
965        .map(|backend| backend.set_host_panel_maximized(panel_id, maximized))
966        .unwrap_or(false)
967}
968
969pub fn invalidate_host_panel(panel_id: &str) -> bool {
970    backend()
971        .map(|backend| backend.invalidate_host_panel(panel_id))
972        .unwrap_or(false)
973}
974
975pub fn is_panel_visible(panel_id: &str) -> bool {
976    backend()
977        .map(|backend| backend.is_panel_visible(panel_id))
978        .unwrap_or(false)
979}
980
981pub fn find_host_window_for_webview(webtag: &WebTag) -> StdResult<WindowsHostWindow> {
982    let content = find_webview_content_window(webtag).ok_or_else(|| {
983        WebViewError::WebView(format!("no window registered for {}", webtag.key()))
984    })?;
985    Ok(WindowsHostWindow {
986        window: content.window,
987    })
988}
989
990pub fn request_host_window_layout(window: WindowsHostWindow) -> bool {
991    backend()
992        .map(|backend| backend.request_host_window_layout(window))
993        .unwrap_or(false)
994}
995
996pub fn active_content_screen_rect() -> Option<WindowsContentRect> {
997    backend()
998        .ok()
999        .and_then(|backend| backend.active_content_screen_rect())
1000}
1001
1002pub fn find_webview_content_window(webtag: &WebTag) -> Option<WindowsWebViewContentWindow> {
1003    backend()
1004        .ok()
1005        .and_then(|backend| backend.find_webview_content_window(webtag))
1006}
1007
1008pub fn webview_window_snapshot(webtag: &WebTag) -> StdResult<WindowsWebViewWindowSnapshot> {
1009    backend()?.webview_window_snapshot(webtag)
1010}
1011
1012pub fn show_webview_window(webtag: &WebTag, title: &str, activate: bool) -> StdResult<()> {
1013    backend()?.show_webview_window(webtag, title, activate)
1014}
1015
1016pub fn show_webview_window_with_content_size(
1017    webtag: &WebTag,
1018    title: &str,
1019    activate: bool,
1020    width: Option<i32>,
1021    height: Option<i32>,
1022) -> StdResult<()> {
1023    backend()?.show_webview_window_with_content_size(webtag, title, activate, width, height)
1024}
1025
1026pub fn show_webview_window_with_chrome(
1027    webtag: &WebTag,
1028    title: &str,
1029    activate: bool,
1030    width: Option<i32>,
1031    height: Option<i32>,
1032    full_chrome: bool,
1033) -> StdResult<()> {
1034    backend()?.show_webview_window_with_chrome(webtag, title, activate, width, height, full_chrome)
1035}
1036
1037pub fn navigate_webview_window(
1038    webtag: &WebTag,
1039    title: &str,
1040    activate: bool,
1041    animation: WindowsNavAnimation,
1042) -> StdResult<()> {
1043    backend()?.navigate_webview_window(webtag, title, activate, animation)
1044}
1045
1046pub fn hide_webview_window(webtag: &WebTag) -> StdResult<()> {
1047    backend()?.hide_webview_window(webtag)
1048}
1049
1050pub fn post_to_window_thread(window: isize, callback: Box<dyn FnOnce() + Send>) -> bool {
1051    backend()
1052        .map(|backend| backend.post_to_window_thread(window, callback))
1053        .unwrap_or(false)
1054}