Skip to main content

winio_ui_app_kit/widgets/
webview.rs

1use std::{
2    cell::{Cell, RefCell},
3    ptr::NonNull,
4    rc::Rc,
5};
6
7use cookie::Cookie;
8use futures_util::FutureExt;
9use inherit_methods_macro::inherit_methods;
10use objc2::{
11    AnyThread, DeclaredClass, MainThreadOnly, Message, define_class, msg_send,
12    rc::{Allocated, Retained},
13    runtime::{AnyObject, Bool, ProtocolObject},
14};
15use objc2_app_kit::{NSAlert, NSAlertFirstButtonReturn, NSTextField, NSWindow};
16use objc2_foundation::{
17    MainThreadMarker, NSArray, NSError, NSHTTPCookie, NSJSONSerialization, NSJSONWritingOptions,
18    NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, NSURL, NSURLRequest,
19    NSUTF8StringEncoding, ns_string,
20};
21use objc2_web_kit::{
22    WKFrameInfo, WKNavigation, WKNavigationDelegate, WKUIDelegate, WKWebView,
23    WKWebViewConfiguration,
24};
25use winio_callback::Callback;
26use winio_handle::AsContainer;
27use winio_primitive::{Point, Size};
28use winio_ui_apple_common::{cookie_from_ns, cookie_to_ns};
29
30use crate::{Error, GlobalRuntime, Result, Widget, catch, from_nsstring};
31
32#[derive(Debug)]
33pub struct WebView {
34    handle: Widget,
35    view: Retained<WKWebView>,
36    config: Retained<WKWebViewConfiguration>,
37    delegate: Retained<WebViewDelegate>,
38}
39
40#[inherit_methods(from = "self.handle")]
41impl WebView {
42    pub async fn new(parent: impl AsContainer) -> Result<Self> {
43        let parent = parent.as_container();
44        let parent_view = parent.as_app_kit();
45        let mtm = parent_view.mtm();
46
47        catch(|| unsafe {
48            let frame = parent_view.frame();
49            let config = WKWebViewConfiguration::new(mtm);
50            let view =
51                WKWebView::initWithFrame_configuration(WKWebView::alloc(mtm), frame, &config);
52            let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
53
54            let delegate = WebViewDelegate::new(mtm);
55            delegate.ivars().parent_window.replace(view.window());
56
57            let del_obj = ProtocolObject::from_ref(&*delegate);
58            view.setNavigationDelegate(Some(del_obj));
59            let del_obj = ProtocolObject::from_ref(&*delegate);
60            view.setUIDelegate(Some(del_obj));
61
62            Ok(Self {
63                handle,
64                view,
65                config,
66                delegate,
67            })
68        })
69        .flatten()
70    }
71
72    pub fn is_visible(&self) -> Result<bool>;
73
74    pub fn set_visible(&mut self, v: bool) -> Result<()>;
75
76    pub fn is_enabled(&self) -> Result<bool> {
77        Ok(true)
78    }
79
80    pub fn set_enabled(&mut self, _: bool) -> Result<()> {
81        Ok(())
82    }
83
84    pub fn loc(&self) -> Result<Point>;
85
86    pub fn set_loc(&mut self, p: Point) -> Result<()>;
87
88    pub fn size(&self) -> Result<Size>;
89
90    pub fn set_size(&mut self, v: Size) -> Result<()>;
91
92    pub fn source(&self) -> Result<String> {
93        catch(|| unsafe {
94            self.view
95                .URL()
96                .and_then(|url| url.absoluteString())
97                .map(|s| from_nsstring(&s))
98                .unwrap_or_default()
99        })
100    }
101
102    pub fn set_source(&mut self, s: impl AsRef<str>) -> Result<()> {
103        let s = s.as_ref();
104        if s.is_empty() {
105            return self.set_html("");
106        }
107
108        catch(|| {
109            let url = NSURL::URLWithString(&NSString::from_str(s)).ok_or(Error::NullPointer)?;
110            let req = NSURLRequest::requestWithURL(&url);
111            unsafe { self.view.loadRequest(&req) };
112            Ok(())
113        })
114        .flatten()
115    }
116
117    pub fn set_html(&mut self, html: impl AsRef<str>) -> Result<()> {
118        catch(|| unsafe {
119            self.view
120                .loadHTMLString_baseURL(&NSString::from_str(html.as_ref()), None);
121        })
122    }
123
124    pub fn can_go_forward(&self) -> Result<bool> {
125        catch(|| unsafe { self.view.canGoForward() })
126    }
127
128    pub fn go_forward(&mut self) -> Result<()> {
129        catch(|| unsafe {
130            self.view.goForward();
131        })
132    }
133
134    pub fn can_go_back(&self) -> Result<bool> {
135        catch(|| unsafe { self.view.canGoBack() })
136    }
137
138    pub fn go_back(&mut self) -> Result<()> {
139        catch(|| unsafe {
140            self.view.goBack();
141        })
142    }
143
144    pub fn reload(&mut self) -> Result<()> {
145        catch(|| unsafe {
146            self.view.reload();
147        })
148    }
149
150    pub fn stop(&mut self) -> Result<()> {
151        catch(|| unsafe {
152            self.view.stopLoading();
153        })
154    }
155
156    pub async fn wait_navigating(&self) {
157        self.delegate.ivars().navigating.wait().await
158    }
159
160    pub async fn wait_navigated(&self) {
161        self.delegate.ivars().navigated.wait().await
162    }
163
164    pub async fn cookies(&self) -> Result<Vec<Cookie<'static>>> {
165        let rx = catch(|| {
166            let (tx, rx) = local_sync::oneshot::channel();
167            let tx = Rc::new(Cell::new(Some(tx)));
168            let handler = move |cookies: NonNull<NSArray<NSHTTPCookie>>| {
169                if let Some(tx) = tx.take() {
170                    tx.send(unsafe { cookies.as_ref() }.retain()).ok();
171                }
172            };
173            let block = block2::StackBlock::new(handler);
174            unsafe {
175                self.config
176                    .websiteDataStore()
177                    .httpCookieStore()
178                    .getAllCookies(&block);
179            }
180            rx
181        })?;
182        let array = rx.await?;
183        catch(|| {
184            let mut cookies = vec![];
185            for cookie in array {
186                cookies.push(cookie_from_ns(&cookie)?);
187            }
188            Ok(cookies)
189        })
190        .flatten()
191    }
192
193    pub async fn set_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
194        let rx = catch(|| {
195            let (tx, rx) = local_sync::oneshot::channel();
196            let tx = Rc::new(Cell::new(Some(tx)));
197            let handler = move || {
198                if let Some(tx) = tx.take() {
199                    tx.send(()).ok();
200                }
201            };
202            let block = block2::StackBlock::new(handler);
203            let ns_cookie = cookie_to_ns(c)?;
204            unsafe {
205                self.config
206                    .websiteDataStore()
207                    .httpCookieStore()
208                    .setCookie_completionHandler(&ns_cookie, Some(&block));
209            }
210            Ok(rx)
211        })
212        .flatten()?;
213        rx.await?;
214        Ok(())
215    }
216
217    pub async fn delete_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
218        let rx = catch(|| {
219            let (tx, rx) = local_sync::oneshot::channel();
220            let tx = Rc::new(Cell::new(Some(tx)));
221            let handler = move || {
222                if let Some(tx) = tx.take() {
223                    tx.send(()).ok();
224                }
225            };
226            let block = block2::StackBlock::new(handler);
227            let ns_cookie = cookie_to_ns(c)?;
228            unsafe {
229                self.config
230                    .websiteDataStore()
231                    .httpCookieStore()
232                    .deleteCookie_completionHandler(&ns_cookie, Some(&block));
233            }
234            Ok(rx)
235        })
236        .flatten()?;
237        rx.await?;
238        Ok(())
239    }
240
241    pub fn run_javascript(
242        &mut self,
243        js: impl AsRef<str>,
244    ) -> Result<impl Future<Output = Result<String>> + 'static> {
245        let rx = catch(|| unsafe {
246            let (tx, rx) = local_sync::oneshot::channel();
247            let tx = Rc::new(Cell::new(Some(tx)));
248            let handler = move |result: *mut AnyObject, error: *mut NSError| {
249                let res = if error.is_null() {
250                    Ok(if result.is_null() {
251                        None
252                    } else {
253                        Some((&*result).retain())
254                    })
255                } else {
256                    Err(Error::NS(Some((&*error).retain())))
257                };
258                if let Some(tx) = tx.take() {
259                    tx.send(res).ok();
260                }
261            };
262            let block = block2::StackBlock::new(handler);
263            self.view.evaluateJavaScript_completionHandler(
264                &NSString::from_str(js.as_ref()),
265                Some(&block),
266            );
267            Ok(rx)
268        })
269        .flatten()?;
270        Ok(rx.map(|res| {
271            let Some(result) = res?? else {
272                return Ok(String::new());
273            };
274            catch(|| {
275                let data = unsafe {
276                    NSJSONSerialization::dataWithJSONObject_options_error(
277                        &result,
278                        NSJSONWritingOptions(0),
279                    )?
280                };
281                let data =
282                    NSString::initWithData_encoding(NSString::alloc(), &data, NSUTF8StringEncoding);
283                data.map(|s| from_nsstring(&s)).ok_or(Error::NullPointer)
284            })
285            .flatten()
286        }))
287    }
288}
289
290winio_handle::impl_as_widget!(WebView, handle);
291
292#[derive(Debug, Default)]
293struct WebViewDelegateIvars {
294    navigating: Callback,
295    navigated: Callback,
296    parent_window: RefCell<Option<Retained<NSWindow>>>,
297}
298
299define_class! {
300    #[unsafe(super(NSObject))]
301    #[name = "WinioWebViewDelegate"]
302    #[ivars = WebViewDelegateIvars]
303    #[thread_kind = MainThreadOnly]
304    #[derive(Debug)]
305    struct WebViewDelegate;
306
307    #[allow(non_snake_case)]
308    impl WebViewDelegate {
309        #[unsafe(method_id(init))]
310        fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
311            let this = this.set_ivars(WebViewDelegateIvars::default());
312            unsafe { msg_send![super(this), init] }
313        }
314    }
315
316    unsafe impl NSObjectProtocol for WebViewDelegate {}
317
318    #[allow(non_snake_case)]
319    unsafe impl WKNavigationDelegate for WebViewDelegate {
320        #[unsafe(method(webView:didCommitNavigation:))]
321        unsafe fn webView_didCommitNavigation(
322            &self,
323            _web_view: &WKWebView,
324            _navigation: Option<&WKNavigation>,
325        ) {
326            self.ivars().navigating.signal::<GlobalRuntime>(());
327        }
328
329        #[unsafe(method(webView:didFinishNavigation:))]
330        unsafe fn webView_didFinishNavigation(
331            &self,
332            _web_view: &WKWebView,
333            _navigation: Option<&WKNavigation>,
334        ) {
335            self.ivars().navigated.signal::<GlobalRuntime>(());
336        }
337    }
338
339    #[allow(non_snake_case)]
340    unsafe impl WKUIDelegate for WebViewDelegate {
341        #[unsafe(method(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:))]
342        unsafe fn webView_runJavaScriptAlertPanelWithMessage_initiatedByFrame_completionHandler(
343            &self,
344            web_view: &WKWebView,
345            message: &NSString,
346            frame: &WKFrameInfo,
347            completion_handler: &block2::DynBlock<dyn Fn()>,
348        ) {
349            let alert = NSAlert::new(self.mtm());
350            alert.setMessageText(message);
351            alert.addButtonWithTitle(ns_string!("OK"));
352            if let Some(window) = self.ivars().parent_window.borrow().as_ref() {
353                let handler = completion_handler.copy();
354                let block = block2::RcBlock::new(move |_| {
355                    handler.call(());
356                });
357                alert.beginSheetModalForWindow_completionHandler(window, Some(&block));
358            } else {
359                alert.runModal();
360                completion_handler.call(());
361            }
362        }
363
364        #[unsafe(method(webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:))]
365        unsafe fn webView_runJavaScriptConfirmPanelWithMessage_initiatedByFrame_completionHandler(
366            &self,
367            web_view: &WKWebView,
368            message: &NSString,
369            frame: &WKFrameInfo,
370            completion_handler: &block2::DynBlock<dyn Fn(Bool)>,
371        ) {
372            let alert = NSAlert::new(self.mtm());
373            alert.setMessageText(message);
374            alert.addButtonWithTitle(ns_string!("OK"));
375            alert.addButtonWithTitle(ns_string!("Cancel"));
376            if let Some(window) = self.ivars().parent_window.borrow().as_ref() {
377                let handler = completion_handler.copy();
378                let block = block2::RcBlock::new(move |return_code| {
379                    handler.call((Bool::new(return_code == NSAlertFirstButtonReturn),));
380                });
381                alert.beginSheetModalForWindow_completionHandler(window, Some(&block));
382            } else {
383                let return_code = alert.runModal();
384                completion_handler.call((Bool::new(return_code == NSAlertFirstButtonReturn),));
385            }
386        }
387
388        #[unsafe(method(webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:))]
389        unsafe fn webView_runJavaScriptTextInputPanelWithPrompt_defaultText_initiatedByFrame_completionHandler(
390            &self,
391            web_view: &WKWebView,
392            prompt: &NSString,
393            default_text: Option<&NSString>,
394            frame: &WKFrameInfo,
395            completion_handler: &block2::DynBlock<dyn Fn(*mut NSString)>,
396        ) {
397            let alert = NSAlert::new(self.mtm());
398            alert.setMessageText(prompt);
399            alert.addButtonWithTitle(ns_string!("OK"));
400            alert.addButtonWithTitle(ns_string!("Cancel"));
401
402            let input = NSTextField::initWithFrame(NSTextField::alloc(self.mtm()), NSRect::new(NSPoint::ZERO, NSSize::new(200.0, 24.0)));
403            if let Some(default_text) = default_text {
404                input.setStringValue(default_text);
405            }
406            alert.setAccessoryView(Some(&input));
407
408            if let Some(window) = self.ivars().parent_window.borrow().as_ref() {
409                let handler = completion_handler.copy();
410                let block = block2::RcBlock::new(move |return_code| {
411                    let result = if return_code == NSAlertFirstButtonReturn {
412                        Retained::into_raw(input.stringValue())
413                    } else {
414                        std::ptr::null_mut()
415                    };
416                    handler.call((result,));
417                });
418                alert.beginSheetModalForWindow_completionHandler(window, Some(&block));
419            } else {
420                let return_code = alert.runModal();
421                let result = if return_code == NSAlertFirstButtonReturn {
422                    Retained::into_raw(input.stringValue())
423                } else {
424                    std::ptr::null_mut()
425                };
426                completion_handler.call((result,));
427            }
428        }
429    }
430}
431
432impl WebViewDelegate {
433    pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
434        unsafe { msg_send![mtm.alloc::<Self>(), init] }
435    }
436}