use std::{cell::RefCell, collections::HashMap};
use cranpose_ui::{CustomPointerIcon, PointerIcon};
use wasm_bindgen::{Clamped, JsCast, JsValue};
use web_sys::{Document, HtmlCanvasElement, HtmlElement, ImageData};
pub(crate) fn sync_pointer_icon(
cursors: &RefCell<WebCursors>,
document: &Document,
canvas: &HtmlCanvasElement,
icon: Option<PointerIcon>,
) {
let Some(icon) = icon else {
return;
};
cursors.borrow_mut().apply(document, canvas, &icon);
}
#[derive(Default)]
pub(crate) struct WebCursors {
encoded: HashMap<u64, String>,
applied: Option<String>,
}
impl WebCursors {
pub(crate) fn apply(
&mut self,
document: &Document,
canvas: &HtmlCanvasElement,
icon: &PointerIcon,
) {
let value = match icon {
PointerIcon::System(system) => system.name().to_string(),
PointerIcon::Custom(custom) => self
.css_url(document, custom)
.unwrap_or_else(|| "default".to_string()),
};
if self.applied.as_deref() == Some(value.as_str()) {
return;
}
let Some(element) = canvas.dyn_ref::<HtmlElement>() else {
return;
};
if element.style().set_property("cursor", &value).is_ok() {
self.applied = Some(value);
}
}
fn css_url(&mut self, document: &Document, custom: &CustomPointerIcon) -> Option<String> {
let id = custom.id();
if let Some(value) = self.encoded.get(&id) {
return Some(value.clone());
}
let data_url = match encode_png_data_url(document, custom) {
Ok(data_url) => data_url,
Err(error) => {
log::warn!("a custom cursor could not be encoded for CSS: {error:?}");
return None;
}
};
let value = format!(
"url(\"{data_url}\") {} {}, default",
custom.hotspot_x(),
custom.hotspot_y()
);
self.encoded.insert(id, value.clone());
Some(value)
}
}
fn encode_png_data_url(document: &Document, custom: &CustomPointerIcon) -> Result<String, JsValue> {
let image = custom.image();
let scratch = document
.create_element("canvas")?
.dyn_into::<HtmlCanvasElement>()?;
scratch.set_width(image.width());
scratch.set_height(image.height());
let context = scratch
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("the browser gave no 2d canvas context"))?
.dyn_into::<web_sys::CanvasRenderingContext2d>()?;
let data = ImageData::new_with_u8_clamped_array_and_sh(
Clamped(image.pixels()),
image.width(),
image.height(),
)?;
context.put_image_data(&data, 0.0, 0.0)?;
scratch.to_data_url_with_type("image/png")
}