1use 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
26pub(crate) struct EmbeddedWebview {
28 webview: Webview<Wry>,
29 desired: Option<Bounds<Pixels>>,
31 applied: Option<Option<Bounds<Pixels>>>,
34 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
45struct 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 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
101pub 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
117pub 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
131pub 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 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 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 #[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
248pub(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 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#[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}