1use crate::{
2 host::Host,
3 page::{Edit, Page, Report},
4};
5use gpui::{
6 Action, AnyWindowHandle, App, Context, EventEmitter, FocusHandle, Focusable, Global,
7 IntoElement, KeyBinding, Render, Subscription, Task, Window, actions, div, prelude::*,
8};
9use serde::de::DeserializeOwned;
10use std::{fmt, rc::Rc, time::Duration};
11
12pub struct WebView {
37 page: Rc<Page>,
38 location: Option<String>,
39 title: String,
40 loading: bool,
41 focus_window: Option<AnyWindowHandle>,
43 _focus: [Subscription; 2],
44 _reports: Task<()>,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum WebViewEvent {
50 Location(String),
53 Title(String),
55 Load(LoadState),
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum LoadState {
60 Started,
61 Finished,
62}
63
64#[derive(Debug)]
66pub enum EvalError {
67 Unavailable,
69 Timeout,
71 Script,
73 Decode(serde_json::Error),
75}
76
77impl fmt::Display for EvalError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::Unavailable => f.write_str("no page to evaluate in"),
81 Self::Timeout => f.write_str("script timed out"),
82 Self::Script => f.write_str("script threw or returned no JSON value"),
83 Self::Decode(error) => write!(f, "script value: {error}"),
84 }
85 }
86}
87
88impl std::error::Error for EvalError {}
89
90impl WebView {
91 pub fn new(url: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
92 Page::start(window, cx);
93 bind_edits(cx);
94 let focus = cx.focus_handle();
95 let subscriptions = Self::listen(&focus, window, cx);
96 let (reports, received) = async_channel::unbounded();
97 let task = cx.spawn(async move |this, cx| {
99 while let Ok(report) = received.recv().await {
100 if this.update(cx, |this, cx| this.report(report, cx)).is_err() {
101 return;
102 }
103 }
104 });
105 Self {
106 page: Rc::new(Page::new(url.into(), focus, reports, cx)),
107 location: None,
108 title: String::new(),
109 loading: false,
110 focus_window: Some(window.window_handle()),
111 _focus: subscriptions,
112 _reports: task,
113 }
114 }
115
116 fn listen(
118 focus: &FocusHandle,
119 window: &mut Window,
120 cx: &mut Context<Self>,
121 ) -> [Subscription; 2] {
122 [
123 cx.on_focus(focus, window, |this: &mut Self, window, _| {
124 this.page.watch_keys(window);
125 this.page.take_keys()
126 }),
127 cx.on_blur(focus, window, |this: &mut Self, _, _| this.page.give_keys()),
128 ]
129 }
130
131 pub fn location(&self) -> Option<&str> {
133 self.location.as_deref()
134 }
135
136 pub fn title(&self) -> &str {
137 &self.title
138 }
139
140 pub fn is_loading(&self) -> bool {
141 self.loading
142 }
143
144 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
148 *self.page.user_agent.borrow_mut() = Some(user_agent.into());
149 self
150 }
151
152 pub fn load(&mut self, url: impl Into<String>) {
155 self.page.load(url.into());
156 }
157
158 pub fn back(&mut self) {
159 self.page.back();
160 }
161
162 pub fn forward(&mut self) {
163 self.page.forward();
164 }
165
166 pub fn reload(&mut self) {
167 self.page.reload();
168 }
169
170 pub fn eval<T: DeserializeOwned + 'static>(
174 &self,
175 script: &str,
176 timeout: Duration,
177 cx: &App,
178 ) -> Task<Result<T, EvalError>> {
179 let (answer, answered) = async_channel::bounded::<Option<String>>(2);
180 let timer = answer.clone();
181 let script = format!("JSON.stringify((\n{script}\n) ?? null)");
185 if !self.page.eval(&script, move |json| {
186 let _ = answer.try_send(Some(json));
187 }) {
188 return Task::ready(Err(EvalError::Unavailable));
189 }
190 let expired = cx.background_executor().timer(timeout);
191 cx.foreground_executor()
192 .spawn(async move {
193 expired.await;
194 let _ = timer.try_send(None);
195 })
196 .detach();
197 cx.foreground_executor().spawn(async move {
198 let json = answered
199 .recv()
200 .await
201 .ok()
202 .flatten()
203 .ok_or(EvalError::Timeout)?;
204 let json = serde_json::from_str::<Option<String>>(&json)
205 .ok()
206 .flatten()
207 .ok_or(EvalError::Script)?;
208 serde_json::from_str(&json).map_err(EvalError::Decode)
209 })
210 }
211
212 fn report(&mut self, report: Report, cx: &mut Context<Self>) {
213 match report {
214 Report::Pressed => {
216 if self.page.holds_keys()
217 && let Some(window) = self.page.window()
218 {
219 let focus = self.page.focus.clone();
220 App::defer(cx, move |cx| {
221 let _ = window.update(cx, |_, window, cx| window.focus(&focus, cx));
222 });
223 }
224 }
225 Report::Key(keystroke) => {
226 if let Some(window) = self.page.window() {
227 App::defer(cx, move |cx| {
228 let _ = window.update(cx, |_, window, cx| {
229 window.dispatch_keystroke(keystroke, cx);
230 });
231 });
232 }
233 }
234 Report::Moved => {
235 if let Some(url) = self.page.location() {
236 self.locate(url, cx);
237 }
238 }
239 Report::Load(state, url) => {
240 self.locate(url, cx);
241 self.loading = state == LoadState::Started;
242 cx.emit(WebViewEvent::Load(state));
243 }
244 Report::Title(title) => {
246 if title != self.title {
247 self.title = title.clone();
248 cx.emit(WebViewEvent::Title(title));
249 }
250 }
251 Report::Still(still) => {
252 self.page.captured(still);
253 cx.notify();
254 }
255 }
256 }
257
258 fn locate(&mut self, url: String, cx: &mut Context<Self>) {
259 if self.location.as_ref() != Some(&url) {
260 self.location = Some(url.clone());
261 cx.emit(WebViewEvent::Location(url));
262 }
263 }
264}
265
266impl EventEmitter<WebViewEvent> for WebView {}
267
268impl Focusable for WebView {
269 fn focus_handle(&self, _cx: &App) -> FocusHandle {
270 self.page.focus.clone()
271 }
272}
273
274impl Render for WebView {
275 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
276 if self.focus_window != Some(window.window_handle()) {
277 Page::start(window, cx);
278 self._focus = Self::listen(&self.page.focus, window, cx);
279 self.focus_window = Some(window.window_handle());
280 }
281 if self.page.focus.is_focused(window) {
282 self.page.watch_keys(window);
283 }
284 div()
285 .size_full()
286 .key_context(KEY_CONTEXT)
287 .on_action(on_edit::<Copy>(Edit::Copy, cx))
288 .on_action(on_edit::<Cut>(Edit::Cut, cx))
289 .on_action(on_edit::<Paste>(Edit::Paste, cx))
290 .on_action(on_edit::<SelectAll>(Edit::SelectAll, cx))
291 .on_action(on_edit::<Undo>(Edit::Undo, cx))
292 .on_action(on_edit::<Redo>(Edit::Redo, cx))
293 .child(Host {
294 surface: self.page.clone(),
295 })
296 }
297}
298
299actions!(webview, [Copy, Cut, Paste, SelectAll, Undo, Redo]);
300
301const KEY_CONTEXT: &str = "WebView";
302
303fn on_edit<A: Action>(
304 edit: Edit,
305 cx: &mut Context<WebView>,
306) -> impl Fn(&A, &mut Window, &mut App) + 'static {
307 cx.listener(move |this, _: &A, _, _| this.page.edit(edit))
308}
309
310fn bind_edits(cx: &mut App) {
312 struct Bound;
313 impl Global for Bound {}
314
315 if !Page::BINDS_EDITS || cx.has_global::<Bound>() {
316 return;
317 }
318 cx.set_global(Bound);
319 let context = Some(KEY_CONTEXT);
320 cx.bind_keys([
321 KeyBinding::new("cmd-c", Copy, context),
322 KeyBinding::new("cmd-x", Cut, context),
323 KeyBinding::new("cmd-v", Paste, context),
324 KeyBinding::new("cmd-a", SelectAll, context),
325 KeyBinding::new("cmd-z", Undo, context),
326 KeyBinding::new("cmd-shift-z", Redo, context),
327 ]);
328}