Skip to main content

windows_webview/
webview.rs

1use super::*;
2use crate::handler::subscription;
3
4/// The level WebView2 should target for the browser's memory usage, set with
5/// [`WebView::set_memory_usage_target_level`].
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum MemoryUsageTargetLevel {
8    /// Normal memory usage.
9    Normal,
10    /// Reduced memory usage, suitable for a hidden or background `WebView`.
11    Low,
12}
13
14impl MemoryUsageTargetLevel {
15    fn from_raw(value: COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL) -> Self {
16        match value {
17            1 => Self::Low,
18            _ => Self::Normal,
19        }
20    }
21
22    fn to_raw(self) -> COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL {
23        match self {
24            Self::Normal => 0,
25            Self::Low => 1,
26        }
27    }
28}
29
30/// A request to navigate with a custom HTTP method, headers, or body, passed to
31/// [`WebView::navigate_with_request`]. Defaults to a `GET` with no extra headers
32/// or body.
33#[derive(Clone, Debug)]
34pub struct NavigationRequest {
35    uri: String,
36    method: String,
37    headers: Vec<(String, String)>,
38    body: Vec<u8>,
39}
40
41impl NavigationRequest {
42    /// Creates a `GET` request for `uri`.
43    pub fn new(uri: &str) -> Self {
44        Self {
45            uri: uri.to_string(),
46            method: "GET".to_string(),
47            headers: Vec::new(),
48            body: Vec::new(),
49        }
50    }
51
52    /// Sets the HTTP method, for example `POST`.
53    pub fn method(mut self, method: &str) -> Self {
54        self.method = method.to_string();
55        self
56    }
57
58    /// Adds a request header, such as an `Authorization` token.
59    pub fn header(mut self, name: &str, value: &str) -> Self {
60        self.headers.push((name.to_string(), value.to_string()));
61        self
62    }
63
64    /// Sets the request body bytes.
65    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
66        self.body = body.into();
67        self
68    }
69}
70
71/// How a folder mapped with
72/// [`WebView::set_virtual_host_name_to_folder_mapping`] may be accessed.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum HostResourceAccessKind {
75    /// Resources from other origins cannot access the mapped content.
76    Deny,
77    /// Resources from any origin may access the mapped content.
78    Allow,
79    /// Like [`Deny`](Self::Deny), but cross-origin requests are allowed through
80    /// CORS.
81    DenyCors,
82}
83
84impl HostResourceAccessKind {
85    fn to_raw(self) -> COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND {
86        match self {
87            Self::Deny => 0,
88            Self::Allow => 1,
89            Self::DenyCors => 2,
90        }
91    }
92}
93
94/// A WebView2 browser. Navigate to URLs and run JavaScript against the hosted
95/// page.
96#[derive(Clone)]
97pub struct WebView(pub(crate) ICoreWebView2);
98
99impl WebView {
100    /// Wraps an existing `ICoreWebView2`. Used by the optional `reactor` feature
101    /// to build a `WebView` from the WinUI XAML `WebView2` control's bridged COM
102    /// core.
103    #[cfg(feature = "reactor")]
104    pub(crate) fn from_core(core: ICoreWebView2) -> Self {
105        Self(core)
106    }
107
108    /// Navigates the browser to the given URI.
109    pub fn navigate(&self, uri: &str) -> Result<()> {
110        let uri = HSTRING::from(uri);
111        unsafe { self.0.Navigate(&uri) }.ok()
112    }
113
114    /// Navigates the browser to the given HTML content as the document.
115    pub fn navigate_to_string(&self, html: &str) -> Result<()> {
116        let html = HSTRING::from(html);
117        unsafe { self.0.NavigateToString(&html) }.ok()
118    }
119
120    /// Navigates the browser using a [`NavigationRequest`], allowing a custom
121    /// HTTP method, request headers, or body - for example a `POST` or an
122    /// `Authorization` header that a plain [`navigate`](Self::navigate) cannot
123    /// supply.
124    pub fn navigate_with_request(&self, request: &NavigationRequest) -> Result<()> {
125        let source: ICoreWebView2_2 = self.0.cast()?;
126        let environment: ICoreWebView2Environment2 = unsafe { source.Environment()? }.cast()?;
127
128        let uri = HSTRING::from(&request.uri);
129        let method = HSTRING::from(&request.method);
130        let mut headers = String::new();
131        for (name, value) in &request.headers {
132            headers.push_str(name);
133            headers.push_str(": ");
134            headers.push_str(value);
135            headers.push_str("\r\n");
136        }
137        let headers = HSTRING::from(&headers);
138        let stream = if request.body.is_empty() {
139            None
140        } else {
141            unsafe { SHCreateMemStream(request.body.as_ptr(), request.body.len() as u32) }
142        };
143
144        unsafe {
145            let request =
146                environment.CreateWebResourceRequest(&uri, &method, stream.as_ref(), &headers)?;
147            source.NavigateWithWebResourceRequest(&request).ok()
148        }
149    }
150
151    /// Reloads the current page.
152    pub fn reload(&self) -> Result<()> {
153        unsafe { self.0.Reload() }.ok()
154    }
155
156    /// Opens the DevTools window for the page, the same view shown by the
157    /// browser's "Inspect" command.
158    pub fn open_dev_tools_window(&self) -> Result<()> {
159        unsafe { self.0.OpenDevToolsWindow() }.ok()
160    }
161
162    /// Stops any in-progress navigation or download.
163    pub fn stop(&self) -> Result<()> {
164        unsafe { self.0.Stop() }.ok()
165    }
166
167    /// Navigates back to the previous page in the navigation history.
168    pub fn go_back(&self) -> Result<()> {
169        unsafe { self.0.GoBack() }.ok()
170    }
171
172    /// Navigates forward to the next page in the navigation history.
173    pub fn go_forward(&self) -> Result<()> {
174        unsafe { self.0.GoForward() }.ok()
175    }
176
177    /// Returns the URI of the current top-level document.
178    pub fn source(&self) -> String {
179        unsafe { string::take_result(self.0.Source()) }
180    }
181
182    /// Returns the title of the current top-level document.
183    pub fn document_title(&self) -> String {
184        unsafe { string::take_result(self.0.DocumentTitle()) }
185    }
186
187    /// Maps a virtual host name to a local folder so the page can load its files
188    /// over a normal URL such as `https://app.example/index.html`. `access_kind`
189    /// controls cross-origin access to the mapped files.
190    pub fn set_virtual_host_name_to_folder_mapping(
191        &self,
192        host_name: &str,
193        folder_path: &str,
194        access_kind: HostResourceAccessKind,
195    ) -> Result<()> {
196        let source: ICoreWebView2_3 = self.0.cast()?;
197        let host_name = HSTRING::from(host_name);
198        let folder_path = HSTRING::from(folder_path);
199        unsafe {
200            source
201                .SetVirtualHostNameToFolderMapping(&host_name, &folder_path, access_kind.to_raw())
202                .ok()
203        }
204    }
205
206    /// Removes a mapping previously created with
207    /// [`set_virtual_host_name_to_folder_mapping`](Self::set_virtual_host_name_to_folder_mapping).
208    pub fn clear_virtual_host_name_to_folder_mapping(&self, host_name: &str) -> Result<()> {
209        let source: ICoreWebView2_3 = self.0.cast()?;
210        let host_name = HSTRING::from(host_name);
211        unsafe { source.ClearVirtualHostNameToFolderMapping(&host_name) }.ok()
212    }
213
214    /// Returns the [`CookieManager`] for reading, writing, and deleting the
215    /// browser's cookies.
216    pub fn cookie_manager(&self) -> Result<CookieManager> {
217        let source: ICoreWebView2_2 = self.0.cast()?;
218        Ok(CookieManager(unsafe { source.CookieManager()? }))
219    }
220
221    /// Returns the [`Profile`] this browser belongs to, exposing its color
222    /// scheme, download folder, and browsing-data controls.
223    pub fn profile(&self) -> Result<Profile> {
224        let source: ICoreWebView2_13 = self.0.cast()?;
225        Ok(Profile(unsafe { source.Profile()? }))
226    }
227
228    /// Returns `true` if the page currently has an element displayed full screen
229    /// (for example a video using the HTML Fullscreen API).
230    pub fn contains_fullscreen_element(&self) -> bool {
231        unsafe { self.0.ContainsFullScreenElement() }.is_ok_and(|value| value.as_bool())
232    }
233
234    /// Returns the memory-usage level WebView2 is targeting.
235    pub fn memory_usage_target_level(&self) -> Result<MemoryUsageTargetLevel> {
236        let source: ICoreWebView2_19 = self.0.cast()?;
237        Ok(MemoryUsageTargetLevel::from_raw(unsafe {
238            source.MemoryUsageTargetLevel()?
239        }))
240    }
241
242    /// Hints the memory-usage level WebView2 should target. Set
243    /// [`MemoryUsageTargetLevel::Low`] when the `WebView` is hidden so the
244    /// browser can trim memory, and back to `Normal` when it is shown again.
245    pub fn set_memory_usage_target_level(&self, level: MemoryUsageTargetLevel) -> Result<()> {
246        let source: ICoreWebView2_19 = self.0.cast()?;
247        unsafe { source.SetMemoryUsageTargetLevel(level.to_raw()) }.ok()
248    }
249
250    /// Returns the [`Settings`] controlling features such as JavaScript, the dev
251    /// tools, and context menus.
252    pub fn settings(&self) -> Result<Settings> {
253        unsafe { Ok(Settings(self.0.Settings()?)) }
254    }
255
256    /// Asynchronously runs JavaScript in the context of the current page. The
257    /// `handler` closure receives the JSON-encoded result on the UI thread.
258    pub fn execute_script<F: FnOnce(Result<String>) + 'static>(
259        &self,
260        javascript: &str,
261        handler: F,
262    ) -> Result<()> {
263        let javascript = HSTRING::from(javascript);
264        let handler = handler::ExecuteScriptCompleted::create(handler);
265        unsafe { self.0.ExecuteScript(&javascript, &handler) }.ok()
266    }
267
268    /// Asynchronously calls a Chrome DevTools Protocol method.
269    ///
270    /// `params_json` is the method's JSON object argument, or `"{}"` for none.
271    pub fn call_dev_tools_protocol_method<F: FnOnce(Result<String>) + 'static>(
272        &self,
273        method: &str,
274        params_json: &str,
275        handler: F,
276    ) -> Result<()> {
277        let method = HSTRING::from(method);
278        let params = HSTRING::from(params_json);
279        let handler = handler::CallDevToolsProtocolMethodCompleted::create(handler);
280        unsafe {
281            self.0
282                .CallDevToolsProtocolMethod(&method, &params, &handler)
283        }
284        .ok()
285    }
286
287    /// Subscribes to a Chrome DevTools Protocol event by name.
288    ///
289    /// Most CDP events require enabling their domain before they fire.
290    pub fn on_dev_tools_protocol_event<F>(
291        &self,
292        event_name: &str,
293        handler: F,
294    ) -> Result<EventRegistration>
295    where
296        F: FnMut(DevToolsProtocolEventReceivedArgs) + 'static,
297    {
298        let event_name = HSTRING::from(event_name);
299        let receiver = unsafe { self.0.GetDevToolsProtocolEventReceiver(&event_name)? };
300        let handler = handler::DevToolsProtocolEventReceived::create(handler);
301        let token = unsafe { receiver.add_DevToolsProtocolEventReceived(&handler)? };
302        Ok(EventRegistration::new(move || {
303            let _ = unsafe { receiver.remove_DevToolsProtocolEventReceived(token) };
304        }))
305    }
306
307    /// Registers JavaScript to run before any other script in each new document.
308    ///
309    /// Pumps the calling thread's message loop until registration completes, so
310    /// call it during setup before handing control to your own message loop.
311    pub fn add_script_to_execute_on_document_created(&self, javascript: &str) -> Result<ScriptId> {
312        let javascript = HSTRING::from(javascript);
313        let slot = pump::slot();
314        let handler = handler::AddScriptCompleted::create(pump::slot_handler(&slot));
315        unsafe {
316            self.0
317                .AddScriptToExecuteOnDocumentCreated(&javascript, &handler)
318                .ok()?;
319        }
320        Ok(ScriptId(pump::wait(&slot)?))
321    }
322
323    /// Removes a script previously registered on document creation.
324    pub fn remove_script_to_execute_on_document_created(&self, id: &ScriptId) -> Result<()> {
325        let id = HSTRING::from(&id.0);
326        unsafe { self.0.RemoveScriptToExecuteOnDocumentCreated(&id) }.ok()
327    }
328
329    subscription! {
330        /// Subscribes to the navigation-starting event, raised before each
331        /// navigation. The handler may inspect the target and cancel it via
332        /// [`NavigationStartingArgs::set_cancel`].
333        on_navigation_starting(NavigationStartingArgs) =>
334            NavigationStarting, add_NavigationStarting / remove_NavigationStarting
335    }
336
337    subscription! {
338        /// Subscribes to the navigation-completed event.
339        on_navigation_completed(NavigationCompletedArgs) =>
340            NavigationCompleted, add_NavigationCompleted / remove_NavigationCompleted
341    }
342
343    subscription! {
344        /// Subscribes to the process-failed event.
345        ///
346        /// A renderer crash can be reloaded; a browser-process exit requires a new `WebView`.
347        on_process_failed(ProcessFailedArgs) =>
348            ProcessFailed, add_ProcessFailed / remove_ProcessFailed
349    }
350
351    /// Subscribes to HTML fullscreen state changes.
352    pub fn on_contains_fullscreen_element_changed<F: FnMut(bool) + 'static>(
353        &self,
354        handler: F,
355    ) -> Result<EventRegistration> {
356        let handler = handler::ContainsFullScreenElementChanged::create(handler);
357        let token = unsafe { self.0.add_ContainsFullScreenElementChanged(&handler)? };
358        let source = self.0.clone();
359        Ok(EventRegistration::new(move || {
360            let _ = unsafe { source.remove_ContainsFullScreenElementChanged(token) };
361        }))
362    }
363
364    /// Posts a message to the hosted page as a JSON value. The page receives it
365    /// via the `window.chrome.webview.addEventListener("message", ...)` event,
366    /// with `event.data` set to the parsed JSON.
367    pub fn post_web_message_as_json(&self, json: &str) -> Result<()> {
368        let json = HSTRING::from(json);
369        unsafe { self.0.PostWebMessageAsJson(&json) }.ok()
370    }
371
372    /// Posts a message to the hosted page as a string. The page receives it via
373    /// the `window.chrome.webview.addEventListener("message", ...)` event, with
374    /// `event.data` set to the string.
375    pub fn post_web_message_as_string(&self, message: &str) -> Result<()> {
376        let message = HSTRING::from(message);
377        unsafe { self.0.PostWebMessageAsString(&message) }.ok()
378    }
379
380    subscription! {
381        /// Subscribes to the web-message-received event, raised when the hosted
382        /// page calls `window.chrome.webview.postMessage`.
383        on_web_message_received(WebMessageReceivedArgs) =>
384            WebMessageReceived, add_WebMessageReceived / remove_WebMessageReceived
385    }
386
387    subscription! {
388        /// Subscribes to the content-loading event, raised when the browser
389        /// starts loading content for a new document.
390        on_content_loading(ContentLoadingArgs) =>
391            ContentLoading, add_ContentLoading / remove_ContentLoading
392    }
393
394    subscription! {
395        /// Subscribes to the document-title-changed event. The handler receives
396        /// the new [`document_title`](Self::document_title).
397        on_document_title_changed(String) =>
398            DocumentTitleChanged, add_DocumentTitleChanged / remove_DocumentTitleChanged
399    }
400
401    /// Subscribes to the window-close-requested event, raised when the hosted
402    /// page calls `window.close()`. The host typically responds by closing its
403    /// window.
404    pub fn on_window_close_requested<F: FnMut() + 'static>(
405        &self,
406        handler: F,
407    ) -> Result<EventRegistration> {
408        let handler = handler::WindowCloseRequested::create(handler);
409        let token = unsafe { self.0.add_WindowCloseRequested(&handler)? };
410        let source = self.0.clone();
411        Ok(EventRegistration::new(move || {
412            let _ = unsafe { source.remove_WindowCloseRequested(token) };
413        }))
414    }
415
416    subscription! {
417        /// Subscribes to the new-window-requested event, raised when the page
418        /// tries to open a new window (for example via `window.open`). The
419        /// handler may suppress, redirect, or
420        /// [defer](NewWindowRequestedArgs::defer) the request.
421        on_new_window_requested(NewWindowRequestedArgs) =>
422            NewWindowRequested, add_NewWindowRequested / remove_NewWindowRequested
423    }
424
425    subscription! {
426        /// Subscribes to the permission-requested event, raised when the page
427        /// requests access to a capability such as the camera or geolocation.
428        /// The handler decides the outcome via
429        /// [`PermissionRequestedArgs::set_state`] and may
430        /// [defer](PermissionRequestedArgs::defer) the decision.
431        on_permission_requested(PermissionRequestedArgs) =>
432            PermissionRequested, add_PermissionRequested / remove_PermissionRequested
433    }
434
435    /// Subscribes to the download-starting event, raised when a download begins.
436    /// The handler receives a [`DownloadStartingArgs`] to inspect or control the
437    /// [`DownloadOperation`], change its destination, or cancel it.
438    pub fn on_download_starting<F: FnMut(DownloadStartingArgs) + 'static>(
439        &self,
440        handler: F,
441    ) -> Result<EventRegistration> {
442        let source: ICoreWebView2_4 = self.0.cast()?;
443        let handler = handler::DownloadStarting::create(handler);
444        let token = unsafe { source.add_DownloadStarting(&handler)? };
445        Ok(EventRegistration::new(move || {
446            let _ = unsafe { source.remove_DownloadStarting(token) };
447        }))
448    }
449
450    /// Subscribes to matching resource requests and optionally fulfills them from memory.
451    pub fn on_web_resource_requested<F>(
452        &self,
453        uri_filter: &str,
454        handler: F,
455    ) -> Result<EventRegistration>
456    where
457        F: FnMut(WebResourceRequest) -> Option<WebResourceResponse> + 'static,
458    {
459        let environment = unsafe { self.0.cast::<ICoreWebView2_2>()?.Environment()? };
460        let filter = HSTRING::from(uri_filter);
461        unsafe { protocol::add_requested_filter(&self.0, &filter)? };
462        let handler = protocol::WebResourceRequested::create(environment, handler);
463        let token = match unsafe { self.0.add_WebResourceRequested(&handler) } {
464            Ok(token) => token,
465            Err(err) => {
466                unsafe {
467                    protocol::remove_requested_filter(&self.0, &filter);
468                };
469                return Err(err);
470            }
471        };
472        let source = self.0.clone();
473        Ok(EventRegistration::new(move || {
474            let _ = unsafe { source.remove_WebResourceRequested(token) };
475            unsafe {
476                protocol::remove_requested_filter(&source, &filter);
477            };
478        }))
479    }
480}