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    /// Hide the exclusive-tray flyout if it is showing.
238    fn hide_exclusive_tray_popover(&self) -> bool {
239        false
240    }
241
242    /// Show the exclusive-tray flyout next to the notify icon.
243    fn show_exclusive_tray_popover(&self) -> bool {
244        false
245    }
246
247    fn update_host_panel_body(&self, _panel_id: &str, _body: &str) -> StdResult<()> {
248        unsupported_operation("update_host_panel_body")
249    }
250
251    fn set_host_panel_tabs(&self, _panel_id: &str, _tabs: Vec<WindowsHostPanelTab>) -> bool {
252        false
253    }
254
255    fn set_host_panel_maximized(&self, _panel_id: &str, _maximized: bool) -> bool {
256        false
257    }
258
259    fn invalidate_host_panel(&self, _panel_id: &str) -> bool {
260        false
261    }
262
263    fn is_panel_visible(&self, _panel_id: &str) -> bool {
264        false
265    }
266
267    fn find_webview_content_window(&self, _webtag: &WebTag) -> Option<WindowsWebViewContentWindow> {
268        None
269    }
270
271    fn webview_window_snapshot(&self, _webtag: &WebTag) -> StdResult<WindowsWebViewWindowSnapshot> {
272        unsupported_operation("webview_window_snapshot")
273    }
274
275    fn show_webview_window(
276        &self,
277        _webtag: &WebTag,
278        _title: &str,
279        _activate: bool,
280    ) -> StdResult<()> {
281        unsupported_operation("show_webview_window")
282    }
283
284    fn show_webview_window_with_content_size(
285        &self,
286        _webtag: &WebTag,
287        _title: &str,
288        _activate: bool,
289        _width: Option<i32>,
290        _height: Option<i32>,
291    ) -> StdResult<()> {
292        unsupported_operation("show_webview_window_with_content_size")
293    }
294
295    /// `full_chrome` runs the page to the window edge while the system keeps
296    /// minimize, maximize, resize, and drag.
297    fn show_webview_window_with_chrome(
298        &self,
299        _webtag: &WebTag,
300        _title: &str,
301        _activate: bool,
302        _width: Option<i32>,
303        _height: Option<i32>,
304        _full_chrome: bool,
305    ) -> StdResult<()> {
306        unsupported_operation("show_webview_window_with_chrome")
307    }
308
309    fn navigate_webview_window(
310        &self,
311        _webtag: &WebTag,
312        _title: &str,
313        _activate: bool,
314        _animation: WindowsNavAnimation,
315    ) -> StdResult<()> {
316        unsupported_operation("navigate_webview_window")
317    }
318
319    fn hide_webview_window(&self, _webtag: &WebTag) -> StdResult<()> {
320        unsupported_operation("hide_webview_window")
321    }
322
323    fn request_host_window_layout(&self, _window: WindowsHostWindow) -> bool {
324        false
325    }
326
327    fn active_content_screen_rect(&self) -> Option<WindowsContentRect> {
328        None
329    }
330
331    fn post_to_window_thread(&self, _window: isize, _callback: Box<dyn FnOnce() + Send>) -> bool {
332        false
333    }
334
335    fn sync_webview_window_layout(&self, _webtag: &WebTag) {}
336
337    /// Repaints an aside panel's chrome after a tab-strip change that leaves
338    /// the attached layout untouched (e.g. an inactive tab closed).
339    fn refresh_aside_panel(&self, _panel_id: &str) {}
340}
341
342pub fn refresh_aside_panel(panel_id: &str) {
343    if let Ok(backend) = backend() {
344        backend.refresh_aside_panel(panel_id);
345    }
346}
347
348pub fn set_windows_host_backend(backend: Arc<dyn WindowsHostBackend>) {
349    if BACKEND.set(backend).is_err() {
350        log::warn!("Windows host backend is already installed; ignoring replacement");
351    }
352}
353
354fn backend() -> StdResult<&'static Arc<dyn WindowsHostBackend>> {
355    BACKEND
356        .get()
357        .ok_or_else(|| WebViewError::WebView("Windows host backend is not installed".to_string()))
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361/// A rectangle of a host window whose pixels the compositor owns, not GDI.
362///
363/// `BitBlt` cannot see DirectComposition content, so anything drawn that way —
364/// WebView2 surfaces, the terminal grid — has to hand its pixels to the
365/// screenshot path or it is simply missing from every capture.
366pub struct WindowsSurfaceCapture {
367    /// Top-left in the host window's client coordinates.
368    pub x: i32,
369    pub y: i32,
370    pub width: u32,
371    pub height: u32,
372    /// Row-major BGRA, `width * height * 4` bytes.
373    pub pixels: Vec<u8>,
374}
375
376type SurfaceCaptureProvider = fn(usize) -> Vec<WindowsSurfaceCapture>;
377
378static SURFACE_CAPTURE_PROVIDERS: OnceLock<Mutex<Vec<SurfaceCaptureProvider>>> = OnceLock::new();
379
380fn surface_capture_providers() -> &'static Mutex<Vec<SurfaceCaptureProvider>> {
381    SURFACE_CAPTURE_PROVIDERS.get_or_init(|| Mutex::new(Vec::new()))
382}
383
384/// Offer composited pixels to screenshots. Called once per renderer.
385pub fn register_surface_capture_provider(provider: SurfaceCaptureProvider) {
386    if let Ok(mut providers) = surface_capture_providers().lock() {
387        providers.push(provider);
388    }
389}
390
391/// Every composited rectangle in `window_id`, for the screenshot path.
392pub fn surface_captures(window_id: usize) -> Vec<WindowsSurfaceCapture> {
393    let providers = match surface_capture_providers().lock() {
394        Ok(providers) => providers.clone(),
395        Err(_) => return Vec::new(),
396    };
397    providers
398        .into_iter()
399        .flat_map(|provider| provider(window_id))
400        .collect()
401}
402
403pub struct WindowsWebViewWindowSnapshot {
404    pub window_id: usize,
405    pub webtag_key: String,
406    pub visible: bool,
407    pub window_left: i32,
408    pub window_top: i32,
409    pub window_width: i32,
410    pub window_height: i32,
411    pub content_left: i32,
412    pub content_top: i32,
413    pub content_width: u32,
414    pub content_height: u32,
415    /// Composition-clip corner radii `[tl, tr, br, bl]` of the live surface
416    /// (zeros for windowed hosting), so screenshot compositing can reproduce
417    /// the on-screen rounding.
418    pub content_corner_radii: [i32; 4],
419}
420
421#[derive(Debug, Clone, Copy, PartialEq)]
422pub struct WindowsWebViewContentWindow {
423    pub window: isize,
424    pub content_left: i32,
425    pub content_top: i32,
426    pub content_width: i32,
427    pub content_height: i32,
428    pub scale: f64,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
432pub enum WindowsPanelPosition {
433    Left,
434    #[default]
435    Right,
436    Top,
437    Bottom,
438}
439
440/// The page-transition animation a `navigate` should play, mirroring the
441/// `AnimationType` the core computes from the JS navigation verb (forward slide
442/// for `navigateTo`, backward slide for `navigateBack`, none for
443/// `redirectTo`/`switchTab`/`reLaunch`). Kept as a contract-local enum so this
444/// crate needs no dependency on `lingxia-platform`.
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
446pub enum WindowsNavAnimation {
447    #[default]
448    None,
449    Forward,
450    Backward,
451}
452
453#[derive(Clone, Default)]
454pub struct WindowsWindowLayout {
455    payload: Option<Arc<dyn Any + Send + Sync>>,
456}
457
458impl std::fmt::Debug for WindowsWindowLayout {
459    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460        f.debug_struct("WindowsWindowLayout")
461            .field("has_payload", &self.payload.is_some())
462            .finish()
463    }
464}
465
466impl WindowsWindowLayout {
467    pub fn new<T>(payload: T) -> Self
468    where
469        T: Any + Send + Sync + 'static,
470    {
471        Self {
472            payload: Some(Arc::new(payload)),
473        }
474    }
475
476    pub fn empty() -> Self {
477        Self::default()
478    }
479
480    pub fn is_empty(&self) -> bool {
481        self.payload.is_none()
482    }
483
484    pub fn downcast_ref<T>(&self) -> Option<&T>
485    where
486        T: Any + 'static,
487    {
488        self.payload.as_deref()?.downcast_ref::<T>()
489    }
490}
491
492#[derive(Debug, Clone, PartialEq, Eq)]
493pub struct WindowsHostPanelTab {
494    pub id: u64,
495    pub title: String,
496    pub active: bool,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct WindowsHostPanelContent {
501    pub title: Option<String>,
502    pub body: Option<String>,
503    pub tabs: Vec<WindowsHostPanelTab>,
504    pub maximized: bool,
505    /// Whether the panel header exposes its expand/restore control. Native
506    /// main workspaces fill the workspace by definition, so only asides show
507    /// this affordance.
508    pub show_maximize: bool,
509}
510
511#[derive(Debug, Clone, PartialEq)]
512pub struct WindowsChromePanel {
513    pub panel_id: String,
514    pub webtag_key: String,
515    pub title: String,
516    pub rect: RECT,
517    /// Top-band slice (aligned with the main navbar baseline) where a browser
518    /// aside paints its address bar; `None` for panels with no band header.
519    pub header_rect: Option<RECT>,
520    /// Gutter between this panel and the neighboring workspace region. It is
521    /// both the resize hit target and the exposed first-layer shell surface.
522    pub resize_handle: Option<RECT>,
523    pub host_content: Option<WindowsHostPanelContent>,
524    pub docked: bool,
525    /// Covers the main workspace without reserving split space.
526    pub overlay: bool,
527}
528
529#[derive(Debug, Clone, PartialEq, Eq)]
530pub struct WindowsChromePanelLayoutInput {
531    pub panel_id: String,
532    pub webtag_key: String,
533    pub position: WindowsPanelPosition,
534    pub requested_size: Option<i32>,
535    pub docked: bool,
536    /// Cover the host workspace without consuming main layout space.
537    pub overlay: bool,
538    pub maximized: bool,
539}
540
541#[derive(Debug, Clone, PartialEq)]
542pub struct WindowsChromePanelLayout {
543    pub panel_id: String,
544    pub webtag_key: String,
545    pub rect: RECT,
546    /// Top-band slice for a browser aside's address bar (see
547    /// [`WindowsChromePanel::header_rect`]); `None` when the panel has none.
548    pub header_rect: Option<RECT>,
549    pub resize_handle: Option<RECT>,
550    /// Covers the main workspace without reserving split space.
551    pub overlay: bool,
552}
553
554#[derive(Debug, Clone, PartialEq)]
555pub struct WindowsChromeAttachedLayout {
556    /// Full main region after aside arbitration, including main-owned chrome.
557    pub main_region: RECT,
558    /// Main WebView viewport after reserving its navigation bar.
559    pub main: RECT,
560    pub panels: Vec<WindowsChromePanelLayout>,
561}
562
563#[derive(Debug, Clone, PartialEq)]
564pub struct WindowsChromeAttachedState {
565    pub main_region: RECT,
566    pub main: RECT,
567    pub panels: Vec<WindowsChromePanel>,
568}
569
570#[derive(Debug, Clone)]
571pub struct WindowsChromeState {
572    pub hwnd: HWND,
573    pub client: RECT,
574    pub layout: WindowsWindowLayout,
575    pub attached: Option<WindowsChromeAttachedState>,
576    pub frame_button_hover: Option<WindowsFrameButton>,
577    pub frame_button_pressed: Option<WindowsFrameButton>,
578    /// Client-space cursor position while over this window's chrome; drives
579    /// hover feedback (frame buttons keep their dedicated state above).
580    pub cursor: Option<(i32, i32)>,
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub enum WindowsFrameButton {
585    Minimize,
586    Maximize,
587    Close,
588}
589
590#[derive(Debug, Clone, PartialEq)]
591pub struct WindowsChromeCommand {
592    pub id: String,
593    pub payload: serde_json::Value,
594    pub focus: Option<String>,
595    pub double_click: Option<Box<WindowsChromeCommand>>,
596    pub include_screen_position: bool,
597}
598
599impl WindowsChromeCommand {
600    pub fn new(id: impl Into<String>) -> Self {
601        Self {
602            id: id.into(),
603            payload: serde_json::Value::Null,
604            focus: None,
605            double_click: None,
606            include_screen_position: false,
607        }
608    }
609
610    pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
611        self.payload = payload;
612        self
613    }
614
615    pub fn with_focus(mut self, surface_id: impl Into<String>) -> Self {
616        self.focus = Some(surface_id.into());
617        self
618    }
619
620    pub fn with_double_click(mut self, command: WindowsChromeCommand) -> Self {
621        self.double_click = Some(Box::new(command));
622        self
623    }
624
625    pub fn with_screen_position(mut self) -> Self {
626        self.include_screen_position = true;
627        self
628    }
629}
630
631#[derive(Debug, Clone, PartialEq)]
632pub enum WindowsChromeHit {
633    Caption,
634    FrameButton(WindowsFrameButton),
635    Focusable {
636        id: String,
637        context_menu: Option<WindowsChromeCommand>,
638        /// Optional command invoked on left-button-down in addition to
639        /// focusing the surface (e.g. focusing the terminal pane under the
640        /// cursor). Carries the click's screen position when requested.
641        click_command: Option<WindowsChromeCommand>,
642    },
643    Command(WindowsChromeCommand),
644    CommandWithContext {
645        command: WindowsChromeCommand,
646        context_menu: WindowsChromeCommand,
647    },
648    Chrome,
649}
650
651pub trait WindowsChromeRenderer: Send + Sync {
652    fn content_rect(&self, client: RECT, layout: &WindowsWindowLayout) -> RECT;
653
654    fn attached_layout(
655        &self,
656        client: RECT,
657        layout: &WindowsWindowLayout,
658        panels: &[WindowsChromePanelLayoutInput],
659    ) -> Option<WindowsChromeAttachedLayout> {
660        let _ = (client, layout, panels);
661        None
662    }
663
664    fn paint(&self, hdc: HDC, state: &WindowsChromeState);
665
666    fn paint_region(&self, hdc: HDC, state: &WindowsChromeState, invalid: RECT) {
667        let _ = invalid;
668        self.paint(hdc, state);
669    }
670
671    fn hit_test(&self, state: &WindowsChromeState, point: (i32, i32)) -> Option<WindowsChromeHit>;
672
673    fn frame_button_rect(
674        &self,
675        state: &WindowsChromeState,
676        button: WindowsFrameButton,
677    ) -> Option<RECT> {
678        let _ = (state, button);
679        None
680    }
681
682    /// Bounding rect of the hover-highlightable element under `point`; the
683    /// host invalidates the rects the cursor enters/leaves so hover feedback
684    /// repaints exactly the affected element.
685    fn hover_rect(&self, state: &WindowsChromeState, point: (i32, i32)) -> Option<RECT> {
686        let _ = (state, point);
687        None
688    }
689
690    /// Translate a wheel gesture over owner-drawn chrome into a runtime
691    /// command. Returning `None` lets the host forward the gesture normally.
692    fn mouse_wheel(
693        &self,
694        state: &WindowsChromeState,
695        point: (i32, i32),
696        delta: i16,
697    ) -> Option<WindowsChromeCommand> {
698        let _ = (state, point, delta);
699        None
700    }
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
704pub struct WindowsHostPanelKeyEvent {
705    pub vk: u32,
706    pub ctrl: bool,
707    pub shift: bool,
708    pub alt: bool,
709    pub character: Option<char>,
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub struct WindowsHostWindow {
714    pub window: isize,
715}
716
717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
718pub struct WindowsContentRect {
719    pub host_window: isize,
720    pub left: i32,
721    pub top: i32,
722    pub width: i32,
723    pub height: i32,
724    pub dpi: u32,
725}
726
727pub fn set_default_window_size(width: i32, height: i32) {
728    if width > 0 && height > 0 {
729        let _ = DEFAULT_WINDOW_SIZE.set((width, height));
730    }
731}
732
733pub fn default_window_size() -> (i32, i32) {
734    DEFAULT_WINDOW_SIZE.get().copied().unwrap_or((1024, 768))
735}
736
737pub fn set_windows_chrome_renderer(renderer: Arc<dyn WindowsChromeRenderer>) {
738    let slot = WINDOWS_CHROME_RENDERER.get_or_init(|| Mutex::new(None));
739    if let Ok(mut slot) = slot.lock() {
740        *slot = Some(renderer);
741    }
742}
743
744pub fn windows_chrome_renderer() -> Option<Arc<dyn WindowsChromeRenderer>> {
745    WINDOWS_CHROME_RENDERER
746        .get()
747        .and_then(|renderer| renderer.lock().ok())
748        .and_then(|renderer| renderer.clone())
749}
750
751pub fn set_webview_close_handler(webtag: &WebTag, handler: CloseHandler) {
752    let handlers = CLOSE_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
753    if let Ok(mut handlers) = handlers.lock() {
754        handlers.insert(webtag.key().to_string(), handler);
755    }
756}
757
758pub fn webview_close_handler(webtag_key: &str) -> Option<CloseHandler> {
759    CLOSE_HANDLERS
760        .get()
761        .and_then(|handlers| handlers.lock().ok())
762        .and_then(|handlers| handlers.get(webtag_key).cloned())
763}
764
765pub fn set_webview_visibility_handler(handler: WebViewVisibilityHandler) {
766    let slot = VISIBILITY_HANDLER.get_or_init(|| Mutex::new(None));
767    if let Ok(mut slot) = slot.lock() {
768        *slot = Some(handler);
769    }
770}
771
772pub fn webview_visibility_handler() -> Option<WebViewVisibilityHandler> {
773    VISIBILITY_HANDLER
774        .get()
775        .and_then(|slot| slot.lock().ok())
776        .and_then(|slot| slot.clone())
777}
778
779pub fn set_webview_chrome_event_handler(webtag: &WebTag, handler: ChromeEventHandler) {
780    let handlers = CHROME_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
781    if let Ok(mut handlers) = handlers.lock() {
782        handlers.insert(webtag.key().to_string(), handler);
783    }
784}
785
786pub fn webview_chrome_event_handler(webtag_key: &str) -> Option<ChromeEventHandler> {
787    CHROME_HANDLERS
788        .get()
789        .and_then(|handlers| handlers.lock().ok())
790        .and_then(|handlers| handlers.get(webtag_key).cloned())
791}
792
793pub fn add_host_window_created_handler(handler: HostWindowCreatedHandler) {
794    let handlers = HOST_WINDOW_CREATED_HANDLERS.get_or_init(|| Mutex::new(Vec::new()));
795    if let Ok(mut handlers) = handlers.lock() {
796        handlers.push(handler);
797    }
798}
799
800pub fn host_window_created_handlers() -> Vec<HostWindowCreatedHandler> {
801    HOST_WINDOW_CREATED_HANDLERS
802        .get()
803        .and_then(|state| state.lock().ok())
804        .map(|state| state.clone())
805        .unwrap_or_default()
806}
807
808pub fn set_host_panel_input_handler(panel_id: &str, handler: WindowsHostPanelInputHandler) {
809    let handlers = HOST_PANEL_INPUT_HANDLERS.get_or_init(|| Mutex::new(HashMap::new()));
810    if let Ok(mut handlers) = handlers.lock() {
811        handlers.insert(panel_id.to_string(), handler);
812    }
813}
814
815pub fn clear_host_panel_input_handler(panel_id: &str) {
816    if let Some(handlers) = HOST_PANEL_INPUT_HANDLERS.get()
817        && let Ok(mut handlers) = handlers.lock()
818    {
819        handlers.remove(panel_id);
820    }
821}
822
823pub fn host_panel_input_handler(panel_id: &str) -> Option<WindowsHostPanelInputHandler> {
824    HOST_PANEL_INPUT_HANDLERS
825        .get()
826        .and_then(|handlers| handlers.lock().ok())
827        .and_then(|handlers| handlers.get(panel_id).cloned())
828}
829
830pub fn set_webview_window_layout(webtag: &WebTag, layout: WindowsWindowLayout) -> StdResult<()> {
831    let layouts = WINDOW_LAYOUTS.get_or_init(|| Mutex::new(HashMap::new()));
832    if let Ok(mut layouts) = layouts.lock() {
833        layouts.insert(webtag.key().to_string(), layout);
834    }
835    if let Ok(backend) = backend() {
836        backend.sync_webview_window_layout(webtag);
837    }
838    Ok(())
839}
840
841pub fn current_window_layout(webtag_key: &str) -> WindowsWindowLayout {
842    WINDOW_LAYOUTS
843        .get()
844        .and_then(|layouts| layouts.lock().ok())
845        .and_then(|layouts| layouts.get(webtag_key).cloned())
846        .unwrap_or_default()
847}
848
849pub fn cleanup_webview_state(webtag_key: &str) {
850    if let Some(handlers) = CLOSE_HANDLERS.get()
851        && let Ok(mut handlers) = handlers.lock()
852    {
853        handlers.remove(webtag_key);
854    }
855    if let Some(handlers) = CHROME_HANDLERS.get()
856        && let Ok(mut handlers) = handlers.lock()
857    {
858        handlers.remove(webtag_key);
859    }
860    if let Some(layouts) = WINDOW_LAYOUTS.get()
861        && let Ok(mut layouts) = layouts.lock()
862    {
863        layouts.remove(webtag_key);
864    }
865}
866
867pub fn show_webview_as_panel(webtag: &WebTag, title: &str, panel_id: &str) -> StdResult<()> {
868    backend()?.show_webview_as_panel(webtag, title, panel_id)
869}
870
871pub fn show_webview_as_adaptive_panel(
872    webtag: &WebTag,
873    title: &str,
874    panel_id: &str,
875    position: WindowsPanelPosition,
876    preferred_size: Option<i32>,
877) -> StdResult<()> {
878    backend()?.show_webview_as_adaptive_panel(webtag, title, panel_id, position, preferred_size)
879}
880
881pub fn show_webview_as_overlay_panel(
882    webtag: &WebTag,
883    title: &str,
884    panel_id: &str,
885    position: WindowsPanelPosition,
886) -> StdResult<()> {
887    backend()?.show_webview_as_overlay_panel(webtag, title, panel_id, position)
888}
889
890pub fn present_webview_in_active_group(webtag: &WebTag) -> StdResult<()> {
891    backend()?.present_webview_in_active_group(webtag)
892}
893
894pub fn active_host_window_is_device_framed() -> bool {
895    backend()
896        .map(|backend| backend.active_host_window_is_device_framed())
897        .unwrap_or(false)
898}
899
900pub fn active_host_window_webtag_key() -> Option<String> {
901    backend()
902        .ok()
903        .and_then(|backend| backend.active_host_window_webtag_key())
904}
905
906pub fn present_webview_as_group_main(webtag: &WebTag, group_key: String) -> StdResult<()> {
907    backend()?.present_webview_as_group_main(webtag, group_key)
908}
909
910pub fn present_webview_as_overlay(
911    webtag: &WebTag,
912    width: f64,
913    height: f64,
914    width_ratio: f64,
915    height_ratio: f64,
916    position: u8,
917) -> StdResult<()> {
918    backend()?.present_webview_as_overlay(
919        webtag,
920        width,
921        height,
922        width_ratio,
923        height_ratio,
924        position,
925    )
926}
927
928pub fn configure_webview_surface_interaction(
929    webtag: &WebTag,
930    close_button: bool,
931    dismiss_on_outside: bool,
932    modal: bool,
933) -> StdResult<()> {
934    backend()?.configure_webview_surface_interaction(
935        webtag,
936        close_button,
937        dismiss_on_outside,
938        modal,
939    )
940}
941
942pub fn resize_host_window_content(webtag: &WebTag, width: i32, height: i32) -> StdResult<()> {
943    backend()?.resize_host_window_content(webtag, width, height)
944}
945
946pub fn restore_presented_group_main() -> StdResult<()> {
947    backend()?.restore_presented_group_main()
948}
949
950pub fn show_interactive_host_panel(
951    panel_id: &str,
952    title: &str,
953    body: &str,
954    position: WindowsPanelPosition,
955) -> StdResult<()> {
956    backend()?.show_interactive_host_panel(panel_id, title, body, position)
957}
958
959pub fn hide_host_panel(panel_id: &str) -> StdResult<()> {
960    backend()?.hide_host_panel(panel_id)
961}
962
963pub fn hide_exclusive_tray_popover() -> bool {
964    backend()
965        .map(|backend| backend.hide_exclusive_tray_popover())
966        .unwrap_or(false)
967}
968
969pub fn show_exclusive_tray_popover() -> bool {
970    backend()
971        .map(|backend| backend.show_exclusive_tray_popover())
972        .unwrap_or(false)
973}
974
975pub fn update_host_panel_body(panel_id: &str, body: &str) -> StdResult<()> {
976    backend()?.update_host_panel_body(panel_id, body)
977}
978
979pub fn set_host_panel_tabs(panel_id: &str, tabs: Vec<WindowsHostPanelTab>) -> bool {
980    backend()
981        .map(|backend| backend.set_host_panel_tabs(panel_id, tabs))
982        .unwrap_or(false)
983}
984
985pub fn set_host_panel_maximized(panel_id: &str, maximized: bool) -> bool {
986    backend()
987        .map(|backend| backend.set_host_panel_maximized(panel_id, maximized))
988        .unwrap_or(false)
989}
990
991pub fn invalidate_host_panel(panel_id: &str) -> bool {
992    backend()
993        .map(|backend| backend.invalidate_host_panel(panel_id))
994        .unwrap_or(false)
995}
996
997pub fn is_panel_visible(panel_id: &str) -> bool {
998    backend()
999        .map(|backend| backend.is_panel_visible(panel_id))
1000        .unwrap_or(false)
1001}
1002
1003pub fn find_host_window_for_webview(webtag: &WebTag) -> StdResult<WindowsHostWindow> {
1004    let content = find_webview_content_window(webtag).ok_or_else(|| {
1005        WebViewError::WebView(format!("no window registered for {}", webtag.key()))
1006    })?;
1007    Ok(WindowsHostWindow {
1008        window: content.window,
1009    })
1010}
1011
1012pub fn request_host_window_layout(window: WindowsHostWindow) -> bool {
1013    backend()
1014        .map(|backend| backend.request_host_window_layout(window))
1015        .unwrap_or(false)
1016}
1017
1018pub fn active_content_screen_rect() -> Option<WindowsContentRect> {
1019    backend()
1020        .ok()
1021        .and_then(|backend| backend.active_content_screen_rect())
1022}
1023
1024pub fn find_webview_content_window(webtag: &WebTag) -> Option<WindowsWebViewContentWindow> {
1025    backend()
1026        .ok()
1027        .and_then(|backend| backend.find_webview_content_window(webtag))
1028}
1029
1030pub fn webview_window_snapshot(webtag: &WebTag) -> StdResult<WindowsWebViewWindowSnapshot> {
1031    backend()?.webview_window_snapshot(webtag)
1032}
1033
1034pub fn show_webview_window(webtag: &WebTag, title: &str, activate: bool) -> StdResult<()> {
1035    backend()?.show_webview_window(webtag, title, activate)
1036}
1037
1038pub fn show_webview_window_with_content_size(
1039    webtag: &WebTag,
1040    title: &str,
1041    activate: bool,
1042    width: Option<i32>,
1043    height: Option<i32>,
1044) -> StdResult<()> {
1045    backend()?.show_webview_window_with_content_size(webtag, title, activate, width, height)
1046}
1047
1048pub fn show_webview_window_with_chrome(
1049    webtag: &WebTag,
1050    title: &str,
1051    activate: bool,
1052    width: Option<i32>,
1053    height: Option<i32>,
1054    full_chrome: bool,
1055) -> StdResult<()> {
1056    backend()?.show_webview_window_with_chrome(webtag, title, activate, width, height, full_chrome)
1057}
1058
1059pub fn navigate_webview_window(
1060    webtag: &WebTag,
1061    title: &str,
1062    activate: bool,
1063    animation: WindowsNavAnimation,
1064) -> StdResult<()> {
1065    backend()?.navigate_webview_window(webtag, title, activate, animation)
1066}
1067
1068pub fn hide_webview_window(webtag: &WebTag) -> StdResult<()> {
1069    backend()?.hide_webview_window(webtag)
1070}
1071
1072pub fn post_to_window_thread(window: isize, callback: Box<dyn FnOnce() + Send>) -> bool {
1073    backend()
1074        .map(|backend| backend.post_to_window_thread(window, callback))
1075        .unwrap_or(false)
1076}