Skip to main content

winio_ui_winui/widgets/
webview.rs

1use std::rc::Rc;
2
3use cookie::Cookie;
4use futures_util::TryFutureExt;
5use inherit_methods_macro::inherit_methods;
6use send_wrapper::SendWrapper;
7use windows::{
8    Foundation::{TypedEventHandler, Uri},
9    Win32::Foundation::E_INVALIDARG,
10    core::{HSTRING, Interface, h},
11};
12use winio_callback::Callback;
13use winio_handle::AsContainer;
14use winio_primitive::{Point, Size};
15use winui3::Microsoft::{
16    UI::Xaml::Controls as MUXC,
17    Web::WebView2::Core::{
18        CoreWebView2Cookie, CoreWebView2CookieManager, CoreWebView2CookieSameSiteKind,
19    },
20};
21
22use crate::{Error, GlobalRuntime, Result, Widget};
23
24#[derive(Debug)]
25pub struct WebView {
26    on_navigating: SendWrapper<Rc<Callback>>,
27    on_navigated: SendWrapper<Rc<Callback>>,
28    handle: Widget,
29    view: MUXC::WebView2,
30}
31
32#[inherit_methods(from = "self.handle")]
33impl WebView {
34    pub async fn new(parent: impl AsContainer) -> Result<Self> {
35        #[cfg(feature = "webview-system")]
36        {
37            fn add_webview2sdk_path() {
38                use std::path::PathBuf;
39
40                use windows::{
41                    Win32::{
42                        System::LibraryLoader::{
43                            AddDllDirectory, LOAD_LIBRARY_SEARCH_SYSTEM32,
44                            LOAD_LIBRARY_SEARCH_USER_DIRS, SetDefaultDllDirectories,
45                        },
46                        UI::Shell::{CSIDL_WINDOWS, SHGetSpecialFolderPathW},
47                    },
48                    core::PCWSTR,
49                };
50
51                unsafe {
52                    SetDefaultDllDirectories(
53                        LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32,
54                    )
55                    .ok();
56
57                    let mut buffer = [0u16; 260];
58                    if SHGetSpecialFolderPathW(None, &mut buffer, CSIDL_WINDOWS as _, false)
59                        .ok()
60                        .is_ok()
61                    {
62                        let windir =
63                            widestring::U16CStr::from_ptr_str(buffer.as_ptr()).to_os_string();
64                        let dlldir = PathBuf::from(windir).join(r"SystemApps\Shared\WebView2SDK");
65
66                        if let Ok(dlldir) = widestring::U16CString::from_os_str(&dlldir) {
67                            AddDllDirectory(PCWSTR(dlldir.as_ptr()));
68                        }
69                    }
70                }
71            }
72
73            use std::sync::Once;
74
75            static ADD_PATH: Once = Once::new();
76
77            ADD_PATH.call_once(add_webview2sdk_path);
78        }
79        let view = MUXC::WebView2::new()?;
80        view.EnsureCoreWebView2Async()?.await?;
81        let on_navigating = SendWrapper::new(Rc::new(Callback::new()));
82        {
83            let on_navigating = on_navigating.clone();
84            view.NavigationStarting(&TypedEventHandler::new(move |_, _| {
85                on_navigating.signal::<GlobalRuntime>(());
86                Ok(())
87            }))?;
88        }
89        let on_navigated = SendWrapper::new(Rc::new(Callback::new()));
90        {
91            let on_navigated = on_navigated.clone();
92            view.NavigationCompleted(&TypedEventHandler::new(move |_, _| {
93                on_navigated.signal::<GlobalRuntime>(());
94                Ok(())
95            }))?;
96        }
97        Ok(Self {
98            on_navigating,
99            on_navigated,
100            handle: Widget::new(parent, view.cast()?)?,
101            view,
102        })
103    }
104
105    pub fn is_visible(&self) -> Result<bool>;
106
107    pub fn set_visible(&mut self, v: bool) -> Result<()>;
108
109    pub fn is_enabled(&self) -> Result<bool>;
110
111    pub fn set_enabled(&mut self, v: bool) -> Result<()>;
112
113    pub fn loc(&self) -> Result<Point>;
114
115    pub fn set_loc(&mut self, v: Point) -> Result<()>;
116
117    pub fn size(&self) -> Result<Size>;
118
119    pub fn set_size(&mut self, v: Size) -> Result<()>;
120
121    pub fn source(&self) -> Result<String> {
122        Ok(self.view.Source()?.ToString()?.to_string_lossy())
123    }
124
125    pub fn set_source(&mut self, s: impl AsRef<str>) -> Result<()> {
126        let s = s.as_ref();
127        if s.is_empty() {
128            return self.set_html("");
129        }
130        self.view.SetSource(&Uri::CreateUri(&HSTRING::from(s))?)?;
131        Ok(())
132    }
133
134    pub fn set_html(&mut self, s: impl AsRef<str>) -> Result<()> {
135        self.view.NavigateToString(&HSTRING::from(s.as_ref()))?;
136        Ok(())
137    }
138
139    pub fn can_go_forward(&self) -> Result<bool> {
140        self.view.CanGoForward()
141    }
142
143    pub fn go_forward(&mut self) -> Result<()> {
144        self.view.GoForward()?;
145        Ok(())
146    }
147
148    pub fn can_go_back(&self) -> Result<bool> {
149        self.view.CanGoBack()
150    }
151
152    pub fn go_back(&mut self) -> Result<()> {
153        self.view.GoBack()?;
154        Ok(())
155    }
156
157    pub fn reload(&mut self) -> Result<()> {
158        self.view.Reload()?;
159        Ok(())
160    }
161
162    pub fn stop(&mut self) -> Result<()> {
163        self.view.CoreWebView2()?.Stop()?;
164        Ok(())
165    }
166
167    pub async fn wait_navigating(&self) {
168        self.on_navigating.wait().await;
169    }
170
171    pub async fn wait_navigated(&self) {
172        self.on_navigated.wait().await;
173    }
174
175    fn cookie_manager(&self) -> Result<CoreWebView2CookieManager> {
176        self.view.CoreWebView2()?.CookieManager()
177    }
178
179    pub async fn cookies(&self) -> Result<Vec<Cookie<'static>>> {
180        let cookies = self.cookie_manager()?.GetCookiesAsync(h!(""))?.await?;
181        let mut result = vec![];
182        for i in 0..cookies.Size()? {
183            let cookie = cookies.GetAt(i)?;
184            result.push(webview_cookie_to_cookie(&cookie)?);
185        }
186        Ok(result)
187    }
188
189    pub async fn set_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
190        let manager = self.cookie_manager()?;
191        manager.AddOrUpdateCookie(&cookie_to_webview_cookie(c, &manager)?)?;
192        Ok(())
193    }
194
195    pub async fn delete_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
196        let manager = self.cookie_manager()?;
197        manager.DeleteCookie(&cookie_to_webview_cookie(c, &manager)?)?;
198        Ok(())
199    }
200
201    pub fn run_javascript(
202        &mut self,
203        s: impl AsRef<str>,
204    ) -> Result<impl Future<Output = Result<String>> + 'static> {
205        self.view
206            .ExecuteScriptAsync(&HSTRING::from(s.as_ref()))
207            .map(|fut| fut.into_future().map_ok(|result| result.to_string_lossy()))
208    }
209}
210
211winio_handle::impl_as_widget!(WebView, handle);
212
213fn cookie_to_webview_cookie(
214    c: &Cookie<'_>,
215    manager: &CoreWebView2CookieManager,
216) -> Result<CoreWebView2Cookie> {
217    let cookie = manager.CreateCookie(
218        &HSTRING::from(c.name()),
219        &HSTRING::from(c.value()),
220        &HSTRING::from(c.domain().unwrap_or_default()),
221        &HSTRING::from(c.path().unwrap_or_default()),
222    )?;
223    if let Some(expires) = c.expires() {
224        match expires {
225            cookie::Expiration::Session => cookie.SetExpires(-1.0)?,
226            cookie::Expiration::DateTime(dt) => {
227                let timestamp = dt.unix_timestamp() as f64;
228                cookie.SetExpires(timestamp)?;
229            }
230        }
231    }
232    if let Some(is_secure) = c.secure() {
233        cookie.SetIsSecure(is_secure)?;
234    }
235    if let Some(is_http_only) = c.http_only() {
236        cookie.SetIsHttpOnly(is_http_only)?;
237    }
238    if let Some(same_site) = c.same_site() {
239        cookie.SetSameSite(match same_site {
240            cookie::SameSite::Lax => CoreWebView2CookieSameSiteKind::Lax,
241            cookie::SameSite::Strict => CoreWebView2CookieSameSiteKind::Strict,
242            cookie::SameSite::None => CoreWebView2CookieSameSiteKind::None,
243        })?;
244    }
245    Ok(cookie)
246}
247
248fn webview_cookie_to_cookie(c: &CoreWebView2Cookie) -> Result<Cookie<'static>> {
249    let name = c.Name()?.to_string_lossy();
250    let value = c.Value()?.to_string_lossy();
251    let domain = c.Domain()?.to_string_lossy();
252    let path = c.Path()?.to_string_lossy();
253    let expires = c.Expires()?;
254    let is_secure = c.IsSecure()?;
255    let is_http_only = c.IsHttpOnly()?;
256    let same_site = c.SameSite()?;
257    let is_session = c.IsSession()?;
258    let cookie = Cookie::build((name, value))
259        .domain(domain)
260        .path(path)
261        .expires(if is_session {
262            cookie::Expiration::Session
263        } else {
264            cookie::Expiration::DateTime(
265                time::OffsetDateTime::from_unix_timestamp(expires as _)
266                    .map_err(|_| Error::from_hresult(E_INVALIDARG))?,
267            )
268        })
269        .secure(is_secure)
270        .http_only(is_http_only)
271        .same_site(match same_site {
272            CoreWebView2CookieSameSiteKind::Lax => cookie::SameSite::Lax,
273            CoreWebView2CookieSameSiteKind::Strict => cookie::SameSite::Strict,
274            CoreWebView2CookieSameSiteKind::None => cookie::SameSite::None,
275            _ => return Err(Error::from_hresult(E_INVALIDARG)),
276        })
277        .build();
278    Ok(cookie)
279}