1extern crate gpui_kit as gpui;
2
3#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
4mod composition;
5#[cfg(target_os = "linux")]
6mod linux;
7#[cfg(target_os = "macos")]
8mod macos;
9
10#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
11use composition::NativeWebViewSurface;
12#[cfg(all(feature = "gpui-fast", target_os = "linux"))]
13use linux::NativeWebViewSurface;
14
15use std::{cell::Cell, ops::Deref, rc::Rc};
16
17use wry::Rect;
18
19use gpui::{
20 App, Bounds, ContentMask, DismissEvent, Element, ElementId, Entity, EventEmitter, FocusHandle,
21 Focusable, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, MouseDownEvent,
22 ParentElement as _, Pixels, Render, Size, Style, Styled as _, Window, canvas, div,
23};
24
25const _: () = {
28 assert!(
29 !gpui::GPUI_FAST || cfg!(feature = "gpui-fast"),
30 "gpui-kit is built with `gpui-fast`, but gpui-webview is not: enable the \
31 `gpui-webview/gpui-fast` feature so the WebView uses GPUI Fast's window composition"
32 );
33 assert!(
34 gpui::GPUI_FAST || !cfg!(feature = "gpui-fast"),
35 "gpui-webview is built with `gpui-fast`, but gpui-kit is not: enable the \
36 `gpui-kit/gpui-fast` feature, which selects the GPUI backend"
37 );
38};
39
40#[derive(Clone)]
46pub struct WebViewHandle(Rc<wry::WebView>);
47
48impl WebViewHandle {
49 pub fn raw(&self) -> &wry::WebView {
51 &self.0
52 }
53}
54
55pub struct WebView {
63 focus_handle: FocusHandle,
64 webview: Rc<wry::WebView>,
65 visible: Cell<bool>,
66 bounds: Bounds<Pixels>,
67 zoom: Cell<f64>,
69 geometry_failed: Cell<bool>,
71 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
72 composition: Option<NativeWebViewSurface>,
73 #[cfg(target_os = "linux")]
75 scale_factor: Cell<f32>,
76 #[cfg(target_os = "linux")]
78 origin: Rc<Cell<gpui::Point<Pixels>>>,
79}
80
81impl Drop for WebView {
82 fn drop(&mut self) {
83 self.hide();
84 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
85 if let Some(surface) = self.composition.take() {
86 surface.release(self.webview.clone());
87 }
88 }
89}
90
91impl WebView {
92 pub fn new(webview: wry::WebView, window: &mut Window, cx: &mut App) -> Self {
94 let _ = webview.set_bounds(Rect::default());
95
96 #[cfg(all(feature = "gpui-fast", target_os = "macos"))]
97 let composition = match NativeWebViewSurface::new(&webview, window, cx) {
98 Ok(surface) => Some(surface),
99 Err(error) => {
100 log::warn!("WebView composition unavailable: {error:#}");
101 None
102 }
103 };
104 #[cfg(target_os = "linux")]
105 let origin = Rc::new(Cell::new(gpui::Point::default()));
106 #[cfg(target_os = "linux")]
107 linux::forward_mouse_down(&webview, origin.clone(), window, cx);
108 #[cfg(target_os = "linux")]
109 linux::match_scale_factor(&webview, window.scale_factor(), 1.0);
110 #[cfg(not(any(all(feature = "gpui-fast", target_os = "macos"), target_os = "linux")))]
111 let _ = window;
112
113 Self {
114 focus_handle: cx.focus_handle(),
115 visible: Cell::new(true),
116 bounds: Bounds::default(),
117 zoom: Cell::new(1.0),
118 geometry_failed: Cell::new(false),
119 webview: Rc::new(webview),
120 #[cfg(all(feature = "gpui-fast", target_os = "macos"))]
121 composition,
122 #[cfg(all(feature = "gpui-fast", target_os = "linux"))]
123 composition: None,
124 #[cfg(target_os = "linux")]
125 scale_factor: Cell::new(window.scale_factor()),
126 #[cfg(target_os = "linux")]
127 origin,
128 }
129 }
130
131 pub fn build(
138 builder: wry::WebViewBuilder,
139 window: &mut Window,
140 cx: &mut App,
141 ) -> wry::Result<Self> {
142 #[cfg(target_os = "linux")]
143 let webview = {
144 let parent = linux::X11Parent::new(window)?;
145 linux::ensure_gtk(cx)?;
146
147 #[cfg(feature = "gpui-fast")]
148 match NativeWebViewSurface::new(window, cx) {
149 Ok(surface) => {
150 let webview = builder.build_as_child(&surface.parent()?)?;
151 let mut this = Self::new(webview, window, cx);
152 this.composition = Some(surface);
153 return Ok(this);
154 }
155 Err(error) => log::warn!("WebView composition unavailable: {error:#}"),
156 }
157
158 builder.build_as_child(&parent)?
159 };
160 #[cfg(not(target_os = "linux"))]
161 let webview = builder.build_as_child(&*window)?;
162
163 Ok(Self::new(webview, window, cx))
164 }
165
166 pub fn set_bounds(&self, bounds: Rect) -> wry::Result<()> {
169 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
170 if let Some(surface) = &self.composition {
171 return surface
172 .set_bounds(&self.webview, bounds)
173 .map_err(|error| wry::Error::Io(std::io::Error::other(error)));
174 }
175 self.webview.set_bounds(bounds)
176 }
177
178 pub fn set_geometry(
183 &self,
184 document: Bounds<Pixels>,
185 clip: Bounds<Pixels>,
186 ) -> anyhow::Result<()> {
187 let visible = document.intersect(&clip);
188 if visible.is_empty() || !self.visible.get() {
189 self.set_native_visible(false)?;
190 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
191 if let Some(surface) = &self.composition {
192 if self.visible.get() && !document.is_empty() {
194 surface.set_geometry(&self.webview, document, document)?;
195 }
196 }
197 return Ok(());
198 }
199 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
200 if let Some(surface) = &self.composition {
201 surface.set_geometry(&self.webview, document, visible)?;
202 surface.set_visible(true)?;
203 self.webview.set_visible(true)?;
204 return Ok(());
205 }
206 #[cfg(target_os = "linux")]
207 let rect = linux::device_bounds(document, self.scale_factor.get());
208 #[cfg(not(target_os = "linux"))]
209 let rect = Rect {
210 position: wry::dpi::LogicalPosition::new(
211 f64::from(document.origin.x),
212 f64::from(document.origin.y),
213 )
214 .into(),
215 size: wry::dpi::LogicalSize::new(
216 f64::from(document.size.width),
217 f64::from(document.size.height),
218 )
219 .into(),
220 };
221 self.set_bounds(rect)?;
222 self.set_native_visible(true)?;
223 Ok(())
224 }
225
226 fn set_native_visible(&self, visible: bool) -> wry::Result<()> {
229 self.webview.set_visible(visible)?;
230 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
231 if let Some(surface) = &self.composition {
232 surface
233 .set_visible(visible)
234 .map_err(|error| wry::Error::Io(std::io::Error::other(error)))?;
235 }
236 Ok(())
237 }
238
239 pub fn set_visible(&self, visible: bool) -> wry::Result<()> {
241 let focus_result = if !visible && self.visible.get() {
243 self.webview.focus_parent()
244 } else {
245 Ok(())
246 };
247 self.set_native_visible(visible)?;
248 self.visible.set(visible);
249 focus_result
250 }
251
252 pub fn show(&mut self) {
254 let _ = self.set_visible(true);
255 }
256
257 pub fn hide(&mut self) {
259 let _ = self.set_visible(false);
260 }
261
262 pub fn visible(&self) -> bool {
264 self.visible.get()
265 }
266
267 pub fn bounds(&self) -> Bounds<Pixels> {
269 self.bounds
270 }
271
272 pub fn back(&mut self) -> anyhow::Result<()> {
274 Ok(self.webview.evaluate_script("history.back();")?)
275 }
276
277 pub fn forward(&mut self) -> anyhow::Result<()> {
279 Ok(self.webview.evaluate_script("history.forward();")?)
280 }
281
282 pub fn load_url(&mut self, url: &str) {
284 let _ = self.webview.load_url(url);
285 }
286
287 pub fn set_theme(&self, theme: wry::Theme) -> wry::Result<()> {
293 #[cfg(target_os = "linux")]
294 linux::set_theme(theme);
295 #[cfg(target_os = "macos")]
296 macos::set_theme(&self.webview, theme);
297 #[cfg(target_os = "windows")]
298 wry::WebViewExtWindows::set_theme(&*self.webview, theme)?;
299 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
300 let _ = theme;
301 Ok(())
302 }
303
304 pub fn set_zoom(&self, zoom: f64) -> wry::Result<()> {
308 self.zoom.set(zoom);
309 #[cfg(target_os = "linux")]
312 linux::match_scale_factor(&self.webview, self.scale_factor.get(), zoom);
313 #[cfg(not(target_os = "linux"))]
314 self.webview.zoom(zoom)?;
315 Ok(())
316 }
317
318 pub fn zoom(&self) -> f64 {
320 self.zoom.get()
321 }
322
323 pub fn raw(&self) -> &wry::WebView {
325 &self.webview
326 }
327
328 pub fn handle(&self) -> WebViewHandle {
330 WebViewHandle(self.webview.clone())
331 }
332}
333
334impl Deref for WebView {
335 type Target = wry::WebView;
336
337 fn deref(&self) -> &Self::Target {
338 &self.webview
339 }
340}
341
342impl Focusable for WebView {
343 fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
344 self.focus_handle.clone()
345 }
346}
347
348impl EventEmitter<DismissEvent> for WebView {}
349
350impl Render for WebView {
351 fn render(
352 &mut self,
353 window: &mut gpui::Window,
354 cx: &mut gpui::Context<Self>,
355 ) -> impl IntoElement {
356 let view = cx.entity().clone();
357
358 div()
359 .track_focus(&self.focus_handle)
360 .size_full()
361 .child({
362 let view = cx.entity().clone();
363 canvas(
364 move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
365 |_, _, _, _| {},
366 )
367 .absolute()
368 .size_full()
369 })
370 .child(WebViewElement::new(self.webview.clone(), view, window, cx))
371 }
372}
373
374pub struct WebViewElement {
376 parent: Entity<WebView>,
377 view: Rc<wry::WebView>,
378}
379
380impl WebViewElement {
381 pub fn new(
383 view: Rc<wry::WebView>,
384 parent: Entity<WebView>,
385 _window: &mut Window,
386 _cx: &mut App,
387 ) -> Self {
388 Self { view, parent }
389 }
390}
391
392impl IntoElement for WebViewElement {
393 type Element = WebViewElement;
394
395 fn into_element(self) -> Self::Element {
396 self
397 }
398}
399
400impl Element for WebViewElement {
401 type RequestLayoutState = ();
402 type PrepaintState = Option<Hitbox>;
403
404 fn id(&self) -> Option<ElementId> {
405 None
406 }
407
408 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
409 None
410 }
411
412 fn request_layout(
413 &mut self,
414 _: Option<&GlobalElementId>,
415 _: Option<&gpui::InspectorElementId>,
416 window: &mut Window,
417 cx: &mut App,
418 ) -> (LayoutId, Self::RequestLayoutState) {
419 let style = Style {
420 size: Size::full(),
421 flex_shrink: 1.,
422 ..Default::default()
423 };
424
425 let id = window.request_layout(style, [], cx);
427 (id, ())
428 }
429
430 fn prepaint(
431 &mut self,
432 _: Option<&GlobalElementId>,
433 _: Option<&gpui::InspectorElementId>,
434 bounds: Bounds<Pixels>,
435 _: &mut Self::RequestLayoutState,
436 window: &mut Window,
437 cx: &mut App,
438 ) -> Self::PrepaintState {
439 if !self.parent.read(cx).visible() {
440 return None;
441 }
442
443 let parent = self.parent.read(cx);
444 #[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
445 if let Some(surface) = &parent.composition {
446 surface.set_scale_factor(window.scale_factor());
447 }
448 #[cfg(target_os = "linux")]
449 if parent.scale_factor.replace(window.scale_factor()) != window.scale_factor() {
450 linux::match_scale_factor(&parent.webview, window.scale_factor(), parent.zoom.get());
451 }
452 #[cfg(target_os = "linux")]
453 parent.origin.set(bounds.origin);
454 let clip = window.content_mask().bounds;
455 if let Err(error) = parent.set_geometry(bounds, clip) {
456 if !parent.geometry_failed.replace(true) {
457 log::warn!("WebView geometry update failed: {error:#}");
458 }
459 let _ = parent.set_native_visible(false);
460 return None;
461 }
462 parent.geometry_failed.set(false);
463 let visible = bounds.intersect(&clip);
464 if visible.is_empty() {
465 return None;
466 }
467
468 Some(window.insert_hitbox(visible, gpui::HitboxBehavior::BlockMouse))
470 }
471
472 fn paint(
473 &mut self,
474 _: Option<&GlobalElementId>,
475 _: Option<&gpui::InspectorElementId>,
476 bounds: Bounds<Pixels>,
477 _: &mut Self::RequestLayoutState,
478 hitbox: &mut Self::PrepaintState,
479 window: &mut Window,
480 _: &mut App,
481 ) {
482 let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds);
483 window.with_content_mask(Some(ContentMask { bounds }), |window| {
484 let webview = self.view.clone();
485 window.on_mouse_event(move |event: &MouseDownEvent, _, _, _| {
486 if !bounds.contains(&event.position) {
487 let _ = webview.focus_parent();
489 }
490 });
491 });
492 }
493}