use std::cell::Cell;
use wasm_bindgen::prelude::*;
use web_sys::{window, Document, Element, HtmlElement};
use super::{event, panel};
use crate::scope::Scope;
pub(super) const ROOT_ID: &str = "__pp_devtools_root";
pub(super) const META_ID: &str = "__pp_dev_meta";
pub(super) const INSPECT_BTN_ID: &str = "__pp_dev_btn_inspect";
pub(super) const BODY_ID: &str = "__pp_dev_body";
pub(super) const TABS_ID: &str = "__pp_dev_tabs";
thread_local! {
static VISIBLE: Cell<bool> = const { Cell::new(true) };
static SHELL_BUILT: Cell<bool> = const { Cell::new(false) };
static COLLAPSED: Cell<bool> = const { Cell::new(false) };
static LAST_META: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
static LAST_INSPECT_RENDERED: Cell<bool> = const { Cell::new(false) };
static LAST_COLLAPSED_RENDERED: Cell<Option<bool>> = const { Cell::new(None) };
static LAST_TABS_FP: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
}
pub(super) fn is_visible() -> bool {
VISIBLE.with(|c| c.get())
}
pub(super) fn set_visible(v: bool) {
VISIBLE.with(|c| c.set(v));
}
pub(super) fn toggle_collapse() {
COLLAPSED.with(|c| c.set(!c.get()));
}
pub(super) fn is_collapsed() -> bool {
COLLAPSED.with(|c| c.get())
}
pub(super) fn panel_root() -> Option<Element> {
window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id(ROOT_ID))
}
pub(super) fn ensure_root(doc: &Document) -> Element {
if let Some(el) = doc.get_element_by_id(ROOT_ID) {
return el;
}
let el = doc.create_element("div").expect("create_element div");
let _ = el.set_attribute("id", ROOT_ID);
if let Some(body) = doc.body() {
let _ = body.append_child(&el);
}
event::attach(&el);
el
}
pub(super) fn build_shell_once(root: &Element) {
if SHELL_BUILT.with(|c| c.get()) {
return;
}
let shell = format!(
"<header class=\"__pp_dev_header\">\
<span>pocopine devtools</span>\
<div class=\"__pp_dev_actions\">\
<button id=\"{btn}\" class=\"__pp_dev_btn\" data-action=\"toggle-inspect\">\
inspect\
</button>\
<button class=\"__pp_dev_btn __pp_dev_collapse\" data-action=\"toggle-collapse\" \
title=\"collapse\">−</button>\
<button class=\"__pp_dev_btn __pp_dev_close\" data-action=\"close\">×</button>\
</div>\
</header>\
<div id=\"{tabs}\" class=\"__pp_dev_tabs\" style=\"display:none\"></div>\
<div id=\"{meta}\" class=\"__pp_dev_meta\"></div>\
<div id=\"{body}\" class=\"__pp_dev_body\"></div>",
btn = INSPECT_BTN_ID,
tabs = TABS_ID,
meta = META_ID,
body = BODY_ID,
);
root.set_inner_html(&shell);
SHELL_BUILT.with(|c| c.set(true));
}
pub(super) fn update_collapse_state(root: &Element) {
let collapsed = is_collapsed();
let prev = LAST_COLLAPSED_RENDERED.with(|c| c.get());
if prev == Some(collapsed) {
return;
}
if collapsed {
let _ = root.set_attribute("data-collapsed", "true");
} else {
let _ = root.remove_attribute("data-collapsed");
}
if let Some(btn) = root.query_selector(".__pp_dev_collapse").ok().flatten() {
btn.set_text_content(Some(if collapsed { "+" } else { "−" }));
let _ = btn.set_attribute("title", if collapsed { "expand" } else { "collapse" });
}
LAST_COLLAPSED_RENDERED.with(|c| c.set(Some(collapsed)));
}
pub(super) fn update_inspect_button(root: &Element) {
let on = super::inspect::is_on();
let prev = LAST_INSPECT_RENDERED.with(|c| c.get());
if on == prev {
return;
}
if let Some(btn) = root
.query_selector(&format!("#{INSPECT_BTN_ID}"))
.ok()
.flatten()
{
if on {
let _ = btn.set_attribute("class", "__pp_dev_btn __pp_dev_btn_on");
btn.set_text_content(Some("inspecting…"));
} else {
let _ = btn.set_attribute("class", "__pp_dev_btn");
btn.set_text_content(Some("inspect"));
}
}
LAST_INSPECT_RENDERED.with(|c| c.set(on));
}
pub(super) fn update_meta_line(root: &Element, scopes: &[Scope]) {
let meta_text = format!("{} scopes", scopes.len());
let needs_write = LAST_META.with(|c| {
let mut cur = c.borrow_mut();
if *cur != meta_text {
*cur = meta_text.clone();
true
} else {
false
}
});
if needs_write {
if let Some(meta) = root.query_selector(&format!("#{META_ID}")).ok().flatten() {
meta.set_text_content(Some(&meta_text));
}
}
}
pub(super) fn update_tab_strip(root: &Element) {
let summary = panel::summary();
if summary.len() < 2 {
if let Some(tabs) = root.query_selector(&format!("#{TABS_ID}")).ok().flatten() {
if let Ok(html_el) = tabs.dyn_into::<HtmlElement>() {
let _ = html_el.style().set_property("display", "none");
}
}
return;
}
let fp = summary
.iter()
.map(|(id, _, active)| format!("{id}:{active}"))
.collect::<Vec<_>>()
.join(";");
let changed = LAST_TABS_FP.with(|c| {
let mut cur = c.borrow_mut();
if *cur != fp {
*cur = fp;
true
} else {
false
}
});
if !changed {
return;
}
let Some(tabs) = root.query_selector(&format!("#{TABS_ID}")).ok().flatten() else {
return;
};
if let Ok(html_el) = tabs.clone().dyn_into::<HtmlElement>() {
let _ = html_el.style().remove_property("display");
}
let mut html = String::new();
for (idx, (id, label, active)) in summary.iter().enumerate() {
let cls = if *active {
"__pp_dev_seg_btn __pp_dev_seg_btn_on"
} else {
"__pp_dev_seg_btn"
};
html.push_str(&format!(
"<button class=\"{cls}\" data-action=\"select-panel\" \
data-panel-idx=\"{idx}\" data-panel-id=\"{id}\">{label}</button>"
));
}
tabs.set_inner_html(&format!("<div class=\"__pp_dev_seg\">{html}</div>"));
}
pub(super) fn host_for_panel(doc: &Document, idx: usize, panel_id: &str) -> Option<Element> {
let body = doc.get_element_by_id(BODY_ID)?;
let host_id = format!("__pp_dev_panel_{panel_id}");
if let Some(el) = doc.get_element_by_id(&host_id) {
update_host_visibility(&el, idx);
return Some(el);
}
let el = doc.create_element("div").ok()?;
let _ = el.set_attribute("id", &host_id);
let _ = el.set_attribute("class", "__pp_dev_panel_host");
update_host_visibility(&el, idx);
let _ = body.append_child(&el);
Some(el)
}
fn update_host_visibility(el: &Element, idx: usize) {
let active = panel::active_index();
if let Ok(html) = el.clone().dyn_into::<HtmlElement>() {
let style = html.style();
if idx == active {
let _ = style.remove_property("display");
} else {
let _ = style.set_property("display", "none");
}
}
}
pub(super) fn sync_panel_host_visibility(doc: &Document) {
let summary = panel::summary();
for (idx, (id, _, _)) in summary.iter().enumerate() {
let host_id = format!("__pp_dev_panel_{id}");
if let Some(el) = doc.get_element_by_id(&host_id) {
update_host_visibility(&el, idx);
}
}
}