use std::cell::{Cell, RefCell};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use web_sys::{window, HtmlElement, KeyboardEvent};
use crate::scope::Scope;
mod event;
mod health;
mod highlight;
pub mod hooks;
mod inspect;
mod panel;
mod panels;
pub mod ring;
mod router_log;
mod shell;
mod signal_last;
mod style;
mod util;
type RenderClosure = Closure<dyn FnMut()>;
type KeyClosure = Closure<dyn FnMut(KeyboardEvent)>;
thread_local! {
static INSTALLED: Cell<bool> = const { Cell::new(false) };
static INTERVAL_ID: Cell<Option<i32>> = const { Cell::new(None) };
static RENDER_CB: RefCell<Option<RenderClosure>> = RefCell::new(None);
static KEY_CB: RefCell<Option<KeyClosure>> = RefCell::new(None);
}
pub fn install() {
if INSTALLED.with(|c| c.get()) {
return;
}
INSTALLED.with(|c| c.set(true));
style::inject();
attach_key_listener();
register_builtin_panels();
register_default_hooks();
start_render_loop();
}
fn register_builtin_panels() {
panel::register(Box::new(panels::scope::ScopeInspector));
panel::register(Box::new(panels::timeline::Timeline));
panel::register(Box::new(panels::graph::Graph));
panel::register(Box::new(panels::health::Health));
panel::register(Box::new(panels::router::RouterPanel));
}
fn register_default_hooks() {
use std::rc::Rc;
hooks::set_on_effect_run(Rc::new(|id, scope, dur| {
ring::push_effect_run(id, scope, dur);
}));
hooks::set_on_handler_invoke(Rc::new(|scope, name, args, dur| {
let summary = summarise_args(args);
ring::push_handler(scope, name, summary, dur);
}));
hooks::set_on_signal_trigger(Rc::new(|id| {
signal_last::record(id);
}));
hooks::set_on_queue_change(Rc::new(|_added| {}));
hooks::set_on_route_change(Rc::new(|path, params| {
router_log::push(path, params);
}));
}
fn summarise_args(args: &js_sys::Array) -> String {
const MAX_TOTAL: usize = 200;
let n = args.length();
if n == 0 {
return String::new();
}
let mut out = String::new();
for i in 0..n {
if i > 0 {
out.push_str(", ");
}
out.push_str(&util::stringify(&args.get(i)));
if out.chars().count() >= MAX_TOTAL {
break;
}
}
if out.chars().count() > MAX_TOTAL {
out.truncate(
out.char_indices()
.nth(MAX_TOTAL)
.map(|(i, _)| i)
.unwrap_or(out.len()),
);
out.push('…');
}
out
}
fn attach_key_listener() {
let Some(win) = window() else { return };
let cb: KeyClosure = Closure::wrap(Box::new(move |ev: KeyboardEvent| {
if ev.ctrl_key() && ev.shift_key() && ev.key().eq_ignore_ascii_case("d") {
ev.prevent_default();
toggle();
}
}));
let _ = win.add_event_listener_with_callback("keydown", cb.as_ref().unchecked_ref());
KEY_CB.with(|c| *c.borrow_mut() = Some(cb));
}
fn start_render_loop() {
let Some(win) = window() else { return };
let cb: RenderClosure = Closure::wrap(Box::new(move || {
render();
}));
if let Ok(id) =
win.set_interval_with_callback_and_timeout_and_arguments_0(cb.as_ref().unchecked_ref(), 200)
{
INTERVAL_ID.with(|c| c.set(Some(id)));
}
RENDER_CB.with(|c| *c.borrow_mut() = Some(cb));
render();
}
pub fn toggle() {
let next = !shell::is_visible();
shell::set_visible(next);
if let Some(root) = shell::panel_root() {
if let Ok(html_el) = root.clone().dyn_into::<HtmlElement>() {
let style = html_el.style();
let _ = style.set_property("display", if next { "block" } else { "none" });
}
}
}
fn render() {
if !INSTALLED.with(|c| c.get()) {
return;
}
let Some(doc) = window().and_then(|w| w.document()) else {
return;
};
let root = shell::ensure_root(&doc);
let Ok(html_el) = root.clone().dyn_into::<HtmlElement>() else {
return;
};
if !shell::is_visible() {
let _ = html_el.style().set_property("display", "none");
return;
} else {
let _ = html_el.style().remove_property("display");
}
shell::build_shell_once(&root);
shell::update_collapse_state(&root);
shell::update_inspect_button(&root);
shell::update_tab_strip(&root);
if shell::is_collapsed() {
return;
}
health::sample_tick();
let scopes = Scope::all();
shell::update_meta_line(&root, &scopes);
panel::ensure_mounted(|idx, id| shell::host_for_panel(&doc, idx, id));
shell::sync_panel_host_visibility(&doc);
let active_idx = panel::active_index();
if let Some(active_id) = panel::summary()
.into_iter()
.nth(active_idx)
.map(|(id, _, _)| id)
{
let host_id = format!("__pp_dev_panel_{active_id}");
if let Some(host) = doc.get_element_by_id(&host_id) {
panel::render_active(&host);
}
}
}