bezel-browser 0.2.25

A platform webview hosted in a gpui window
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
use crate::{
    DataStore,
    host::Host,
    page::{Edit, Page, Report},
};
use gpui::{
    Action, AnyWindowHandle, App, Context, EventEmitter, FocusHandle, Focusable, Global,
    IntoElement, KeyBinding, Render, Subscription, Task, Window, actions, div, prelude::*,
};
use serde::de::DeserializeOwned;
use std::{fmt, rc::Rc, time::Duration};

/// A webview showing one page.
///
/// The page is built after the first time the view is painted, in the window
/// it is painted in, and shows from the paint after that; until then its place
/// is empty. It moves to each other window the view is painted in. It sits
/// at the element's bounds in every frame the element is painted and is parked
/// in every frame it is not. A parked page stays loaded. A view painted in two
/// windows in turn moves its page on every paint.
///
/// The page takes keys while the view's focus handle is focused, and a press
/// in the page focuses the handle. On macOS, a press on gpui's view outside
/// the page takes the keys back, whether or not the handle loses focus, and
/// key equivalents (cmd or ctrl held) reach gpui's key dispatch before the
/// page sees them. On Windows, a key with ctrl or alt held, or a function key,
/// goes to gpui's key dispatch in place of the page when the keymap binds it
/// in the view's key context.
///
/// gpui elements behind the page are not hovered, and gpui's cursor over the
/// page is the arrow. On macOS the page sets the cursor over itself.
///
/// In a frame where a [`ui::cover`] recorded after the view overlaps it, the
/// page is parked. On macOS a still of the page, taken while it is still up,
/// is painted in its place; until the still arrives the page stays over the
/// cover. Elsewhere the place is left empty.
///
/// Paints nothing off macOS and Windows.
pub struct WebView {
    page: Rc<Page>,
    location: Option<String>,
    title: String,
    loading: bool,
    /// Whether the page could step back, and forward, when it last moved.
    history: (bool, bool),
    /// The window [`Self::_focus`] listens in.
    focus_window: Option<AnyWindowHandle>,
    _focus: [Subscription; 2],
    _reports: Task<()>,
}

/// What the page reports about itself.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WebViewEvent {
    /// The page's URL changed: a load, a redirect, or the page's own history
    /// move (`pushState`, `replaceState`, a fragment).
    Location(String),
    /// The page's title changed.
    Title(String),
    /// Whether [`WebView::back`] and [`WebView::forward`] would move the page
    /// changed.
    History {
        back: bool,
        forward: bool,
    },
    Load(LoadState),
    /// The page asked to open this URL in a window of its own: a
    /// `target="_blank"` link or `window.open`. No window opens and the page
    /// stays where it is; where the URL goes is the host's.
    NewWindow(String),
    /// The page logged, with a page built [`WebView::with_console`].
    Console(ConsoleMessage),
}

/// One `console` call, uncaught error or unhandled rejection in the page.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConsoleMessage {
    pub level: ConsoleLevel,
    /// The logged values, each a string as is, an error as its stack, and
    /// anything else as JSON, joined by spaces.
    pub text: String,
    /// The URL of the frame that logged.
    pub source: String,
}

/// The `console` method a [`ConsoleMessage`] came from. Uncaught errors and
/// unhandled rejections are `Error`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConsoleLevel {
    Debug,
    Log,
    Info,
    Warn,
    Error,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoadState {
    Started,
    Finished,
}

/// Why [`WebView::eval`] has no value.
#[derive(Debug)]
pub enum EvalError {
    /// The page is not built yet, or not on this platform.
    Unavailable,
    /// No answer within the timeout.
    Timeout,
    /// The script threw, or its value has no JSON form.
    Script,
    /// The JSON does not decode as the asked type.
    Decode(serde_json::Error),
}

impl fmt::Display for EvalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Unavailable => f.write_str("no page to evaluate in"),
            Self::Timeout => f.write_str("script timed out"),
            Self::Script => f.write_str("script threw or returned no JSON value"),
            Self::Decode(error) => write!(f, "script value: {error}"),
        }
    }
}

impl std::error::Error for EvalError {}

impl WebView {
    pub fn new(url: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
        Page::start(window, cx);
        bind_edits(cx);
        let focus = cx.focus_handle();
        let subscriptions = Self::listen(&focus, window, cx);
        let (reports, received) = async_channel::unbounded();
        // Ends when the page drops, which drops every sender.
        let task = cx.spawn(async move |this, cx| {
            while let Ok(report) = received.recv().await {
                if this.update(cx, |this, cx| this.report(report, cx)).is_err() {
                    return;
                }
            }
        });
        Self {
            page: Rc::new(Page::new(url.into(), focus, reports, cx)),
            location: None,
            title: String::new(),
            loading: false,
            history: (false, false),
            focus_window: Some(window.window_handle()),
            _focus: subscriptions,
            _reports: task,
        }
    }

