1use crate::{
2 DataStore,
3 host::Host,
4 page::{Edit, Page, Report},
5};
6use gpui::{
7 Action, AnyWindowHandle, App, Context, EventEmitter, FocusHandle, Focusable, Global,
8 IntoElement, KeyBinding, Render, Subscription, Task, Window, actions, div, prelude::*,
9};
10use serde::de::DeserializeOwned;
11use std::{fmt, rc::Rc, time::Duration};
12
13pub struct WebView {
39 page: Rc<Page>,
40 location: Option<String>,
41 title: String,
42 loading: bool,
43 history: (bool, bool),
45 focus_window: Option<AnyWindowHandle>,
47 _focus: [Subscription; 2],
48 _reports: Task<()>,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub enum WebViewEvent {
54 Location(String),
57 Title(String),
59 History {
62 back: bool,
63 forward: bool,
64 },
65 Load(LoadState),
66 NewWindow(String),
70 Console(ConsoleMessage),
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct ConsoleMessage {
77 pub level: ConsoleLevel,
78 pub text: String,
81 pub source: String,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum ConsoleLevel {
89 Debug,
90 Log,
91 Info,
92 Warn,
93 Error,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum LoadState {
98 Started,
99 Finished,
100}
101
102#[derive(Debug)]
104pub enum EvalError {
105 Unavailable,
107 Timeout,
109 Script,
111 Decode(serde_json::Error),
113}
114
115impl fmt::Display for EvalError {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 Self::Unavailable => f.write_str("no page to evaluate in"),
119 Self::Timeout => f.write_str("script timed out"),
120 Self::Script => f.write_str("script threw or returned no JSON value"),
121 Self::Decode(error) => write!(f, "script value: {error}"),
122 }
123 }
124}
125
126impl std::error::Error for EvalError {}
127
128impl WebView {
129 pub fn new(url: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
130 Page::start(window, cx);
131 bind_edits(cx);
132 let focus = cx.focus_handle();
133 let subscriptions = Self::listen(&focus, window, cx);
134 let (reports, received) = async_channel::unbounded();
135 let task = cx.spawn(async move |this, cx| {
137 while let Ok(report) = received.recv().await {
138 if this.update(cx, |this, cx| this.report(report, cx)).is_err() {
139 return;
140 }
141 }
142 });
143 Self {
144 page: Rc::new(Page::new(url.into(), focus, reports, cx)),
145 location: None,
146 title: String::new(),
147 loading: false,
148 history: (false, false),
149 focus_window: Some(window.window_handle()),
150 _focus: subscriptions,
151 _reports: task,
152 }
153 }
154
155 fn listen(
157 focus: &FocusHandle,
158 window: &mut Window,
159 cx: &mut Context<Self>,
160 ) -> [Subscription; 2] {
161 [
162 cx.on_focus(focus, window, |this: &mut Self, window, _| {
163 this.page.watch_keys(window);
164 this.page.take_keys()
165 }),
166 cx.on_blur(focus, window, |this: &mut Self, _, _| this.page.give_keys()),
167 ]
168 }
169
170 pub fn location(&self) -> Option<&str> {
172 self.location.as_deref()
173 }
174
175 pub fn title(&self) -> &str {
176 &self.title
177 }
178
179 pub fn can_go_back(&self) -> bool {
182 self.history.0
183 }
184
185 pub fn can_go_forward(&self) -> bool {
188 self.history.1
189 }
190
191 pub fn is_loading(&self) -> bool {
192 self.loading
193 }
194
195 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
199 *self.page.user_agent.borrow_mut() = Some(user_agent.into());
200 self
201 }
202
203 pub fn with_console(self) -> Self {
208 self.page.console.set(true);
209 self
210 }
211
212 pub fn with_data_store(self, store: DataStore) -> Self {
216 *self.page.store.borrow_mut() = store;
217 self
218 }
219
220 #[cfg(feature = "inspector")]
224 pub fn with_inspector(self) -> Self {
225 self.page.inspector.set(true);
226 self
227 }
228
229 #[cfg(feature = "inspector")]
232 pub fn open_inspector(&self) {
233 self.page.open_inspector();
234 }
235
236 pub fn load(&mut self, url: impl Into<String>) {
239 self.page.load(url.into());
240 }
241
242 pub fn release_keys(&self) {
245 self.page.give_keys();
246 }
247
248 pub fn back(&mut self) {
249 self.page.back();
250 }
251
252 pub fn forward(&mut self) {
253 self.page.forward();
254 }
255
256 pub fn reload(&mut self) {
257 self.page.reload();
258 }
259
260 pub fn reload_bypassing_cache(&mut self) {
262 self.page.reload_bypassing_cache();
263 }
264
265 pub fn eval<T: DeserializeOwned + 'static>(
269 &self,
270 script: &str,
271 timeout: Duration,
272 cx: &App,
273 ) -> Task<Result<T, EvalError>> {
274 let (answer, answered) = async_channel::bounded::<Option<String>>(2);
275 let timer = answer.clone();
276 let script = format!("JSON.stringify((\n{script}\n) ?? null)");
280 if !self.page.eval(&script, move |json| {
281 let _ = answer.try_send(Some(json));
282 }) {
283 return Task::ready(Err(EvalError::Unavailable));
284 }
285 let expired = cx.background_executor().timer(timeout);
286 cx.foreground_executor()
287 .spawn(async move {
288 expired.await;
289 let _ = timer.try_send(None);
290 })
291 .detach();
292 cx.foreground_executor().spawn(async move {
293 let json = answered
294 .recv()
295 .await
296 .ok()
297 .flatten()
298 .ok_or(EvalError::Timeout)?;
299 let json = serde_json::from_str::<Option<String>>(&json)
300 .ok()
301 .flatten()
302 .ok_or(EvalError::Script)?;
303 serde_json::from_str(&json).map_err(EvalError::Decode)
304 })
305 }
306
307 fn report(&mut self, report: Report, cx: &mut Context<Self>) {
308 match report {
309 Report::Pressed => {
311 if self.page.holds_keys()
312 && let Some(window) = self.page.window()
313 {
314 let focus = self.page.focus.clone();
315 App::defer(cx, move |cx| {
316 let _ = window.update(cx, |_, window, cx| window.focus(&focus, cx));
317 });
318 }
319 }
320 Report::Key(keystroke) => {
321 if let Some(window) = self.page.window() {
322 App::defer(cx, move |cx| {
323 let _ = window.update(cx, |_, window, cx| {
324 window.dispatch_keystroke(keystroke, cx);
325 });
326 });
327 }
328 }
329 Report::Moved => {
330 if let Some(url) = self.page.location() {
331 self.locate(url, cx);
332 }
333 self.step(cx);
334 }
335 Report::Load(state, url) => {
336 self.locate(url, cx);
337 self.loading = state == LoadState::Started;
338 cx.emit(WebViewEvent::Load(state));
339 self.step(cx);
340 }
341 Report::Title(title) => {
343 if title != self.title {
344 self.title = title.clone();
345 cx.emit(WebViewEvent::Title(title));
346 }
347 }
348 Report::Opened(url) => cx.emit(WebViewEvent::NewWindow(url)),
349 Report::Console(message) => cx.emit(WebViewEvent::Console(message)),
350 Report::Still(still) => {
351 self.page.captured(still);
352 cx.notify();
353 }
354 }
355 }
356
357 fn step(&mut self, cx: &mut Context<Self>) {
358 let history = self.page.history();
359 if history != self.history {
360 self.history = history;
361 let (back, forward) = history;
362 cx.emit(WebViewEvent::History { back, forward });
363 }
364 }
365
366 fn locate(&mut self, url: String, cx: &mut Context<Self>) {
367 if self.location.as_ref() != Some(&url) {
368 self.location = Some(url.clone());
369 cx.emit(WebViewEvent::Location(url));
370 }
371 }
372}
373
374impl EventEmitter<WebViewEvent> for WebView {}
375
376impl Focusable for WebView {
377 fn focus_handle(&self, _cx: &App) -> FocusHandle {
378 self.page.focus.clone()
379 }
380}
381
382impl Render for WebView {
383 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
384 if self.focus_window != Some(window.window_handle()) {
385 Page::start(window, cx);
386 self._focus = Self::listen(&self.page.focus, window, cx);
387 self.focus_window = Some(window.window_handle());
388 }
389 if self.page.focus.is_focused(window) {
390 self.page.watch_keys(window);
391 }
392 div()
393 .size_full()
394 .key_context(KEY_CONTEXT)
395 .on_action(on_edit::<Copy>(Edit::Copy, cx))
396 .on_action(on_edit::<Cut>(Edit::Cut, cx))
397 .on_action(on_edit::<Paste>(Edit::Paste, cx))
398 .on_action(on_edit::<SelectAll>(Edit::SelectAll, cx))
399 .on_action(on_edit::<Undo>(Edit::Undo, cx))
400 .on_action(on_edit::<Redo>(Edit::Redo, cx))
401 .child(Host {
402 surface: self.page.clone(),
403 })
404 }
405}
406
407actions!(webview, [Copy, Cut, Paste, SelectAll, Undo, Redo]);
408
409const KEY_CONTEXT: &str = "WebView";
410
411fn on_edit<A: Action>(
412 edit: Edit,
413 cx: &mut Context<WebView>,
414) -> impl Fn(&A, &mut Window, &mut App) + 'static {
415 cx.listener(move |this, _: &A, _, _| this.page.edit(edit))
416}
417
418fn bind_edits(cx: &mut App) {
420 struct Bound;
421 impl Global for Bound {}
422
423 if !Page::BINDS_EDITS || cx.has_global::<Bound>() {
424 return;
425 }
426 cx.set_global(Bound);
427 let context = Some(KEY_CONTEXT);
428 cx.bind_keys([
429 KeyBinding::new("cmd-c", Copy, context),
430 KeyBinding::new("cmd-x", Cut, context),
431 KeyBinding::new("cmd-v", Paste, context),
432 KeyBinding::new("cmd-a", SelectAll, context),
433 KeyBinding::new("cmd-z", Undo, context),
434 KeyBinding::new("cmd-shift-z", Redo, context),
435 ]);
436}