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