use std::cell::{Cell, RefCell};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use crate::app::{dom, templates};
use super::{bridge, worker, FB_H, FB_W};
thread_local! {
static POINTER: Cell<(i32, i32)> = const { Cell::new((0, 0)) };
static POINTER_DOWN: Cell<i32> = const { Cell::new(0) };
static ACTIVE_CANVAS_ID: RefCell<String> = RefCell::new(String::from("display-canvas"));
}
pub(super) fn set_active_canvas(canvas_id: &str) {
let previous = ACTIVE_CANVAS_ID.with(|c| c.borrow().clone());
if previous != canvas_id && is_embed_canvas_id(&previous) && is_embed_canvas_id(canvas_id) {
retire_embed_card(&previous);
}
ACTIVE_CANVAS_ID.with(|c| *c.borrow_mut() = canvas_id.to_string());
}
fn is_embed_canvas_id(id: &str) -> bool {
id.starts_with("embed-canvas")
}
fn retire_embed_card(canvas_id: &str) {
let card_id = crate::app::templates::embed_card_id(canvas_id);
crate::app::dom::swap_outer(
&card_id,
&format!("<div id=\"{card_id}\" class=\"embed-card-closed\"></div>"),
);
}
pub(super) fn reset_pointer_down() {
POINTER_DOWN.with(|d| d.set(0));
}
pub(crate) fn close_embed(canvas_id: &str) {
let owns_worker = ACTIVE_CANVAS_ID.with(|c| *c.borrow() == canvas_id);
if owns_worker {
super::stop();
}
retire_embed_card(canvas_id);
}
pub(crate) fn pointer_is_down() -> bool {
POINTER_DOWN.with(|d| d.get()) == 1
}
pub(crate) fn should_track_move(target_id: &str) -> bool {
is_cartridge_canvas_id(target_id) || pointer_is_down()
}
pub(crate) fn set_pointer_down(down: bool) {
POINTER_DOWN.with(|d| d.set(if down { 1 } else { 0 }));
forward_pointer_to_worker();
}
fn forward_pointer_to_worker() {
if worker::is_active() {
let (x, y) = POINTER.with(|p| p.get());
let down = POINTER_DOWN.with(|d| d.get());
worker::post_input(x, y, down);
}
}
pub(crate) fn set_pointer(client_x: f64, client_y: f64) {
let active_id = ACTIVE_CANVAS_ID.with(|c| c.borrow().clone());
let Some(el) = dom::by_id(&active_id) else { return };
let Ok(canvas) = el.dyn_into::<HtmlCanvasElement>() else { return };
let rect = canvas.get_bounding_client_rect();
let (rect_w, rect_h) = (rect.width(), rect.height());
if rect_w <= 0.0 || rect_h <= 0.0 {
return;
}
let fb_w = if canvas.width() > 0 { canvas.width() } else { FB_W };
let fb_h = if canvas.height() > 0 { canvas.height() } else { FB_H };
let fx = ((client_x - rect.left()) * (fb_w as f64 / rect_w)).clamp(0.0, (fb_w - 1) as f64) as i32;
let fy = ((client_y - rect.top()) * (fb_h as f64 / rect_h)).clamp(0.0, (fb_h - 1) as f64) as i32;
POINTER.with(|p| p.set((fx, fy)));
forward_pointer_to_worker();
}
thread_local! {
static LAST_CARTRIDGE: RefCell<Option<Vec<u8>>> = const { RefCell::new(None) };
}
pub(super) fn remember_last_cartridge(wasm: &[u8]) {
LAST_CARTRIDGE.with(|c| *c.borrow_mut() = Some(wasm.to_vec()));
}
pub(crate) async fn relaunch_last_in_fullscreen() {
let Some(wasm) = LAST_CARTRIDGE.with(|c| c.borrow().clone()) else {
crate::app::opfs::toggle_display();
return;
};
if let Err(e) = super::run_wasm(&wasm).await {
embed_trace(&format!("fullscreen relaunch failed: {e:?}"));
}
}
const MAX_PENDING_EMBEDS: usize = 4;
thread_local! {
static PENDING_EMBEDS: RefCell<std::collections::VecDeque<Vec<u8>>> =
const { RefCell::new(std::collections::VecDeque::new()) };
}
pub(crate) fn stash_pending_embed(wasm: Vec<u8>) {
PENDING_EMBEDS.with(|c| {
let mut queue = c.borrow_mut();
if queue.len() >= MAX_PENDING_EMBEDS {
queue.pop_front();
}
queue.push_back(wasm);
});
}
pub(crate) fn reset_pending_embeds() {
PENDING_EMBEDS.with(|c| c.borrow_mut().clear());
}
thread_local! {
static EMBED_CANVAS_SEQ: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
pub(crate) fn next_embed_canvas_id() -> String {
EMBED_CANVAS_SEQ.with(|c| {
let n = c.get().wrapping_add(1);
c.set(n);
format!("embed-canvas-{n}")
})
}
pub(crate) async fn launch_pending_embed(card_id: &str) {
let Some(wasm) = PENDING_EMBEDS.with(|c| c.borrow_mut().pop_front()) else {
embed_trace(&format!("no-stash for #{card_id}"));
return;
};
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
let Ok(Some(el)) = doc.query_selector(&format!("#{card_id} canvas.embed-app-canvas")) else {
embed_trace(&format!("no-canvas inside #{card_id}"));
return;
};
let Ok(canvas) = el.dyn_into::<HtmlCanvasElement>() else { return };
match super::run_in_canvas(canvas, &wasm).await {
Ok(()) => embed_trace(&format!("launched into #{card_id}")),
Err(e) => embed_trace(&format!("run failed in #{card_id}: {e:?}")),
}
}
fn embed_trace(msg: &str) {
web_sys::console::warn_1(&JsValue::from_str(&format!("[embed] {msg}")));
let _ = js_sys::Reflect::set(
&js_sys::global(),
&JsValue::from_str("__lhEmbedTrace"),
&JsValue::from_str(msg),
);
}
pub(crate) fn is_cartridge_canvas_id(id: &str) -> bool {
id == "display-canvas" || id.starts_with("embed-canvas")
}
pub(super) fn mount_canvas() -> Result<CanvasRenderingContext2d, JsValue> {
dom::swap_outer("display-overlay", &templates::display_overlay().into_string());
size_and_get_ctx()
}
pub(crate) fn snapshot_data_url() -> Option<String> {
let canvas = dom::by_id("display-canvas")?
.dyn_into::<HtmlCanvasElement>()
.ok()?;
canvas.to_data_url().ok()
}
pub(super) fn size_and_get_ctx() -> Result<CanvasRenderingContext2d, JsValue> {
let canvas = dom::by_id("display-canvas")
.ok_or_else(|| JsValue::from_str("display-canvas missing"))?
.dyn_into::<HtmlCanvasElement>()?;
canvas.set_width(FB_W);
canvas.set_height(FB_H);
let ctx = canvas
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("no 2d context"))?
.dyn_into::<CanvasRenderingContext2d>()?;
Ok(ctx)
}
pub(super) fn open_broadcast_composer(title: &str, default_body: &str) {
dom::swap_outer(
"broadcast-composer",
&templates::broadcast_composer(title, default_body).into_string(),
);
if let Some(input) = dom::by_id("broadcast-input")
.and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok())
{
let _ = input.focus();
input.select();
}
}
pub(crate) fn broadcast_composer_open() -> bool {
dom::by_id("broadcast-composer")
.map(|el| !el.has_attribute("hidden"))
.unwrap_or(false)
}
pub(crate) fn close_broadcast_composer() {
dom::swap_outer(
"broadcast-composer",
&templates::broadcast_composer_closed().into_string(),
);
}
pub(crate) fn broadcast_send(title: String) {
let body: String = dom::by_id("broadcast-input")
.and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|i| i.value())
.unwrap_or_default()
.trim()
.chars()
.take(200)
.collect();
close_broadcast_composer();
if title.is_empty() {
return;
}
wasm_bindgen_futures::spawn_local(bridge::feed::do_feed_broadcast(title, body));
}