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};
pub struct WebView {
page: Rc<Page>,
location: Option<String>,
title: String,
loading: bool,
history: (bool, bool),
focus_window: Option<AnyWindowHandle>,
_focus: [Subscription; 2],
_reports: Task<()>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WebViewEvent {
Location(String),
Title(String),
History {
back: bool,
forward: bool,
},
Load(LoadState),
NewWindow(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoadState {
Started,
Finished,
}
#[derive(Debug)]
pub enum EvalError {
Unavailable,
Timeout,
Script,
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();
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,
}
}
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()),
]
}
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
pub fn title(&self) -> &str {
&self.title
}
pub fn can_go_back(&self) -> bool {
self.history.0
}
pub fn can_go_forward(&self) -> bool {
self.history.1
}
pub fn is_loading(&self) -> bool {
self.loading
}
pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
*self.page.user_agent.borrow_mut() = Some(user_agent.into());
self
}
pub fn with_data_store(self, store: DataStore) -> Self {
*self.page.store.borrow_mut() = store;
self
}
pub fn load(&mut self, url: impl Into<String>) {
self.page.load(url.into());
}
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();
}
pub fn reload_bypassing_cache(&mut self) {
self.page.reload_bypassing_cache();
}
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();
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 {
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);
}
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::Still(still) => {
self.page.captured(still);
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))
}
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),
]);
}