use super::canvas2d::Canvas2DRenderer;
use super::events::{keyboard_event_to_presentar, mouse_event_to_presentar};
use presentar_core::draw::DrawCommand;
use presentar_core::{Brick, Constraints, Event, RecordingCanvas, Rect, Size, Widget};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{window, HtmlCanvasElement, KeyboardEvent, MouseEvent};
#[wasm_bindgen]
pub struct App {
renderer: Canvas2DRenderer,
canvas: HtmlCanvasElement,
width: f32,
height: f32,
click_callback: Option<Closure<dyn FnMut(MouseEvent)>>,
mousemove_callback: Option<Closure<dyn FnMut(MouseEvent)>>,
keydown_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
}
#[wasm_bindgen]
impl App {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<App, JsValue> {
console_error_panic_hook::set_once();
let document = window()
.ok_or("No window")?
.document()
.ok_or("No document")?;
let canvas = document
.get_element_by_id(canvas_id)
.ok_or_else(|| format!("Canvas '{}' not found", canvas_id))?
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| "Element is not a canvas")?;
let width = canvas.width() as f32;
let height = canvas.height() as f32;
let renderer = Canvas2DRenderer::new(canvas.clone()).map_err(|e| JsValue::from_str(&e))?;
Ok(Self {
renderer,
canvas,
width,
height,
click_callback: None,
mousemove_callback: None,
keydown_callback: None,
})
}
pub fn on_click(&mut self, callback: js_sys::Function) {
let cb = Closure::new(move |e: MouseEvent| {
let event = mouse_event_to_presentar(&e, "click");
let json = serde_json::to_string(&event).unwrap_or_default();
let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&json));
});
self.canvas
.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())
.ok();
self.click_callback = Some(cb);
}
pub fn on_mousemove(&mut self, callback: js_sys::Function) {
let cb = Closure::new(move |e: MouseEvent| {
let event = mouse_event_to_presentar(&e, "mousemove");
let json = serde_json::to_string(&event).unwrap_or_default();
let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&json));
});
self.canvas
.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())
.ok();
self.mousemove_callback = Some(cb);
}
pub fn on_keydown(&mut self, callback: js_sys::Function) {
let document = window().and_then(|w| w.document());
if let Some(doc) = document {
let cb = Closure::new(move |e: KeyboardEvent| {
let event = keyboard_event_to_presentar(&e, "keydown");
let json = serde_json::to_string(&event).unwrap_or_default();
let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&json));
});
doc.add_event_listener_with_callback("keydown", cb.as_ref().unchecked_ref())
.ok();
self.keydown_callback = Some(cb);
}
}
pub fn width(&self) -> f32 {
self.width
}
pub fn height(&self) -> f32 {
self.height
}
pub fn clear(&self) {
self.renderer.clear();
}
pub fn render_json(&self, json: &str) -> Result<(), JsValue> {
contract_pre_render!();
contract_post_configuration!(&"ok");
let commands: Vec<DrawCommand> = serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {}", e)))?;
self.renderer.render(&commands);
Ok(())
}
pub fn render_counter(&self, count: i32) {
contract_pre_render!();
contract_post_configuration!(&"ok");
use presentar_core::Color;
use presentar_widgets::{Button, Column, Row, Text};
let mut widget = Column::new()
.gap(20.0)
.child(
Text::new("Counter Demo")
.font_size(28.0)
.color(Color::from_hex("#111827").unwrap_or(Color::BLACK)),
)
.child(
Text::new(&format!("Count: {}", count))
.font_size(48.0)
.color(Color::from_hex("#6366f1").unwrap_or(Color::BLUE)),
)
.child(
Row::new()
.gap(16.0)
.child(Button::new("-").padding(12.0))
.child(Button::new("+").padding(12.0)),
);
self.render_widget_internal(&mut widget);
}
pub fn render_dashboard(&self, title: &str, value: f64, progress: f64) {
contract_pre_render!();
contract_post_configuration!(&"ok");
use presentar_core::Color;
use presentar_widgets::{Column, ProgressBar, Text};
let mut widget = Column::new()
.gap(16.0)
.child(
Text::new(title)
.font_size(24.0)
.color(Color::from_hex("#111827").unwrap_or(Color::BLACK)),
)
.child(
Text::new(&format!("${:.2}M", value / 1_000_000.0))
.font_size(36.0)
.color(Color::from_hex("#059669").unwrap_or(Color::GREEN)),
)
.child(
Column::new()
.gap(8.0)
.child(Text::new("Progress").font_size(14.0))
.child(
ProgressBar::new()
.value(progress as f32)
.fill_color(Color::from_hex("#6366f1").unwrap_or(Color::BLUE)),
),
);
self.render_widget_internal(&mut widget);
}
}
impl App {
fn render_widget_internal<W: Widget>(&self, widget: &mut W) {
if !widget.can_render() {
let verification = widget.verify();
let errors: Vec<String> = verification
.failed
.iter()
.map(|(assertion, reason)| format!("{:?}: {}", assertion, reason))
.collect();
web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&format!(
"JIDOKA: Brick '{}' failed verification - rendering blocked: {}",
widget.brick_name(),
errors.join(", ")
)));
return; }
let constraints = Constraints::loose(Size::new(self.width, self.height));
let size = widget.measure(constraints);
let bounds = Rect::new(0.0, 0.0, size.width, size.height);
widget.layout(bounds);
let mut canvas = RecordingCanvas::new();
widget.paint(&mut canvas);
self.renderer.clear();
self.renderer.render(canvas.commands());
}
pub fn render_widget<W: Widget>(&self, widget: &mut W) {
contract_pre_render!();
contract_post_configuration!(&"ok");
self.render_widget_internal(widget);
}
pub fn render_commands(&self, commands: &[DrawCommand]) {
self.renderer.clear();
self.renderer.render(commands);
}
pub fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub fn log(msg: &str) {
web_sys::console::log_1(&JsValue::from_str(msg));
}