1use gpui::prelude::*;
17use gpui::{div, px, Context, EventEmitter, FocusHandle, IntoElement, SharedString, Window};
18
19use crate::theme::{theme, Size};
20
21#[cfg(feature = "webview")]
22use {
23 gpui::{canvas, Bounds, Pixels},
24 std::{cell::RefCell, rc::Rc, time::Duration},
25 wry::{
26 dpi::{LogicalPosition, LogicalSize},
27 PageLoadEvent, Rect, WebViewBuilder,
28 },
29};
30
31#[derive(Debug, Clone)]
33pub enum WebViewEvent {
34 TitleChanged(SharedString),
36 UrlChanged(SharedString),
38 LoadStarted,
40 LoadFinished,
42}
43
44#[derive(Clone)]
46enum Source {
47 Empty,
49 Url(SharedString),
51 Html(SharedString),
53}
54
55pub struct WebView {
57 source: Source,
58 focus: FocusHandle,
59 radius: Option<Size>,
60 bordered: bool,
61 transparent: bool,
62 width: Option<f32>,
63 height: Option<f32>,
64
65 #[cfg(feature = "webview")]
66 inner: Option<Rc<wry::WebView>>,
67 #[cfg(feature = "webview")]
68 queue: Rc<RefCell<Vec<WebViewEvent>>>,
69 #[cfg(feature = "webview")]
70 draining: bool,
71}
72
73impl EventEmitter<WebViewEvent> for WebView {}
74
75impl WebView {
76 pub fn new(cx: &mut Context<Self>) -> Self {
77 WebView {
78 source: Source::Empty,
79 focus: cx.focus_handle(),
80 radius: None,
81 bordered: true,
82 transparent: false,
83 width: None,
84 height: None,
85
86 #[cfg(feature = "webview")]
87 inner: None,
88 #[cfg(feature = "webview")]
89 queue: Rc::new(RefCell::new(Vec::new())),
90 #[cfg(feature = "webview")]
91 draining: false,
92 }
93 }
94
95 pub fn url(mut self, url: impl Into<SharedString>) -> Self {
97 self.source = Source::Url(url.into());
98 self
99 }
100
101 pub fn html(mut self, html: impl Into<SharedString>) -> Self {
103 self.source = Source::Html(html.into());
104 self
105 }
106
107 pub fn radius(mut self, radius: Size) -> Self {
109 self.radius = Some(radius);
110 self
111 }
112
113 pub fn bordered(mut self, bordered: bool) -> Self {
115 self.bordered = bordered;
116 self
117 }
118
119 pub fn transparent(mut self, transparent: bool) -> Self {
121 self.transparent = transparent;
122 self
123 }
124
125 pub fn width(mut self, width: f32) -> Self {
127 self.width = Some(width);
128 self
129 }
130
131 pub fn height(mut self, height: f32) -> Self {
133 self.height = Some(height);
134 self
135 }
136
137 pub fn load_url(&mut self, url: impl Into<SharedString>, cx: &mut Context<Self>) {
139 let url = url.into();
140 #[cfg(feature = "webview")]
141 if let Some(inner) = &self.inner {
142 let _ = inner.load_url(&url);
143 }
144 self.source = Source::Url(url);
145 cx.notify();
146 }
147
148 pub fn load_html(&mut self, html: impl Into<SharedString>, cx: &mut Context<Self>) {
150 let html = html.into();
151 #[cfg(feature = "webview")]
152 if let Some(inner) = &self.inner {
153 let _ = inner.load_html(&html);
154 }
155 self.source = Source::Html(html);
156 cx.notify();
157 }
158
159 pub fn evaluate_script(&self, _js: &str) {
161 #[cfg(feature = "webview")]
162 if let Some(inner) = &self.inner {
163 let _ = inner.evaluate_script(_js);
164 }
165 }
166
167 #[cfg(feature = "webview")]
170 fn ensure_view(&mut self, window: &mut Window, cx: &mut Context<Self>, bounds: Bounds<Pixels>) {
171 if self.inner.is_some() {
172 return;
173 }
174
175 let queue = self.queue.clone();
176 let (q_title, q_nav, q_load) = (queue.clone(), queue.clone(), queue.clone());
177
178 let mut builder = WebViewBuilder::new()
179 .with_bounds(rect_from(bounds))
180 .with_transparent(self.transparent)
181 .with_document_title_changed_handler(move |title| {
182 q_title
183 .borrow_mut()
184 .push(WebViewEvent::TitleChanged(title.into()));
185 })
186 .with_navigation_handler(move |url| {
187 q_nav
188 .borrow_mut()
189 .push(WebViewEvent::UrlChanged(url.into()));
190 true
191 })
192 .with_on_page_load_handler(move |event, _url| {
193 q_load.borrow_mut().push(match event {
194 PageLoadEvent::Started => WebViewEvent::LoadStarted,
195 PageLoadEvent::Finished => WebViewEvent::LoadFinished,
196 });
197 });
198
199 builder = match &self.source {
200 Source::Url(url) => builder.with_url(url.as_ref()),
201 Source::Html(html) => builder.with_html(html.as_ref()),
202 Source::Empty => builder,
203 };
204
205 match builder.build_as_child(&*window) {
206 Ok(view) => self.inner = Some(Rc::new(view)),
207 Err(err) => {
208 eprintln!("guise: failed to create webview: {err}");
209 return;
210 }
211 }
212
213 if !self.draining {
214 self.draining = true;
215 cx.spawn(async move |this, cx| loop {
216 cx.background_executor()
217 .timer(Duration::from_millis(40))
218 .await;
219 let drained: Vec<WebViewEvent> = queue.borrow_mut().drain(..).collect();
220 let pushed = this.update(cx, |_this, cx| {
221 let any = !drained.is_empty();
222 for event in drained {
223 cx.emit(event);
224 }
225 if any {
226 cx.notify();
227 }
228 });
229 if pushed.is_err() {
230 break;
231 }
232 })
233 .detach();
234 }
235 }
236}
237
238#[cfg(feature = "webview")]
239fn rect_from(bounds: Bounds<Pixels>) -> Rect {
240 Rect {
241 position: LogicalPosition::new(bounds.origin.x.to_f64(), bounds.origin.y.to_f64()).into(),
242 size: LogicalSize::new(bounds.size.width.to_f64(), bounds.size.height.to_f64()).into(),
243 }
244}
245
246impl Render for WebView {
247 #[cfg(feature = "webview")]
248 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
249 if self.inner.is_none() {
253 let w = self.width.unwrap_or(800.0);
254 let h = self.height.unwrap_or(600.0);
255 let initial = Bounds {
256 origin: gpui::point(px(0.0), px(0.0)),
257 size: gpui::size(px(w), px(h)),
258 };
259 self.ensure_view(window, cx, initial);
260 }
261
262 let t = theme(cx);
263 let radius = t.radius(self.radius.unwrap_or(t.default_radius));
264 let border = t.border().hsla();
265 let bg = t.surface().hsla();
266
267 let view = self.inner.clone();
270 let surface = canvas(
271 move |_bounds, _window, _app| {},
272 move |bounds, _state, _window, _app| {
273 if let Some(view) = &view {
274 let _ = view.set_bounds(rect_from(bounds));
275 }
276 },
277 )
278 .size_full();
279
280 frame(self.bordered, radius, border, bg, self.width, self.height)
281 .track_focus(&self.focus)
282 .child(surface)
283 }
284
285 #[cfg(not(feature = "webview"))]
286 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
287 let t = theme(cx);
288 let radius = t.radius(self.radius.unwrap_or(t.default_radius));
289 let border = t.border().hsla();
290 let bg = t.surface().hsla();
291 let dimmed = t.dimmed().hsla();
292 let label = match &self.source {
293 Source::Url(url) => url.clone(),
294 Source::Html(html) => SharedString::from(format!("inline HTML ({} bytes)", html.len())),
295 Source::Empty => SharedString::from("no source"),
296 };
297
298 frame(self.bordered, radius, border, bg, self.width, self.height)
299 .track_focus(&self.focus)
300 .items_center()
301 .justify_center()
302 .text_color(dimmed)
303 .child(SharedString::from(format!("WebView (disabled): {label}")))
304 }
305}
306
307fn frame(
309 bordered: bool,
310 radius: f32,
311 border: gpui::Hsla,
312 bg: gpui::Hsla,
313 width: Option<f32>,
314 height: Option<f32>,
315) -> gpui::Stateful<gpui::Div> {
316 let mut root = div().id("guise-webview").flex().overflow_hidden().bg(bg);
317 root = match width {
318 Some(w) => root.w(px(w)),
319 None => root.w_full(),
320 };
321 root = match height {
322 Some(h) => root.h(px(h)),
323 None => root.h_full(),
324 };
325 if bordered {
326 root = root.border_1().border_color(border).rounded(px(radius));
327 }
328 root
329}