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