Skip to main content

browser/
view.rs

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
12/// A webview showing one page.
13///
14/// The page is built the first time the view is painted, in the window it is
15/// painted in, and moves to each other window the view is painted in. It sits
16/// at the element's bounds in every frame the element is painted and is parked
17/// in every frame it is not. A parked page stays loaded. A view painted in two
18/// windows in turn moves its page on every paint.
19///
20/// The page takes keys while the view's focus handle is focused, and a press
21/// in the page focuses the handle. On macOS, key equivalents (cmd or ctrl
22/// held) reach gpui's key dispatch before the page sees them. On Windows, a
23/// key with ctrl or alt held, or a function key, goes to gpui's key dispatch
24/// in place of the page when the keymap binds it in the view's key context.
25///
26/// gpui elements behind the page are not hovered, and gpui's cursor over the
27/// page is the arrow. On macOS the page sets the cursor over itself.
28///
29/// In a frame where a [`ui::cover`] recorded after the view overlaps it, the
30/// page is parked. On macOS a still of the page, taken while it is still up,
31/// is painted in its place; until the still arrives the page stays over the
32/// cover. Elsewhere the place is left empty.
33///
34/// Linux needs gpui on X11 and paints nothing under Wayland. Paints nothing
35/// off macOS, Windows and Linux.
36pub struct WebView {
37    page: Rc<Page>,
38    location: Option<String>,
39    title: String,
40    loading: bool,
41    /// The window [`Self::_focus`] listens in.
42    focus_window: Option<AnyWindowHandle>,
43    _focus: [Subscription; 2],
44    _reports: Task<()>,
45}
46
47/// What the page reports about itself.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum WebViewEvent {
50    /// The page's URL changed: a load, a redirect, or the page's own history
51    /// move (`pushState`, `replaceState`, a fragment).
52    Location(String),
53    /// The page's title changed.
54    Title(String),
55    Load(LoadState),
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum LoadState {
60    Started,
61    Finished,
62}
63
64/// Why [`WebView::eval`] has no value.
65#[derive(Debug)]
66pub enum EvalError {
67    /// The page is not built yet, or not on this platform.
68    Unavailable,
69    /// No answer within the timeout.
70    Timeout,
71    /// The script threw, or its value has no JSON form.
72    Script,
73    /// The JSON does not decode as the asked type.
74    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        // Ends when the page drops, which drops every sender.
98        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    /// Focus listeners are per window.
117    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    /// The URL the page last reported; `None` before its first report.
132    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    /// The user agent the page is built with, in place of the platform's.
145    /// Read at the first paint. On macOS the default is Safari's, with the
146    /// installed Safari's version.
147    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    /// Before the first paint, replaces the URL the page is built with.
153    /// After it, navigates the page.
154    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    /// Evaluates `script`, a JavaScript expression, in the page's main frame
171    /// and decodes its value from JSON. `undefined` decodes as `null`; a
172    /// promise is not awaited.
173    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        // WebKit hands the value to `NSJSONSerialization`, which cannot encode
182        // a DOM node or `undefined`, so the page encodes it and this decodes
183        // twice.
184        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            // Deferred: the window's handlers may update this view.
215            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            // Live pages rewrite their title, some every second.
245            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
310/// Binds the edit keys in the view's context, once per app.
311fn 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}