use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use wasm_bindgen::prelude::*;
use crate::{epi, App};
use super::{events, AppRunner, PanicHandler};
#[derive(Clone)]
pub struct WebRunner {
panic_handler: PanicHandler,
runner: Rc<RefCell<Option<AppRunner>>>,
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
request_animation_frame_id: Cell<Option<i32>>,
}
impl WebRunner {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
#[cfg(not(web_sys_unstable_apis))]
log::warn!(
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
);
let panic_handler = PanicHandler::install();
Self {
panic_handler,
runner: Rc::new(RefCell::new(None)),
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
request_animation_frame_id: Cell::new(None),
}
}
pub async fn start(
&self,
canvas_id: &str,
web_options: crate::WebOptions,
app_creator: epi::AppCreator,
) -> Result<(), JsValue> {
self.destroy();
let follow_system_theme = web_options.follow_system_theme;
let runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
self.runner.replace(Some(runner));
{
events::install_canvas_events(self)?;
events::install_document_events(self)?;
events::install_window_events(self)?;
super::text_agent::install_text_agent(self)?;
if follow_system_theme {
events::install_color_scheme_change_event(self)?;
}
self.request_animation_frame()?;
}
Ok(())
}
pub fn has_panicked(&self) -> bool {
self.panic_handler.has_panicked()
}
pub fn panic_summary(&self) -> Option<super::PanicSummary> {
self.panic_handler.panic_summary()
}
fn unsubscribe_from_all_events(&self) {
let events_to_unsubscribe: Vec<_> =
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
if !events_to_unsubscribe.is_empty() {
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
for x in events_to_unsubscribe {
if let Err(err) = x.unsubscribe() {
log::warn!(
"Failed to unsubscribe from event: {}",
super::string_from_js_value(&err)
);
}
}
}
}
pub fn destroy(&self) {
self.unsubscribe_from_all_events();
if let Some(id) = self.request_animation_frame_id.get() {
let window = web_sys::window().unwrap();
window.cancel_animation_frame(id).ok();
}
if let Some(runner) = self.runner.replace(None) {
runner.destroy();
}
}
pub(crate) fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
if self.panic_handler.has_panicked() {
self.unsubscribe_from_all_events();
None
} else {
let lock = self.runner.try_borrow_mut().ok()?;
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
.ok()
}
}
pub fn app_mut<ConcreteApp: 'static + App>(
&self,
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
self.try_lock()
.map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
}
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&self,
target: &web_sys::EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, &mut AppRunner) + 'static,
) -> Result<(), wasm_bindgen::JsValue> {
let runner_ref = self.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
if let Some(mut runner_lock) = runner_ref.try_lock() {
let event = event.unchecked_into::<E>();
closure(event, &mut runner_lock);
}
}) as Box<dyn FnMut(web_sys::Event)>);
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
closure,
};
self.events_to_unsubscribe
.borrow_mut()
.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::once({
let runner_ref = self.clone();
move || events::paint_and_schedule(&runner_ref)
});
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
self.request_animation_frame_id.set(Some(id));
closure.forget(); Ok(())
}
}
struct TargetEvent {
target: web_sys::EventTarget,
event_name: String,
closure: Closure<dyn FnMut(web_sys::Event)>,
}
#[allow(unused)]
struct IntervalHandle {
handle: i32,
closure: Closure<dyn FnMut()>,
}
enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
IntervalHandle(IntervalHandle),
}
impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> {
match self {
Self::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback(
handle.event_name.as_str(),
handle.closure.as_ref().unchecked_ref(),
)?;
Ok(())
}
Self::IntervalHandle(handle) => {
let window = web_sys::window().unwrap();
window.clear_interval_with_handle(handle.handle);
Ok(())
}
}
}
}