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