Skip to main content

winio_ui_win32/widgets/
webview.rs

1use std::{cell::RefCell, rc::Rc};
2
3use cookie::Cookie;
4use futures_util::FutureExt;
5use webview2::{
6    COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX, COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE,
7    COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT, CreateCoreWebView2Environment, ICoreWebView2,
8    ICoreWebView2_2, ICoreWebView2Controller, ICoreWebView2Cookie, ICoreWebView2CookieList,
9    ICoreWebView2CookieManager, ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
10    ICoreWebView2CreateCoreWebView2ControllerCompletedHandler_Impl,
11    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
12    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler_Impl, ICoreWebView2Environment,
13    ICoreWebView2ExecuteScriptCompletedHandler, ICoreWebView2ExecuteScriptCompletedHandler_Impl,
14    ICoreWebView2GetCookiesCompletedHandler, ICoreWebView2GetCookiesCompletedHandler_Impl,
15    ICoreWebView2NavigationCompletedEventArgs, ICoreWebView2NavigationCompletedEventHandler,
16    ICoreWebView2NavigationCompletedEventHandler_Impl, ICoreWebView2NavigationStartingEventArgs,
17    ICoreWebView2NavigationStartingEventHandler, ICoreWebView2NavigationStartingEventHandler_Impl,
18};
19use windows::{
20    Win32::Foundation::{E_FAIL, E_INVALIDARG, HWND, RECT},
21    core::{HRESULT, HSTRING, Interface, PCWSTR, Ref, implement},
22};
23use windows_sys::Win32::{Foundation::ERROR_CANCELLED, UI::HiDpi::GetDpiForWindow};
24use winio_callback::Callback;
25use winio_handle::{AsContainer, AsWidget, BorrowedWidget};
26use winio_primitive::{Point, Rect, Size};
27use winio_ui_windows_common::CoTaskMemPtr;
28
29use crate::{Error, Result, widgets::with_u16c};
30
31#[derive(Debug)]
32pub struct WebView {
33    host: ICoreWebView2Controller,
34    view: ICoreWebView2,
35    navigating: Rc<Callback>,
36    navigated: Rc<Callback>,
37}
38
39impl WebView {
40    pub async fn new(parent: impl AsContainer) -> Result<Self> {
41        let (tx, rx) = local_sync::oneshot::channel();
42        let hwnd = parent.as_container().as_win32();
43        unsafe {
44            CreateCoreWebView2Environment(&CreateEnvHandler::create(move |env| {
45                let env = env?;
46                let env = env.ok()?;
47                env.CreateCoreWebView2Controller(
48                    HWND(hwnd),
49                    &CreateControllerHandler::create(move |host| {
50                        let host = host?;
51                        let host = host.ok()?;
52                        let view = host.CoreWebView2()?;
53                        tx.send((host.clone(), view)).ok();
54                        Ok(())
55                    }),
56                )?;
57                Ok(())
58            }))?;
59        }
60        let (host, view) = rx.await.map_err(|_| Error::from_hresult(E_FAIL))?;
61        let navigating = Rc::new(Callback::new());
62        unsafe {
63            let navigating = navigating.clone();
64            view.NavigationStarting(&NavStartingHandler::create(move |_, _| {
65                navigating.signal::<()>(());
66                Ok(())
67            }))?;
68        }
69        let navigated = Rc::new(Callback::new());
70        unsafe {
71            let navigated = navigated.clone();
72            view.NavigationCompleted(&NavCompletedHandler::create(move |_, _| {
73                navigated.signal::<()>(());
74                Ok(())
75            }))?;
76        }
77        unsafe {
78            host.SetIsVisible(true)?;
79        }
80        Ok(Self {
81            host,
82            view,
83            navigating,
84            navigated,
85        })
86    }
87
88    fn dpi(&self) -> Result<f64> {
89        unsafe {
90            let hwnd = self.host.ParentWindow()?;
91            Ok(GetDpiForWindow(hwnd.0) as f64 / 96.0)
92        }
93    }
94
95    fn rect(&self) -> Result<Rect> {
96        let rect = unsafe { self.host.Bounds() }?;
97        Ok(Rect::new(
98            Point::new(rect.left as _, rect.top as _),
99            Size::new((rect.right - rect.left) as _, (rect.bottom - rect.top) as _),
100        ) / self.dpi()?)
101    }
102
103    fn set_rect(&mut self, r: Rect) -> Result<()> {
104        let r = r * self.dpi()?;
105        unsafe {
106            self.host.SetBounds(RECT {
107                left: r.origin.x as _,
108                top: r.origin.y as _,
109                right: (r.origin.x + r.size.width) as _,
110                bottom: (r.origin.y + r.size.height) as _,
111            })?;
112        }
113        Ok(())
114    }
115
116    pub fn is_visible(&self) -> Result<bool> {
117        unsafe { Ok(self.host.IsVisible()?.as_bool()) }
118    }
119
120    pub fn set_visible(&mut self, v: bool) -> Result<()> {
121        unsafe {
122            self.host.SetIsVisible(v)?;
123            Ok(())
124        }
125    }
126
127    pub fn is_enabled(&self) -> Result<bool> {
128        Ok(true)
129    }
130
131    pub fn set_enabled(&mut self, _: bool) -> Result<()> {
132        Ok(())
133    }
134
135    pub fn loc(&self) -> Result<Point> {
136        Ok(self.rect()?.origin)
137    }
138
139    pub fn set_loc(&mut self, p: Point) -> Result<()> {
140        let mut rect = self.rect()?;
141        rect.origin = p;
142        self.set_rect(rect)
143    }
144
145    pub fn size(&self) -> Result<Size> {
146        Ok(self.rect()?.size)
147    }
148
149    pub fn set_size(&mut self, v: Size) -> Result<()> {
150        let mut rect = self.rect()?;
151        rect.size = v;
152        self.set_rect(rect)
153    }
154
155    pub fn source(&self) -> Result<String> {
156        unsafe {
157            let source = CoTaskMemPtr::new(self.view.Source()?.0);
158            source.to_string()
159        }
160    }
161
162    pub fn set_source(&mut self, s: impl AsRef<str>) -> Result<()> {
163        let s = s.as_ref();
164        if s.is_empty() {
165            return self.set_html("");
166        }
167        with_u16c(s, |s| unsafe {
168            self.view.Navigate(PCWSTR(s.as_ptr()))?;
169            Ok(())
170        })
171    }
172
173    pub fn set_html(&mut self, s: impl AsRef<str>) -> Result<()> {
174        with_u16c(s.as_ref(), |s| unsafe {
175            self.view.NavigateToString(PCWSTR(s.as_ptr()))?;
176            Ok(())
177        })
178    }
179
180    pub fn can_go_forward(&self) -> Result<bool> {
181        unsafe { Ok(self.view.CanGoForward()?.as_bool()) }
182    }
183
184    pub fn go_forward(&mut self) -> Result<()> {
185        unsafe {
186            self.view.GoForward()?;
187            Ok(())
188        }
189    }
190
191    pub fn can_go_back(&self) -> Result<bool> {
192        unsafe { Ok(self.view.CanGoBack()?.as_bool()) }
193    }
194
195    pub fn go_back(&mut self) -> Result<()> {
196        unsafe {
197            self.view.GoBack()?;
198            Ok(())
199        }
200    }
201
202    pub fn reload(&mut self) -> Result<()> {
203        unsafe {
204            self.view.Reload()?;
205            Ok(())
206        }
207    }
208
209    pub fn stop(&mut self) -> Result<()> {
210        unsafe {
211            self.view.Stop()?;
212            Ok(())
213        }
214    }
215
216    pub async fn wait_navigating(&self) {
217        self.navigating.wait().await;
218    }
219
220    pub async fn wait_navigated(&self) {
221        self.navigated.wait().await;
222    }
223
224    fn cookie_manager(&self) -> Result<ICoreWebView2CookieManager> {
225        unsafe { self.view.cast::<ICoreWebView2_2>()?.CookieManager() }
226    }
227
228    pub async fn cookies(&self) -> Result<Vec<Cookie<'static>>> {
229        let (tx, rx) = local_sync::oneshot::channel();
230        let handler = GetCookiesHandler::create(move |result| {
231            fn conv_cookies(cookies: Ref<ICoreWebView2CookieList>) -> Result<Vec<Cookie<'static>>> {
232                let list = cookies.ok()?;
233                let mut cookies = vec![];
234                for i in 0..unsafe { list.Count()? } {
235                    let cookie = unsafe { list.GetValueAtIndex(i)? };
236                    cookies.push(webview_cookie_to_cookie(&cookie)?);
237                }
238                Ok(cookies)
239            }
240            tx.send(result.map(conv_cookies)).ok();
241            Ok(())
242        });
243        unsafe { self.cookie_manager()?.GetCookies(None, &handler)? };
244        rx.await
245            .map_err(|_| Error::from_hresult(HRESULT::from_win32(ERROR_CANCELLED)))??
246    }
247
248    pub async fn set_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
249        unsafe {
250            let manager = self.cookie_manager()?;
251            manager.AddOrUpdateCookie(&cookie_to_webview_cookie(c, &manager)?)?;
252        }
253        Ok(())
254    }
255
256    pub async fn delete_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
257        unsafe {
258            let manager = self.cookie_manager()?;
259            manager.DeleteCookie(&cookie_to_webview_cookie(c, &manager)?)?;
260        }
261        Ok(())
262    }
263
264    pub fn run_javascript(
265        &mut self,
266        s: impl AsRef<str>,
267    ) -> Result<impl Future<Output = Result<String>> + 'static> {
268        let s = s.as_ref();
269        let (tx, rx) = local_sync::oneshot::channel();
270        with_u16c(s, |s| unsafe {
271            self.view.ExecuteScript(
272                PCWSTR(s.as_ptr()),
273                &ExecuteScriptHandler::create(move |result| {
274                    tx.send(result.map(|s| s.to_hstring())).ok();
275                    Ok(())
276                }),
277            )?;
278            Ok(())
279        })?;
280        Ok(rx.into_future().map(|result| {
281            result
282                .map_err(|_| Error::from_hresult(HRESULT::from_win32(ERROR_CANCELLED)))
283                .flatten()
284                .map(|s| s.to_string_lossy())
285        }))
286    }
287}
288
289impl AsWidget for WebView {
290    fn as_widget(&self) -> BorrowedWidget<'_> {
291        unimplemented!("cannot get HWND from WebView2")
292    }
293}
294
295fn cookie_to_webview_cookie(
296    c: &Cookie<'_>,
297    manager: &ICoreWebView2CookieManager,
298) -> Result<ICoreWebView2Cookie> {
299    unsafe {
300        let name = HSTRING::from(c.name());
301        let value = HSTRING::from(c.value());
302        let domain = HSTRING::from(c.domain().unwrap_or_default());
303        let path = HSTRING::from(c.path().unwrap_or_default());
304        let cookie = manager.CreateCookie(
305            PCWSTR(name.as_ptr()),
306            PCWSTR(value.as_ptr()),
307            PCWSTR(domain.as_ptr()),
308            PCWSTR(path.as_ptr()),
309        )?;
310        if let Some(expires) = c.expires() {
311            match expires {
312                cookie::Expiration::Session => cookie.SetExpires(-1.0)?,
313                cookie::Expiration::DateTime(dt) => {
314                    let timestamp = dt.unix_timestamp() as f64;
315                    cookie.SetExpires(timestamp)?;
316                }
317            }
318        }
319        if let Some(is_secure) = c.secure() {
320            cookie.SetIsSecure(is_secure)?;
321        }
322        if let Some(is_http_only) = c.http_only() {
323            cookie.SetIsHttpOnly(is_http_only)?;
324        }
325        if let Some(same_site) = c.same_site() {
326            cookie.SetSameSite(match same_site {
327                cookie::SameSite::Lax => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX,
328                cookie::SameSite::Strict => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT,
329                cookie::SameSite::None => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE,
330            })?;
331        }
332        Ok(cookie)
333    }
334}
335
336fn webview_cookie_to_cookie(c: &ICoreWebView2Cookie) -> Result<Cookie<'static>> {
337    let name = unsafe { CoTaskMemPtr::new(c.Name()?.0) };
338    let value = unsafe { CoTaskMemPtr::new(c.Value()?.0) };
339    let domain = unsafe { CoTaskMemPtr::new(c.Domain()?.0) };
340    let path = unsafe { CoTaskMemPtr::new(c.Path()?.0) };
341    let expires = unsafe { c.Expires() }?;
342    let is_secure = unsafe { c.IsSecure()?.as_bool() };
343    let is_http_only = unsafe { c.IsHttpOnly()?.as_bool() };
344    let same_site = unsafe { c.SameSite()? };
345    let is_session = unsafe { c.IsSession()?.as_bool() };
346    let cookie = Cookie::build((unsafe { name.to_string()? }, unsafe { value.to_string()? }))
347        .domain(unsafe { domain.to_string()? })
348        .path(unsafe { path.to_string()? })
349        .expires(if is_session {
350            cookie::Expiration::Session
351        } else {
352            cookie::Expiration::DateTime(
353                time::OffsetDateTime::from_unix_timestamp(expires as _)
354                    .map_err(|_| Error::from_hresult(E_INVALIDARG))?,
355            )
356        })
357        .secure(is_secure)
358        .http_only(is_http_only)
359        .same_site(match same_site {
360            COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX => cookie::SameSite::Lax,
361            COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT => cookie::SameSite::Strict,
362            COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE => cookie::SameSite::None,
363            _ => return Err(Error::from_hresult(E_INVALIDARG)),
364        })
365        .build();
366    Ok(cookie)
367}
368
369#[implement(
370    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
371    Agile = false
372)]
373struct CreateEnvHandler<F>
374where
375    F: FnOnce(Result<Ref<ICoreWebView2Environment>>) -> Result<()> + 'static,
376{
377    f: RefCell<Option<F>>,
378}
379
380impl<F> CreateEnvHandler<F>
381where
382    F: FnOnce(Result<Ref<ICoreWebView2Environment>>) -> Result<()> + 'static,
383{
384    pub fn create(f: F) -> ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler {
385        Self {
386            f: RefCell::new(Some(f)),
387        }
388        .into()
389    }
390}
391
392impl<F> ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler_Impl for CreateEnvHandler_Impl<F>
393where
394    F: FnOnce(Result<Ref<ICoreWebView2Environment>>) -> Result<()> + 'static,
395{
396    fn Invoke(
397        &self,
398        errorcode: HRESULT,
399        createdenvironment: Ref<ICoreWebView2Environment>,
400    ) -> Result<()> {
401        let f = self.f.borrow_mut().take();
402        if let Some(f) = f {
403            f(errorcode.map(|| createdenvironment))
404        } else {
405            Ok(())
406        }
407    }
408}
409
410#[implement(
411    ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
412    Agile = false
413)]
414struct CreateControllerHandler<F>
415where
416    F: FnOnce(Result<Ref<ICoreWebView2Controller>>) -> Result<()> + 'static,
417{
418    f: RefCell<Option<F>>,
419}
420
421impl<F> CreateControllerHandler<F>
422where
423    F: FnOnce(Result<Ref<ICoreWebView2Controller>>) -> Result<()> + 'static,
424{
425    pub fn create(f: F) -> ICoreWebView2CreateCoreWebView2ControllerCompletedHandler {
426        Self {
427            f: RefCell::new(Some(f)),
428        }
429        .into()
430    }
431}
432
433impl<F> ICoreWebView2CreateCoreWebView2ControllerCompletedHandler_Impl
434    for CreateControllerHandler_Impl<F>
435where
436    F: FnOnce(Result<Ref<ICoreWebView2Controller>>) -> Result<()> + 'static,
437{
438    fn Invoke(
439        &self,
440        errorcode: HRESULT,
441        createdcontroller: Ref<ICoreWebView2Controller>,
442    ) -> Result<()> {
443        let f = self.f.borrow_mut().take();
444        if let Some(f) = f {
445            f(errorcode.map(|| createdcontroller))
446        } else {
447            Ok(())
448        }
449    }
450}
451
452#[implement(ICoreWebView2NavigationStartingEventHandler, Agile = false)]
453struct NavStartingHandler<F>
454where
455    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationStartingEventArgs>) -> Result<()>
456        + 'static,
457{
458    f: F,
459}
460
461impl<F> NavStartingHandler<F>
462where
463    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationStartingEventArgs>) -> Result<()>
464        + 'static,
465{
466    pub fn create(f: F) -> ICoreWebView2NavigationStartingEventHandler {
467        Self { f }.into()
468    }
469}
470
471impl<F> ICoreWebView2NavigationStartingEventHandler_Impl for NavStartingHandler_Impl<F>
472where
473    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationStartingEventArgs>) -> Result<()>
474        + 'static,
475{
476    fn Invoke(
477        &self,
478        sender: Ref<ICoreWebView2>,
479        args: Ref<ICoreWebView2NavigationStartingEventArgs>,
480    ) -> Result<()> {
481        (self.f)(sender, args)
482    }
483}
484
485#[implement(ICoreWebView2NavigationCompletedEventHandler, Agile = false)]
486struct NavCompletedHandler<F>
487where
488    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationCompletedEventArgs>) -> Result<()>
489        + 'static,
490{
491    f: F,
492}
493
494impl<F> NavCompletedHandler<F>
495where
496    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationCompletedEventArgs>) -> Result<()>
497        + 'static,
498{
499    pub fn create(f: F) -> ICoreWebView2NavigationCompletedEventHandler {
500        Self { f }.into()
501    }
502}
503
504impl<F> ICoreWebView2NavigationCompletedEventHandler_Impl for NavCompletedHandler_Impl<F>
505where
506    F: Fn(Ref<ICoreWebView2>, Ref<ICoreWebView2NavigationCompletedEventArgs>) -> Result<()>
507        + 'static,
508{
509    fn Invoke(
510        &self,
511        sender: Ref<ICoreWebView2>,
512        args: Ref<ICoreWebView2NavigationCompletedEventArgs>,
513    ) -> Result<()> {
514        (self.f)(sender, args)
515    }
516}
517
518#[implement(ICoreWebView2GetCookiesCompletedHandler, Agile = false)]
519struct GetCookiesHandler<F>
520where
521    F: FnOnce(Result<Ref<ICoreWebView2CookieList>>) -> Result<()> + 'static,
522{
523    f: RefCell<Option<F>>,
524}
525
526impl<F> GetCookiesHandler<F>
527where
528    F: FnOnce(Result<Ref<ICoreWebView2CookieList>>) -> Result<()> + 'static,
529{
530    pub fn create(f: F) -> ICoreWebView2GetCookiesCompletedHandler {
531        Self {
532            f: RefCell::new(Some(f)),
533        }
534        .into()
535    }
536}
537
538impl<F> ICoreWebView2GetCookiesCompletedHandler_Impl for GetCookiesHandler_Impl<F>
539where
540    F: FnOnce(Result<Ref<ICoreWebView2CookieList>>) -> Result<()> + 'static,
541{
542    fn Invoke(&self, errorcode: HRESULT, cookie_list: Ref<ICoreWebView2CookieList>) -> Result<()> {
543        let f = self.f.borrow_mut().take();
544        if let Some(f) = f {
545            f(errorcode.map(|| cookie_list))
546        } else {
547            Ok(())
548        }
549    }
550}
551
552#[implement(ICoreWebView2ExecuteScriptCompletedHandler, Agile = false)]
553struct ExecuteScriptHandler<F>
554where
555    F: FnOnce(Result<PCWSTR>) -> Result<()> + 'static,
556{
557    f: RefCell<Option<F>>,
558}
559
560impl<F> ExecuteScriptHandler<F>
561where
562    F: FnOnce(Result<PCWSTR>) -> Result<()> + 'static,
563{
564    pub fn create(f: F) -> ICoreWebView2ExecuteScriptCompletedHandler {
565        Self {
566            f: RefCell::new(Some(f)),
567        }
568        .into()
569    }
570}
571
572impl<F> ICoreWebView2ExecuteScriptCompletedHandler_Impl for ExecuteScriptHandler_Impl<F>
573where
574    F: FnOnce(Result<PCWSTR>) -> Result<()> + 'static,
575{
576    fn Invoke(&self, errorcode: HRESULT, resultobjectasjson: &PCWSTR) -> Result<()> {
577        let f = self.f.borrow_mut().take();
578        if let Some(f) = f {
579            f(errorcode.map(|| *resultobjectasjson))
580        } else {
581            Ok(())
582        }
583    }
584}