    /// Focus listeners are per window.
    fn listen(
        focus: &FocusHandle,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> [Subscription; 2] {
        [
            cx.on_focus(focus, window, |this: &mut Self, window, _| {
                this.page.watch_keys(window);
                this.page.take_keys()
            }),
            cx.on_blur(focus, window, |this: &mut Self, _, _| this.page.give_keys()),
        ]
    }

    /// The URL the page last reported; `None` before its first report.
    pub fn location(&self) -> Option<&str> {
        self.location.as_deref()
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    /// Whether [`Self::back`] would move the page, as of its last load or
    /// history move. `false` before the first paint.
    pub fn can_go_back(&self) -> bool {
        self.history.0
    }

    /// Whether [`Self::forward`] would move the page, as of its last load or
    /// history move. `false` before the first paint.
    pub fn can_go_forward(&self) -> bool {
        self.history.1
    }

    pub fn is_loading(&self) -> bool {
        self.loading
    }

    /// The user agent the page is built with, in place of the platform's.
    /// Read at the first paint. On macOS the default is Safari's, with the
    /// installed Safari's version.
    pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
        *self.page.user_agent.borrow_mut() = Some(user_agent.into());
        self
    }

    /// Reports the page's console as [`WebViewEvent::Console`]. Read at the
    /// first paint. Engine messages such as failed loads are not console
    /// calls and are not reported. In the inspector, every console message
    /// is attributed to the script that reports it.
    pub fn with_console(self) -> Self {
        self.page.console.set(true);
        self
    }

    /// The store the page is built with, in place of the platform's default.
    /// Read at the first paint. A page keeps the store it was built with: a
    /// different store takes a new `WebView`.
    pub fn with_data_store(self, store: DataStore) -> Self {
        *self.page.store.borrow_mut() = store;
        self
    }

    /// Builds the page inspectable. Read at the first paint. On macOS 13.3+
    /// Safari's Develop menu lists the page, and the page's context menu has
    /// Inspect Element.
    #[cfg(feature = "inspector")]
    pub fn with_inspector(self) -> Self {
        self.page.inspector.set(true);
        self
    }

    /// Opens the inspector of a page built with [`Self::with_inspector`].
    /// Does nothing before the first paint.
    #[cfg(feature = "inspector")]
    pub fn open_inspector(&self) {
        self.page.open_inspector();
    }

    /// Before the first paint, replaces the URL the page is built with.
    /// After it, navigates the page.
    pub fn load(&mut self, url: impl Into<String>) {
        self.page.load(url.into());
    }

    /// Takes the keys back from the page if it holds them, whatever gpui's
    /// focus is.
    pub fn release_keys(&self) {
        self.page.give_keys();
    }

    pub fn back(&mut self) {
        self.page.back();
    }

    pub fn forward(&mut self) {
        self.page.forward();
    }

    pub fn reload(&mut self) {
        self.page.reload();
    }

    /// Reloads from the network, past the cache.
    pub fn reload_bypassing_cache(&mut self) {
        self.page.reload_bypassing_cache();
    }

    /// Evaluates `script`, a JavaScript expression, in the page's main frame
    /// and decodes its value from JSON. `undefined` decodes as `null`; a
    /// promise is not awaited.
    pub fn eval<T: DeserializeOwned + 'static>(
        &self,
        script: &str,
        timeout: Duration,
        cx: &App,
    ) -> Task<Result<T, EvalError>> {
        let (answer, answered) = async_channel::bounded::<Option<String>>(2);
        let timer = answer.clone();
        // WebKit hands the value to `NSJSONSerialization`, which cannot encode
        // a DOM node or `undefined`, so the page encodes it and this decodes
        // twice.
        let script = format!("JSON.stringify((\n{script}\n) ?? null)");
        if !self.page.eval(&script, move |json| {
            let _ = answer.try_send(Some(json));
        }) {
            return Task::ready(Err(EvalError::Unavailable));
        }
        let expired = cx.background_executor().timer(timeout);
        cx.foreground_executor()
            .spawn(async move {
                expired.await;
                let _ = timer.try_send(None);
            })
            .detach();
        cx.foreground_executor().spawn(async move {
            let json = answered
                .recv()
                .await
                .ok()
                .flatten()
                .ok_or(EvalError::Timeout)?;
            let json = serde_json::from_str::<Option<String>>(&json)
                .ok()
                .flatten()
                .ok_or(EvalError::Script)?;
            serde_json::from_str(&json).map_err(EvalError::Decode)
        })
    }

