1use crate::host::{Host, Surface};
2use gpui::{
3 Action, App, Bounds, Context, EventEmitter, FocusHandle, Focusable, Global, IntoElement,
4 KeyBinding, Pixels, Render, Subscription, Task, Window, actions, div, prelude::*,
5};
6use serde::de::DeserializeOwned;
7use std::{
8 cell::{Cell, RefCell},
9 fmt,
10 rc::Rc,
11 time::Duration,
12};
13
14pub struct WebView {
32 page: Rc<Page>,
33 location: Option<String>,
34 title: String,
35 loading: bool,
36 _focus: [Subscription; 2],
37 _reports: Task<()>,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum WebViewEvent {
43 Location(String),
46 Title(String),
48 Load(LoadState),
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum LoadState {
53 Started,
54 Finished,
55}
56
57#[derive(Debug)]
59pub enum EvalError {
60 Unavailable,
62 Timeout,
64 Script,
66 Decode(serde_json::Error),
68}
69
70impl fmt::Display for EvalError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Self::Unavailable => f.write_str("no page to evaluate in"),
74 Self::Timeout => f.write_str("script timed out"),
75 Self::Script => f.write_str("script threw or returned no JSON value"),
76 Self::Decode(error) => write!(f, "script value: {error}"),
77 }
78 }
79}
80
81impl std::error::Error for EvalError {}
82
83impl WebView {
84 pub fn new(url: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
85 #[cfg(target_os = "linux")]
86 gtk_loop::start(window, cx);
87 bind_edits(cx);
88 let focus = cx.focus_handle();
89 let subscriptions = [
90 cx.on_focus(&focus, window, |this: &mut Self, _, _| {
91 this.page.take_keys()
92 }),
93 cx.on_blur(&focus, window, |this: &mut Self, _, _| {
94 this.page.give_keys()
95 }),
96 ];
97 let (reports, received) = async_channel::unbounded();
98 let task = cx.spawn_in(window, async move |this, cx| {
100 while let Ok(report) = received.recv().await {
101 if this
102 .update_in(cx, |this, window, cx| this.report(report, window, cx))
103 .is_err()
104 {
105 return;
106 }
107 }
108 });
109 Self {
110 page: Rc::new(Page::new(url.into(), focus, reports)),
111 location: None,
112 title: String::new(),
113 loading: false,
114 _focus: subscriptions,
115 _reports: task,
116 }
117 }
118
119 pub fn location(&self) -> Option<&str> {
121 self.location.as_deref()
122 }
123
124 pub fn title(&self) -> &str {
125 &self.title
126 }
127
128 pub fn is_loading(&self) -> bool {
129 self.loading
130 }
131
132 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
136 *self.page.user_agent.borrow_mut() = Some(user_agent.into());
137 self
138 }
139
140 pub fn load(&mut self, url: impl Into<String>) {
143 self.page.load(url.into());
144 }
145
146 pub fn back(&mut self) {
147 self.page.back();
148 }
149
150 pub fn forward(&mut self) {
151 self.page.forward();
152 }
153
154 pub fn reload(&mut self) {
155 self.page.reload();
156 }
157
158 pub fn eval<T: DeserializeOwned + 'static>(
162 &self,
163 script: &str,
164 timeout: Duration,
165 cx: &App,
166 ) -> Task<Result<T, EvalError>> {
167 let (answer, answered) = async_channel::bounded::<Option<String>>(2);
168 let timer = answer.clone();
169 let script = format!("JSON.stringify((\n{script}\n) ?? null)");
173 if !self.page.eval(&script, move |json| {
174 let _ = answer.try_send(Some(json));
175 }) {
176 return Task::ready(Err(EvalError::Unavailable));
177 }
178 let expired = cx.background_executor().timer(timeout);
179 cx.foreground_executor()
180 .spawn(async move {
181 expired.await;
182 let _ = timer.try_send(None);
183 })
184 .detach();
185 cx.foreground_executor().spawn(async move {
186 let json = answered
187 .recv()
188 .await
189 .ok()
190 .flatten()
191 .ok_or(EvalError::Timeout)?;
192 let json = serde_json::from_str::<Option<String>>(&json)
193 .ok()
194 .flatten()
195 .ok_or(EvalError::Script)?;
196 serde_json::from_str(&json).map_err(EvalError::Decode)
197 })
198 }
199
200 fn report(&mut self, report: Report, window: &mut Window, cx: &mut Context<Self>) {
201 match report {
202 Report::Pressed => {
203 if self.page.holds_keys() {
204 window.focus(&self.page.focus, cx);
205 }
206 }
207 Report::Moved => {
208 if let Some(url) = self.page.location() {
209 self.locate(url, cx);
210 }
211 }
212 Report::Load(state, url) => {
213 self.locate(url, cx);
214 self.loading = state == LoadState::Started;
215 cx.emit(WebViewEvent::Load(state));
216 }
217 Report::Title(title) => {
219 if title != self.title {
220 self.title = title.clone();
221 cx.emit(WebViewEvent::Title(title));
222 }
223 }
224 }
225 }
226
227 fn locate(&mut self, url: String, cx: &mut Context<Self>) {
228 if self.location.as_ref() != Some(&url) {
229 self.location = Some(url.clone());
230 cx.emit(WebViewEvent::Location(url));
231 }
232 }
233}
234
235impl EventEmitter<WebViewEvent> for WebView {}
236
237impl Focusable for WebView {
238 fn focus_handle(&self, _cx: &App) -> FocusHandle {
239 self.page.focus.clone()
240 }
241}
242
243impl Render for WebView {
244 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
245 div()
246 .size_full()
247 .key_context(KEY_CONTEXT)
248 .on_action(on_edit::<Copy>(Edit::Copy, cx))
249 .on_action(on_edit::<Cut>(Edit::Cut, cx))
250 .on_action(on_edit::<Paste>(Edit::Paste, cx))
251 .on_action(on_edit::<SelectAll>(Edit::SelectAll, cx))
252 .on_action(on_edit::<Undo>(Edit::Undo, cx))
253 .on_action(on_edit::<Redo>(Edit::Redo, cx))
254 .child(Host {
255 surface: self.page.clone(),
256 })
257 }
258}
259
260actions!(webview, [Copy, Cut, Paste, SelectAll, Undo, Redo]);
261
262const KEY_CONTEXT: &str = "WebView";
263
264#[derive(Clone, Copy)]
267#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
268enum Edit {
269 Copy,
270 Cut,
271 Paste,
272 SelectAll,
273 Undo,
274 Redo,
275}
276
277fn on_edit<A: Action>(
278 edit: Edit,
279 cx: &mut Context<WebView>,
280) -> impl Fn(&A, &mut Window, &mut App) + 'static {
281 cx.listener(move |this, _: &A, _, _| this.page.edit(edit))
282}
283
284fn bind_edits(cx: &mut App) {
286 struct Bound;
287 impl Global for Bound {}
288
289 if !cfg!(target_os = "macos") || cx.has_global::<Bound>() {
290 return;
291 }
292 cx.set_global(Bound);
293 let context = Some(KEY_CONTEXT);
294 cx.bind_keys([
295 KeyBinding::new("cmd-c", Copy, context),
296 KeyBinding::new("cmd-x", Cut, context),
297 KeyBinding::new("cmd-v", Paste, context),
298 KeyBinding::new("cmd-a", SelectAll, context),
299 KeyBinding::new("cmd-z", Undo, context),
300 KeyBinding::new("cmd-shift-z", Redo, context),
301 ]);
302}
303
304#[cfg_attr(
306 not(any(target_os = "macos", target_os = "windows", target_os = "linux")),
307 allow(dead_code)
308)]
309enum Report {
310 Pressed,
311 Moved,
313 Load(LoadState, String),
314 Title(String),
315}
316
317struct Page {
318 #[cfg_attr(
320 not(any(target_os = "macos", target_os = "windows", target_os = "linux")),
321 allow(dead_code)
322 )]
323 url: RefCell<String>,
324 #[cfg_attr(
325 not(any(target_os = "macos", target_os = "windows", target_os = "linux")),
326 allow(dead_code)
327 )]
328 user_agent: RefCell<Option<String>>,
329 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
330 view: std::cell::OnceCell<Option<wry::WebView>>,
331 #[cfg(target_os = "macos")]
332 cursor: std::cell::OnceCell<cursor::Watch>,
333 placed: Cell<Option<Bounds<Pixels>>>,
335 owner: Cell<u64>,
336 focus: FocusHandle,
337 #[cfg_attr(
338 not(any(target_os = "macos", target_os = "windows", target_os = "linux")),
339 allow(dead_code)
340 )]
341 reports: async_channel::Sender<Report>,
342 #[cfg(any(target_os = "windows", target_os = "linux"))]
344 focused: Rc<Cell<bool>>,
345}
346
347impl Page {
348 fn new(url: String, focus: FocusHandle, reports: async_channel::Sender<Report>) -> Self {
349 Self {
350 url: RefCell::new(url),
351 user_agent: RefCell::new(None),
352 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
353 view: std::cell::OnceCell::new(),
354 #[cfg(target_os = "macos")]
355 cursor: std::cell::OnceCell::new(),
356 placed: Cell::new(None),
357 owner: Cell::new(0),
358 focus,
359 reports,
360 #[cfg(any(target_os = "windows", target_os = "linux"))]
361 focused: Rc::new(Cell::new(false)),
362 }
363 }
364}
365
366impl Surface for Page {
367 fn focus(&self) -> Option<&FocusHandle> {
368 Some(&self.focus)
369 }
370
371 fn owner(&self) -> &Cell<u64> {
372 &self.owner
373 }
374
375 fn place(&self, bounds: Bounds<Pixels>, window: &Window) {
376 Page::place(self, bounds, window);
377 }
378
379 fn park(&self) {
380 Page::park(self);
381 }
382}
383
384#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
385impl Page {
386 const PARKED: f64 = -20_000.0;
390
391 const MOVED: &str = "(() => {
393 const moved = () => window.ipc.postMessage('moved');
394 for (const name of ['pushState', 'replaceState']) {
395 const original = history[name];
396 history[name] = function (...args) {
397 const result = original.apply(this, args);
398 moved();
399 return result;
400 };
401 }
402 addEventListener('popstate', moved);
403 addEventListener('hashchange', moved);
404 })();";
405
406 fn place(&self, bounds: Bounds<Pixels>, window: &Window) {
407 let view = self.view.get_or_init(|| self.build(bounds, window));
408 let Some(view) = view else { return };
409 let placed = self.placed.get();
410 if placed == Some(bounds) {
411 return;
412 }
413 let _ = view.set_bounds(rect(bounds));
414 let _ = view.set_visible(true);
415 self.placed.set(Some(bounds));
416 if placed.is_none() && self.focus.is_focused(window) {
417 self.take_keys();
418 }
419 }
420
421 fn build(&self, bounds: Bounds<Pixels>, window: &Window) -> Option<wry::WebView> {
422 let url = self.url.borrow();
423 let (ipc, loads, titles) = (
424 self.reports.clone(),
425 self.reports.clone(),
426 self.reports.clone(),
427 );
428 let builder = wry::WebViewBuilder::new()
429 .with_url(url.as_str())
430 .with_bounds(rect(bounds))
431 .with_initialization_script(Self::MOVED)
432 .with_ipc_handler(move |request| {
433 let report = match request.body().as_str() {
434 "pressed" => Report::Pressed,
435 "moved" => Report::Moved,
436 _ => return,
437 };
438 let _ = ipc.try_send(report);
439 })
440 .with_on_page_load_handler(move |event, url| {
441 let state = match event {
442 wry::PageLoadEvent::Started => LoadState::Started,
443 wry::PageLoadEvent::Finished => LoadState::Finished,
444 };
445 let _ = loads.try_send(Report::Load(state, url));
446 })
447 .with_document_title_changed_handler(move |title| {
448 let _ = titles.try_send(Report::Title(title));
449 });
450 let user_agent = self.user_agent.borrow().clone().or_else(default_user_agent);
451 let builder = match user_agent {
452 Some(user_agent) => builder.with_user_agent(user_agent),
453 None => builder,
454 };
455 #[cfg(target_os = "linux")]
456 let window = >k_loop::Parent::of(window).filter(|_| gtk::is_initialized())?;
457 let view = self
458 .configure(builder)
459 .build_as_child(window)
460 .inspect_err(|error| tracing::warn!(%error, url = %url, "webview: build"))
461 .ok()?;
462 self.attach(&view);
463 Some(view)
464 }
465
466 fn built(&self) -> Option<&wry::WebView> {
467 self.view.get()?.as_ref()
468 }
469
470 fn load(&self, url: String) {
471 match self.built() {
472 Some(view) => {
473 let _ = view.load_url(&url);
474 }
475 None => *self.url.borrow_mut() = url,
476 }
477 }
478
479 fn reload(&self) {
480 if let Some(view) = self.built() {
481 let _ = view.reload();
482 }
483 }
484
485 fn location(&self) -> Option<String> {
486 self.built()?.url().ok()
487 }
488
489 fn eval(&self, script: &str, done: impl Fn(String) + Send + 'static) -> bool {
491 self.built()
492 .is_some_and(|view| view.evaluate_script_with_callback(script, done).is_ok())
493 }
494
495 fn take_keys(&self) {
496 let Some(view) = self.built() else {
497 return;
498 };
499 if self.placed.get().is_some() && !closed(view) {
500 let _ = view.focus();
501 }
502 }
503
504 fn give_keys(&self) {
506 if let Some(view) = self.built()
507 && self.holds_keys()
508 {
509 let _ = view.focus_parent();
510 }
511 }
512
513 fn park(&self) {
514 let Some(view) = self.built() else {
515 return;
516 };
517 let Some(bounds) = self.placed.take() else {
518 return;
519 };
520 self.give_keys();
521 #[cfg(target_os = "macos")]
522 if let Some(cursor) = self.cursor.get() {
523 cursor.release();
524 }
525 let _ = view.set_visible(false);
526 if closed(view) {
527 return;
528 }
529 let _ = view.set_bounds(wry::Rect {
532 position: wry::dpi::LogicalPosition::new(Self::PARKED, Self::PARKED).into(),
533 ..rect(bounds)
534 });
535 }
536}
537
538#[cfg(target_os = "macos")]
539impl Page {
540 const PRESSED: &str = "addEventListener('mousedown', () => \
542 window.webkit.messageHandlers.ipc.postMessage('pressed'), true);";
543
544 fn configure<'a>(&self, builder: wry::WebViewBuilder<'a>) -> wry::WebViewBuilder<'a> {
545 builder.with_initialization_script_for_main_only(Self::PRESSED, false)
546 }
547
548 fn attach(&self, view: &wry::WebView) {
549 use wry::WebViewExtMacOS;
550
551 let _ = self.cursor.set(cursor::Watch::new(&view.webview()));
552 }
553
554 fn edit(&self, edit: Edit) {
557 use objc2::{MainThreadMarker, sel};
558 use objc2_app_kit::NSApplication;
559
560 if !self.holds_keys() {
561 return;
562 }
563 let Some(mtm) = MainThreadMarker::new() else {
564 return;
565 };
566 let action = match edit {
567 Edit::Copy => sel!(copy:),
568 Edit::Cut => sel!(cut:),
569 Edit::Paste => sel!(paste:),
570 Edit::SelectAll => sel!(selectAll:),
571 Edit::Undo => sel!(undo:),
572 Edit::Redo => sel!(redo:),
573 };
574 unsafe { NSApplication::sharedApplication(mtm).sendAction_to_from(action, None, None) };
577 }
578
579 fn back(&self) {
580 use wry::WebViewExtMacOS;
581
582 if let Some(view) = self.built() {
583 unsafe { view.webview().goBack() };
585 }
586 }
587
588 fn forward(&self) {
589 use wry::WebViewExtMacOS;
590
591 if let Some(view) = self.built() {
592 unsafe { view.webview().goForward() };
594 }
595 }
596
597 fn holds_keys(&self) -> bool {
599 use objc2_app_kit::NSView;
600 use wry::WebViewExtMacOS;
601
602 let Some(view) = self.built() else {
603 return false;
604 };
605 let page = view.webview();
606 let Some(window) = page.window() else {
607 return false;
608 };
609 window
610 .firstResponder()
611 .and_then(|responder| responder.downcast::<NSView>().ok())
612 .is_some_and(|responder| responder.isDescendantOf(&page))
613 }
614}
615
616#[cfg(target_os = "macos")]
619fn default_user_agent() -> Option<String> {
620 use objc2_foundation::{NSBundle, NSString};
621
622 let version = NSBundle::bundleWithPath(&NSString::from_str("/Applications/Safari.app"))
623 .and_then(|safari| {
624 safari.objectForInfoDictionaryKey(&NSString::from_str("CFBundleShortVersionString"))
625 })
626 .and_then(|version| version.downcast::<NSString>().ok())
627 .map_or_else(|| "26.0".to_owned(), |version| version.to_string());
628 Some(format!(
629 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 \
630 (KHTML, like Gecko) Version/{version} Safari/605.1.15"
631 ))
632}
633
634#[cfg(any(target_os = "windows", target_os = "linux"))]
635fn default_user_agent() -> Option<String> {
636 None
637}
638
639#[cfg(target_os = "macos")]
645mod cursor {
646 use objc2::{
647 AnyThread, DefinedClass, MainThreadMarker, MainThreadOnly, Message, define_class, msg_send,
648 rc::Retained,
649 runtime::{NSObject, NSObjectProtocol},
650 };
651 use objc2_app_kit::{NSEvent, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow};
652 use objc2_foundation::NSRect;
653 use std::cell::Cell;
654
655 pub(super) struct Watch {
656 page: Retained<NSView>,
657 area: Retained<NSTrackingArea>,
658 watcher: Retained<Watcher>,
660 }
661
662 impl Watch {
663 pub(super) fn new(page: &NSView) -> Self {
664 let mtm = MainThreadMarker::new().expect("the page is built on the main thread");
665 let watcher: Retained<Watcher> = {
666 let watcher = Watcher::alloc(mtm).set_ivars(Inside::default());
667 unsafe { msg_send![super(watcher), init] }
669 };
670 let area = unsafe {
673 NSTrackingArea::initWithRect_options_owner_userInfo(
674 NSTrackingArea::alloc(),
675 NSRect::ZERO,
676 NSTrackingAreaOptions::MouseEnteredAndExited
677 | NSTrackingAreaOptions::ActiveAlways
678 | NSTrackingAreaOptions::InVisibleRect,
679 Some(&watcher),
680 None,
681 )
682 };
683 page.addTrackingArea(&area);
684 Self {
685 page: page.retain(),
686 area,
687 watcher,
688 }
689 }
690
691 pub(super) fn release(&self) {
694 if self.watcher.ivars().0.replace(false)
695 && let Some(window) = self.page.window()
696 {
697 enable(&window);
698 }
699 }
700 }
701
702 impl Drop for Watch {
703 fn drop(&mut self) {
704 self.page.removeTrackingArea(&self.area);
705 self.release();
706 }
707 }
708
709 #[derive(Default)]
710 struct Inside(Cell<bool>);
711
712 define_class!(
713 #[unsafe(super(NSObject))]
714 #[thread_kind = MainThreadOnly]
715 #[name = "BezelWebViewCursorWatcher"]
716 #[ivars = Inside]
717 struct Watcher;
718
719 unsafe impl NSObjectProtocol for Watcher {}
720
721 impl Watcher {
722 #[unsafe(method(mouseEntered:))]
723 fn entered(&self, event: &NSEvent) {
724 self.ivars().0.set(true);
725 if let Some(window) = event.window(self.mtm()) {
726 window.disableCursorRects();
727 }
728 }
729
730 #[unsafe(method(mouseExited:))]
731 fn exited(&self, event: &NSEvent) {
732 if self.ivars().0.replace(false)
733 && let Some(window) = event.window(self.mtm())
734 {
735 enable(&window);
736 }
737 }
738 }
739 );
740
741 fn enable(window: &NSWindow) {
742 window.enableCursorRects();
743 if let Some(content) = window.contentView() {
744 window.invalidateCursorRectsForView(&content);
745 }
746 }
747}
748
749#[cfg(target_os = "macos")]
752fn closed(view: &wry::WebView) -> bool {
753 use wry::WebViewExtMacOS;
754
755 view.webview().window().is_none()
756}
757
758#[cfg(target_os = "windows")]
759impl Page {
760 fn configure<'a>(&self, builder: wry::WebViewBuilder<'a>) -> wry::WebViewBuilder<'a> {
761 builder
762 }
763
764 fn attach(&self, view: &wry::WebView) {
766 use webview2_com::FocusChangedEventHandler;
767 use wry::WebViewExtWindows;
768
769 let controller = view.controller();
770 let (got, lost) = (self.focused.clone(), self.focused.clone());
771 let pressed = self.reports.clone();
772 let mut token = 0;
773 unsafe {
775 let _ = controller.add_GotFocus(
776 &FocusChangedEventHandler::create(Box::new(move |_, _| {
777 got.set(true);
778 let _ = pressed.try_send(Report::Pressed);
779 Ok(())
780 })),
781 &mut token,
782 );
783 let _ = controller.add_LostFocus(
784 &FocusChangedEventHandler::create(Box::new(move |_, _| {
785 lost.set(false);
786 Ok(())
787 })),
788 &mut token,
789 );
790 }
791 }
792
793 fn back(&self) {
794 use wry::WebViewExtWindows;
795
796 if let Some(view) = self.built() {
797 let _ = unsafe { view.webview().GoBack() };
799 }
800 }
801
802 fn forward(&self) {
803 use wry::WebViewExtWindows;
804
805 if let Some(view) = self.built() {
806 let _ = unsafe { view.webview().GoForward() };
808 }
809 }
810
811 fn holds_keys(&self) -> bool {
812 self.focused.get()
813 }
814
815 fn edit(&self, _edit: Edit) {}
816}
817
818#[cfg(target_os = "windows")]
819fn closed(_view: &wry::WebView) -> bool {
820 false
821}
822
823#[cfg(target_os = "linux")]
824impl Page {
825 fn configure<'a>(&self, builder: wry::WebViewBuilder<'a>) -> wry::WebViewBuilder<'a> {
826 builder
827 }
828
829 fn attach(&self, view: &wry::WebView) {
831 use gtk::{glib::Propagation, prelude::WidgetExt};
832 use wry::WebViewExtUnix;
833
834 let page = view.webview();
835 let (got, lost) = (self.focused.clone(), self.focused.clone());
836 let pressed = self.reports.clone();
837 page.connect_focus_in_event(move |_, _| {
838 got.set(true);
839 let _ = pressed.try_send(Report::Pressed);
840 Propagation::Proceed
841 });
842 page.connect_focus_out_event(move |_, _| {
843 lost.set(false);
844 Propagation::Proceed
845 });
846 }
847
848 fn back(&self) {
849 use webkit2gtk::WebViewExt;
850 use wry::WebViewExtUnix;
851
852 if let Some(view) = self.built() {
853 view.webview().go_back();
854 }
855 }
856
857 fn forward(&self) {
858 use webkit2gtk::WebViewExt;
859 use wry::WebViewExtUnix;
860
861 if let Some(view) = self.built() {
862 view.webview().go_forward();
863 }
864 }
865
866 fn holds_keys(&self) -> bool {
867 self.focused.get()
868 }
869
870 fn edit(&self, _edit: Edit) {}
871}
872
873#[cfg(target_os = "linux")]
874fn closed(_view: &wry::WebView) -> bool {
875 false
876}
877
878#[cfg(target_os = "linux")]
881mod gtk_loop {
882 use gpui::{App, Window};
883 use raw_window_handle::{
884 HandleError, HasWindowHandle, RawWindowHandle, WindowHandle, XlibWindowHandle,
885 };
886 use std::{os::raw::c_ulong, time::Duration};
887
888 const PUMP: Duration = Duration::from_millis(8);
891
892 pub(super) fn start(window: &Window, cx: &mut App) {
896 if gtk::is_initialized() || Parent::of(window).is_none() {
897 return;
898 }
899 gtk::gdk::set_allowed_backends("x11");
900 if let Err(error) = gtk::init() {
901 tracing::warn!(%error, "webview: gtk");
902 return;
903 }
904 let timers = cx.background_executor().clone();
905 cx.foreground_executor()
906 .spawn(async move {
907 loop {
908 timers.timer(PUMP).await;
909 while gtk::events_pending() {
910 gtk::main_iteration_do(false);
911 }
912 }
913 })
914 .detach();
915 }
916
917 pub(super) struct Parent(XlibWindowHandle);
920
921 impl Parent {
922 pub(super) fn of(window: &Window) -> Option<Self> {
924 match HasWindowHandle::window_handle(window).ok()?.as_raw() {
925 RawWindowHandle::Xcb(handle) => Some(Self(XlibWindowHandle::new(c_ulong::from(
926 handle.window.get(),
927 )))),
928 _ => None,
929 }
930 }
931 }
932
933 impl HasWindowHandle for Parent {
934 fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
935 Ok(unsafe { WindowHandle::borrow_raw(RawWindowHandle::Xlib(self.0)) })
937 }
938 }
939}
940
941#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
942impl Page {
943 fn place(&self, bounds: Bounds<Pixels>, _window: &Window) {
944 self.placed.set(Some(bounds));
945 }
946
947 fn load(&self, url: String) {
948 *self.url.borrow_mut() = url;
949 }
950
951 fn back(&self) {}
952
953 fn forward(&self) {}
954
955 fn reload(&self) {}
956
957 fn location(&self) -> Option<String> {
958 None
959 }
960
961 fn eval(&self, _script: &str, _done: impl Fn(String) + Send + 'static) -> bool {
962 false
963 }
964
965 fn holds_keys(&self) -> bool {
966 false
967 }
968
969 fn edit(&self, _edit: Edit) {}
970
971 fn park(&self) {
972 self.placed.set(None);
973 }
974
975 fn take_keys(&self) {}
976
977 fn give_keys(&self) {}
978}
979
980#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
981fn rect(bounds: Bounds<Pixels>) -> wry::Rect {
982 wry::Rect {
983 position: wry::dpi::LogicalPosition::new(
984 f64::from(f32::from(bounds.origin.x)),
985 f64::from(f32::from(bounds.origin.y)),
986 )
987 .into(),
988 size: wry::dpi::LogicalSize::new(
989 f64::from(f32::from(bounds.size.width)),
990 f64::from(f32::from(bounds.size.height)),
991 )
992 .into(),
993 }
994}