Skip to main content

tauri_plugin_gpui/
webview.rs

1//! Tauri child webviews embedded in a GPUI-backed window.
2//!
3//! [`webview_frame`] is a GPUI element that reserves a rectangle in the
4//! layout and keeps a Tauri child webview (`tauri::Window::add_child`) on
5//! top of it. The webview is shown while its frame is painted and hidden in
6//! any frame that leaves it out, so switching GPUI screens needs no extra
7//! bookkeeping.
8//!
9//! The webview is a native view above the GPUI surface: GPUI content that
10//! overlaps the frame (popovers, menus, tooltips) is drawn underneath it,
11//! and clipping by scrolling ancestors is rectangular.
12
13use std::rc::{Rc, Weak};
14
15use gpui::{
16    App, Bounds, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
17    IntoElement, LayoutId, Pixels, Refineable as _, Style, StyleRefinement, Styled, Window,
18};
19use tauri::{LogicalPosition, LogicalSize, Manager as _, Rect, Webview, Wry};
20
21use crate::{platform::window::WindowInner, runtime};
22
23#[cfg(any(target_os = "linux", target_os = "freebsd"))]
24mod linux;
25
26/// Native state of one embedded webview, owned by its GPUI window.
27pub(crate) struct EmbeddedWebview {
28    webview: Webview<Wry>,
29    /// Where the latest frame places it; `None` once no frame shows it.
30    desired: Option<Bounds<Pixels>>,
31    /// What was last applied to the native webview; `None` before the first
32    /// frame, `Some(None)` while hidden.
33    applied: Option<Option<Bounds<Pixels>>>,
34    /// Live `webview_frame` element states for this webview.
35    frames: usize,
36}
37
38#[cfg(any(target_os = "linux", target_os = "freebsd"))]
39impl Drop for EmbeddedWebview {
40    fn drop(&mut self) {
41        linux::forget(self.webview.label());
42    }
43}
44
45/// GPUI element state of one `webview_frame`. GPUI keeps it while the
46/// element is in the rendered frame, including frames that reuse a cached
47/// view without running `prepaint`, and drops it in the first frame that
48/// leaves the element out: that hides the webview.
49struct FrameState {
50    window: Weak<WindowInner>,
51    label: String,
52}
53
54impl FrameState {
55    fn new(inner: &Rc<WindowInner>, webview: &Webview<Wry>) -> Self {
56        let label = webview.label().to_string();
57        inner
58            .webviews
59            .borrow_mut()
60            .entry(label.clone())
61            .or_insert_with(|| EmbeddedWebview {
62                webview: webview.clone(),
63                desired: None,
64                applied: None,
65                frames: 0,
66            })
67            .frames += 1;
68        Self {
69            window: Rc::downgrade(inner),
70            label,
71        }
72    }
73
74    fn place(&self, bounds: Option<Bounds<Pixels>>) {
75        if let Some(inner) = self.window.upgrade()
76            && let Some(embedded) = inner.webviews.borrow_mut().get_mut(&self.label)
77        {
78            embedded.desired = bounds;
79        }
80    }
81}
82
83impl Drop for FrameState {
84    fn drop(&mut self) {
85        let Some(inner) = self.window.upgrade() else {
86            return;
87        };
88        // Dropped from inside `Window::draw`, which holds no borrow of it.
89        let Ok(mut webviews) = inner.webviews.try_borrow_mut() else {
90            return;
91        };
92        if let Some(embedded) = webviews.get_mut(&self.label) {
93            embedded.frames -= 1;
94            if embedded.frames == 0 {
95                embedded.desired = None;
96            }
97        }
98    }
99}
100
101/// Creates a GPUI element that hosts `webview`, a child webview of the
102/// GPUI-backed Tauri window this element renders in.
103///
104/// The element takes the full size of its parent by default; size it like
105/// any `div`. Create the webview hidden (call `hide()` right after
106/// `add_child`) so it does not flash at its initial bounds, and render each
107/// webview in at most one frame at a time. A webview created to replace a
108/// closed one needs a label of its own: frames track webviews by label.
109pub fn webview_frame(webview: &Webview<Wry>) -> WebviewFrame {
110    WebviewFrame {
111        id: ElementId::Name(format!("webview-frame:{}", webview.label()).into()),
112        webview: webview.clone(),
113        style: StyleRefinement::default(),
114    }
115}
116
117/// Gives keyboard focus to `webview`, a child webview placed with
118/// [`webview_frame`], as clicking it would. Use it when a GPUI control opens
119/// something in the webview that expects typing, such as a search box.
120///
121/// Must be called on the main thread. On Linux the webview must be shown
122/// (painted in the last frame); elsewhere this is `Webview::set_focus`.
123pub fn focus_webview(webview: &Webview<Wry>) -> tauri::Result<()> {
124    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
125    if linux::focus(webview.label()) {
126        return Ok(());
127    }
128    webview.set_focus()
129}
130
131/// The element returned by [`webview_frame`].
132pub struct WebviewFrame {
133    id: ElementId,
134    webview: Webview<Wry>,
135    style: StyleRefinement,
136}
137
138impl Styled for WebviewFrame {
139    fn style(&mut self) -> &mut StyleRefinement {
140        &mut self.style
141    }
142}
143
144impl IntoElement for WebviewFrame {
145    type Element = Self;
146
147    fn into_element(self) -> Self::Element {
148        self
149    }
150}
151
152impl Element for WebviewFrame {
153    type RequestLayoutState = ();
154    type PrepaintState = Option<Hitbox>;
155
156    fn id(&self) -> Option<ElementId> {
157        Some(self.id.clone())
158    }
159
160    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
161        None
162    }
163
164    fn request_layout(
165        &mut self,
166        _: Option<&GlobalElementId>,
167        _: Option<&InspectorElementId>,
168        window: &mut Window,
169        cx: &mut App,
170    ) -> (LayoutId, Self::RequestLayoutState) {
171        let mut style = Style {
172            size: gpui::Size::full(),
173            ..Style::default()
174        };
175        style.refine(&self.style);
176        (window.request_layout(style, [], cx), ())
177    }
178
179    fn prepaint(
180        &mut self,
181        id: Option<&GlobalElementId>,
182        _: Option<&InspectorElementId>,
183        bounds: Bounds<Pixels>,
184        _: &mut Self::RequestLayoutState,
185        window: &mut Window,
186        _: &mut App,
187    ) -> Self::PrepaintState {
188        let visible = bounds.intersect(&window.content_mask().bounds);
189        let visible = (visible.size.width > Pixels::ZERO && visible.size.height > Pixels::ZERO)
190            .then_some(visible);
191        let id = id.expect("webview_frame has an element id");
192        let placed = window.with_element_state::<Option<FrameState>, _>(id, |state, _| {
193            let state = state.flatten().or_else(|| {
194                // Looked up once per element; later frames reuse the state.
195                let window = self.webview.window();
196                match runtime::with(|runtime| Ok(runtime.surface(window.label()))) {
197                    Ok(Some(inner)) => Some(FrameState::new(&inner, &self.webview)),
198                    _ => {
199                        log::warn!(
200                            "webview_frame: `{}` is not a child of a GPUI-backed window",
201                            self.webview.label()
202                        );
203                        None
204                    }
205                }
206            });
207            if let Some(state) = &state {
208                state.place(visible);
209            }
210            (state.is_some(), state)
211        });
212        // The native view receives input over this area; keep GPUI content
213        // underneath from reacting to it too.
214        visible
215            .filter(|_| placed)
216            .map(|visible| window.insert_hitbox(visible, HitboxBehavior::BlockMouse))
217    }
218
219    fn paint(
220        &mut self,
221        _: Option<&GlobalElementId>,
222        _: Option<&InspectorElementId>,
223        _: Bounds<Pixels>,
224        _: &mut Self::RequestLayoutState,
225        hitbox: &mut Self::PrepaintState,
226        window: &mut Window,
227        _: &mut App,
228    ) {
229        if hitbox.is_none() {
230            return;
231        }
232        // Clicks on the webview go to the native view, so any click GPUI
233        // sees is outside it and takes keyboard focus back.
234        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
235        {
236            let label = self.webview.label().to_string();
237            window.on_mouse_event(move |_: &gpui::MouseDownEvent, phase, _, _| {
238                if phase == gpui::DispatchPhase::Bubble {
239                    linux::focus_parent(&label);
240                }
241            });
242        }
243        #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
244        let _ = window;
245    }
246}
247
248/// Applies the frame GPUI just drew to the native webviews: moves painted
249/// ones to their new bounds and hides the ones the frame left out.
250///
251/// Runs from `PlatformWindow::draw`, inside a GPUI update, so the Tauri calls
252/// are deferred until the update returns.
253pub(crate) fn sync(inner: &WindowInner) {
254    let mut changes = Vec::new();
255    let mut webviews = inner.webviews.borrow_mut();
256    for embedded in webviews.values_mut() {
257        let painted = embedded.desired;
258        if embedded.applied != Some(painted) {
259            let was_shown = matches!(embedded.applied, Some(Some(_)));
260            embedded.applied = Some(painted);
261            changes.push((embedded.webview.clone(), painted, was_shown));
262        }
263    }
264    if changes.is_empty() {
265        return;
266    }
267    // Forget hidden webviews that no frame shows and Tauri has closed.
268    webviews.retain(|_, embedded| {
269        embedded.frames > 0
270            || embedded.applied != Some(None)
271            || embedded
272                .webview
273                .get_webview(embedded.webview.label())
274                .is_some()
275    });
276    drop(webviews);
277    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
278    let x11_parent = crate::platform::x11::xid(inner.raw.get().window);
279    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
280    let toplevel = inner.x11.as_ref().map(|x11| x11.toplevel_xid);
281    (inner.defer)(Box::new(move || {
282        for (webview, painted, was_shown) in changes {
283            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
284            if let Some(parent) = x11_parent {
285                linux::apply(&webview, parent, toplevel, painted);
286                continue;
287            }
288            let result = match painted {
289                Some(bounds) => webview
290                    .set_bounds(rect(bounds))
291                    .and_then(|()| if was_shown { Ok(()) } else { webview.show() }),
292                None => webview.hide(),
293            };
294            if let Err(error) = result {
295                log::warn!("failed to update webview `{}`: {error}", webview.label());
296            }
297        }
298    }));
299}
300
301/// Makes the next frame re-apply every webview, e.g. after GPUI's native
302/// surface changed.
303#[cfg(any(target_os = "linux", target_os = "freebsd"))]
304pub(crate) fn invalidate(inner: &WindowInner) {
305    for embedded in inner.webviews.borrow_mut().values_mut() {
306        embedded.applied = None;
307    }
308}
309
310fn rect(bounds: Bounds<Pixels>) -> Rect {
311    Rect {
312        position: LogicalPosition::new(f32::from(bounds.origin.x), f32::from(bounds.origin.y))
313            .into(),
314        size: LogicalSize::new(f32::from(bounds.size.width), f32::from(bounds.size.height)).into(),
315    }
316}