    fn report(&mut self, report: Report, cx: &mut Context<Self>) {
        match report {
            // Deferred: the window's handlers may update this view.
            Report::Pressed => {
                if self.page.holds_keys()
                    && let Some(window) = self.page.window()
                {
                    let focus = self.page.focus.clone();
                    App::defer(cx, move |cx| {
                        let _ = window.update(cx, |_, window, cx| window.focus(&focus, cx));
                    });
                }
            }
            Report::Key(keystroke) => {
                if let Some(window) = self.page.window() {
                    App::defer(cx, move |cx| {
                        let _ = window.update(cx, |_, window, cx| {
                            window.dispatch_keystroke(keystroke, cx);
                        });
                    });
                }
            }
            Report::Moved => {
                if let Some(url) = self.page.location() {
                    self.locate(url, cx);
                }
                self.step(cx);
            }
            Report::Load(state, url) => {
                self.locate(url, cx);
                self.loading = state == LoadState::Started;
                cx.emit(WebViewEvent::Load(state));
                self.step(cx);
            }
            // Live pages rewrite their title, some every second.
            Report::Title(title) => {
                if title != self.title {
                    self.title = title.clone();
                    cx.emit(WebViewEvent::Title(title));
                }
            }
            Report::Opened(url) => cx.emit(WebViewEvent::NewWindow(url)),
            Report::Console(message) => cx.emit(WebViewEvent::Console(message)),
            Report::Still(still) => {
                self.page.captured(still);
                cx.notify();
            }
            Report::Built => cx.notify(),
        }
    }

    fn step(&mut self, cx: &mut Context<Self>) {
        let history = self.page.history();
        if history != self.history {
            self.history = history;
            let (back, forward) = history;
            cx.emit(WebViewEvent::History { back, forward });
        }
    }

    fn locate(&mut self, url: String, cx: &mut Context<Self>) {
        if self.location.as_ref() != Some(&url) {
            self.location = Some(url.clone());
            cx.emit(WebViewEvent::Location(url));
        }
    }
}

impl EventEmitter<WebViewEvent> for WebView {}

impl Focusable for WebView {
    fn focus_handle(&self, _cx: &App) -> FocusHandle {
        self.page.focus.clone()
    }
}

impl Render for WebView {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        if self.focus_window != Some(window.window_handle()) {
            Page::start(window, cx);
            self._focus = Self::listen(&self.page.focus, window, cx);
            self.focus_window = Some(window.window_handle());
        }
        if self.page.focus.is_focused(window) {
            self.page.watch_keys(window);
        }
        div()
            .size_full()
            .key_context(KEY_CONTEXT)
            .on_action(on_edit::<Copy>(Edit::Copy, cx))
            .on_action(on_edit::<Cut>(Edit::Cut, cx))
            .on_action(on_edit::<Paste>(Edit::Paste, cx))
            .on_action(on_edit::<SelectAll>(Edit::SelectAll, cx))
            .on_action(on_edit::<Undo>(Edit::Undo, cx))
            .on_action(on_edit::<Redo>(Edit::Redo, cx))
            .child(Host {
                surface: self.page.clone(),
            })
    }
}

actions!(webview, [Copy, Cut, Paste, SelectAll, Undo, Redo]);

const KEY_CONTEXT: &str = "WebView";

fn on_edit<A: Action>(
    edit: Edit,
    cx: &mut Context<WebView>,
) -> impl Fn(&A, &mut Window, &mut App) + 'static {
    cx.listener(move |this, _: &A, _, _| this.page.edit(edit))
}

/// Binds the edit keys in the view's context, once per app.
fn bind_edits(cx: &mut App) {
    struct Bound;
    impl Global for Bound {}

    if !Page::BINDS_EDITS || cx.has_global::<Bound>() {
        return;
    }
    cx.set_global(Bound);
    let context = Some(KEY_CONTEXT);
    cx.bind_keys([
        KeyBinding::new("cmd-c", Copy, context),
        KeyBinding::new("cmd-x", Cut, context),
        KeyBinding::new("cmd-v", Paste, context),
        KeyBinding::new("cmd-a", SelectAll, context),
        KeyBinding::new("cmd-z", Undo, context),
        KeyBinding::new("cmd-shift-z", Redo, context),
    ]);
}