winio_ui_app_kit/widgets/
webview.rs1use 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}