use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use wasm_bindgen::{prelude::*, Clamped, JsCast};
use web_sys::{Document, HtmlCanvasElement, HtmlElement, Window as WebWindow};
use crate::color::Color;
use crate::event::*;
use crate::renderer::Renderer;
use crate::Mode;
use crate::WindowFlag;
pub fn get_display_size() -> Result<(u32, u32), String> {
let width = window()?
.inner_width()
.map_err(|_| "Cannot read display width.".to_string())?
.as_f64()
.unwrap_or(0.0) as u32;
let height = window()?
.inner_height()
.map_err(|_| "Cannot read display height.".to_string())?
.as_f64()
.unwrap_or(0.0) as u32;
Ok((width, height))
}
#[allow(dead_code)]
pub struct Window {
x: i32,
y: i32,
w: u32,
h: u32,
t: String,
window_async: bool,
mode: Cell<Mode>,
mouse_relative: Rc<RefCell<bool>>,
drop_content: Rc<RefCell<Option<String>>>,
data: Vec<Color>,
canvas: web_sys::HtmlCanvasElement,
context: web_sys::CanvasRenderingContext2d,
events: Rc<RefCell<Vec<Event>>>,
button_state: Rc<RefCell<(bool, bool, bool)>>,
}
impl Renderer for Window {
fn width(&self) -> u32 {
self.w
}
fn height(&self) -> u32 {
self.h
}
fn data(&self) -> &[Color] {
&self.data
}
fn data_mut(&mut self) -> &mut [Color] {
&mut self.data
}
fn sync(&mut self) -> bool {
let bytes = self.data_mut();
let len = bytes.len() * std::mem::size_of::<Color>();
let color_data =
unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u8, len) };
if let Ok(image_data) =
web_sys::ImageData::new_with_u8_clamped_array(Clamped(color_data), self.canvas.width())
{
self.context.put_image_data(&image_data, 0.0, 0.0).unwrap();
}
true
}
fn mode(&self) -> &Cell<Mode> {
&self.mode
}
}
impl Window {
pub fn new(x: i32, y: i32, w: u32, h: u32, title: &str) -> Option<Self> {
Window::new_flags(x, y, w, h, title, &vec![])
}
pub fn new_flags(
x: i32,
y: i32,
w: u32,
h: u32,
title: &str,
_flags: &[WindowFlag],
) -> Option<Self> {
if let Ok(document) = document() {
document.set_title(title);
}
if let Ok(body) = body() {
body.style().set_property("padding", "0").unwrap();
body.style().set_property("margin", "0").unwrap();
}
if let Ok(canvas) = canvas("canvas") {
canvas.style().set_property("display", "block").unwrap();
canvas.style().set_property("margin", "0").unwrap();
canvas.style().set_property("position", "absolute").unwrap();
canvas
.style()
.set_property("left", x.to_string().as_str())
.unwrap();
canvas
.style()
.set_property("top", y.to_string().as_str())
.unwrap();
canvas.set_width(w);
canvas.set_height(h);
let events = Rc::new(RefCell::new(vec![]));
let button_state = Rc::new(RefCell::new((false, false, false)));
let mouse_relative = Rc::new(RefCell::new(false));
let drop_content = Rc::new(RefCell::new(None));
connect_event_handlers(
mouse_relative.clone(),
&canvas,
&events,
&button_state,
&drop_content,
);
if let Ok(context) = context_2d(&canvas) {
return Some(Window {
x,
y,
w,
h,
t: title.to_string(),
window_async: false,
mode: Cell::new(Mode::Blend),
mouse_relative,
drop_content,
data: vec![Color::rgb(255, 255, 255); (w * h) as usize],
canvas,
context,
events,
button_state,
});
}
}
None
}
pub fn clipboard(&self) -> String {
log("clipboard not yet implemented for web");
String::default()
}
pub fn set_clipboard(&mut self, _text: &str) {
log("set_clipboard not yet implemented for web");
}
pub fn pop_drop_content(&self) -> Option<String> {
let result = self.drop_content.borrow().clone();
*self.drop_content.borrow_mut() = None;
result
}
pub fn sync_path(&mut self) {
self.x = self.canvas.offset_left();
self.y = self.canvas.offset_top();
self.w = self.canvas.width();
self.h = self.canvas.height();
if let Ok(document) = document() {
self.t = document.title();
}
}
pub fn x(&self) -> i32 {
self.x
}
pub fn y(&self) -> i32 {
self.y
}
pub fn title(&self) -> String {
self.t.clone()
}
pub fn set_mouse_cursor(&mut self, visible: bool) {
if let Ok(body) = body() {
if visible {
body.style().set_property("cursor", "auto").unwrap();
} else {
body.style().set_property("cursor", "none").unwrap();
}
}
}
pub fn set_mouse_grab(&mut self, grab: bool) {
if grab {
self.canvas.set_capture();
} else {
self.canvas.release_capture();
}
}
pub fn set_mouse_relative(&mut self, relative: bool) {
*self.mouse_relative.borrow_mut() = relative;
}
pub fn set_pos(&mut self, x: i32, y: i32) {
self.canvas
.style()
.set_property("left", x.to_string().as_str())
.unwrap();
self.canvas
.style()
.set_property("top", y.to_string().as_str())
.unwrap();
self.sync_path();
}
pub fn set_size(&mut self, width: u32, height: u32) {
self.canvas.set_width(width);
self.canvas.set_height(height);
self.sync_path();
}
pub fn set_title(&mut self, title: &str) {
if let Ok(document) = document() {
document.set_title(title);
}
self.sync_path();
}
pub fn events(&mut self) -> EventIter {
let mut iter = EventIter {
events: [Event::new(); 16],
i: 0,
count: 0,
};
let mut sync_path = false;
for event in self.events.borrow_mut().pop() {
match event.code {
EVENT_RESIZE | EVENT_MOVE => sync_path = true,
_ => {}
}
if iter.count < iter.events.len() {
iter.events[iter.count] = event;
iter.count += 1;
} else {
break;
}
}
if sync_path {
self.sync_path();
}
iter
}
}
pub struct EventIter {
events: [Event; 16],
i: usize,
count: usize,
}
impl Iterator for EventIter {
type Item = Event;
fn next(&mut self) -> Option<Event> {
if self.i < self.count {
if let Some(event) = self.events.get(self.i) {
self.i += 1;
Some(*event)
} else {
None
}
} else {
None
}
}
}
fn convert_code(code: String, shift: bool) -> Option<(char, u8)> {
match code.as_str() {
"KeyA" => Some((if shift { 'A' } else { 'a' }, K_A)),
"KeyB" => Some((if shift { 'B' } else { 'b' }, K_B)),
"KeyC" => Some((if shift { 'C' } else { 'c' }, K_C)),
"KeyD" => Some((if shift { 'D' } else { 'd' }, K_D)),
"KeyE" => Some((if shift { 'E' } else { 'e' }, K_E)),
"KeyF" => Some((if shift { 'F' } else { 'f' }, K_F)),
"KeyG" => Some((if shift { 'G' } else { 'g' }, K_G)),
"KeyH" => Some((if shift { 'H' } else { 'h' }, K_H)),
"KeyI" => Some((if shift { 'I' } else { 'i' }, K_I)),
"KeyJ" => Some((if shift { 'J' } else { 'j' }, K_J)),
"KeyK" => Some((if shift { 'K' } else { 'k' }, K_K)),
"KeyL" => Some((if shift { 'L' } else { 'l' }, K_L)),
"KeyM" => Some((if shift { 'M' } else { 'm' }, K_M)),
"KeyN" => Some((if shift { 'N' } else { 'n' }, K_N)),
"KeyO" => Some((if shift { 'O' } else { 'o' }, K_O)),
"KeyP" => Some((if shift { 'P' } else { 'p' }, K_P)),
"KeyQ" => Some((if shift { 'Q' } else { 'q' }, K_Q)),
"KeyR" => Some((if shift { 'R' } else { 'r' }, K_R)),
"KeyS" => Some((if shift { 'S' } else { 's' }, K_S)),
"KeyT" => Some((if shift { 'T' } else { 't' }, K_T)),
"KeyU" => Some((if shift { 'U' } else { 'u' }, K_U)),
"KeyV" => Some((if shift { 'V' } else { 'v' }, K_V)),
"KeyW" => Some((if shift { 'W' } else { 'w' }, K_W)),
"KeyX" => Some((if shift { 'X' } else { 'x' }, K_X)),
"KeyY" => Some((if shift { 'Y' } else { 'y' }, K_Y)),
"KeyZ" => Some((if shift { 'Z' } else { 'z' }, K_Z)),
"Digit0" => Some((if shift { ')' } else { '0' }, K_0)),
"Digit1" => Some((if shift { '!' } else { '1' }, K_1)),
"Digit2" => Some((if shift { '@' } else { '2' }, K_2)),
"Digit3" => Some((if shift { '#' } else { '3' }, K_3)),
"Digit4" => Some((if shift { '$' } else { '4' }, K_4)),
"Digit5" => Some((if shift { '%' } else { '5' }, K_5)),
"Digit6" => Some((if shift { '^' } else { '6' }, K_6)),
"Digit7" => Some((if shift { '&' } else { '7' }, K_7)),
"Digit8" => Some((if shift { '*' } else { '8' }, K_8)),
"Digit9" => Some((if shift { '(' } else { '9' }, K_9)),
"Grave" => Some((if shift { '~' } else { '`' }, K_TICK)),
"Subtract" => Some((if shift { '_' } else { '-' }, K_MINUS)),
"Equals" => Some((if shift { '+' } else { '=' }, K_EQUALS)),
"BracketLeft" => Some((if shift { '{' } else { '[' }, K_BRACE_OPEN)),
"BracketRight" => Some((if shift { '}' } else { ']' }, K_BRACE_CLOSE)),
"Backslash" => Some((if shift { '|' } else { '\\' }, K_BACKSLASH)),
"Semicolon" => Some((if shift { ':' } else { ';' }, K_SEMICOLON)),
"Apostrophe" => Some((if shift { '"' } else { '\'' }, K_QUOTE)),
"Comma" => Some((if shift { '<' } else { ',' }, K_COMMA)),
"Period" => Some((if shift { '>' } else { '.' }, K_PERIOD)),
"Slash" => Some((if shift { '?' } else { '/' }, K_SLASH)),
"Space" => Some((' ', K_SPACE)),
"Backspace" => Some(('\0', K_BKSP)),
"Tab" => Some(('\t', K_TAB)),
"ControlLeft" => Some(('\0', K_CTRL)),
"ControlRight" => Some(('\0', K_CTRL)),
"AltLeft" => Some(('\0', K_ALT)),
"AltRight" => Some(('\0', K_ALT)),
"Enter" => Some(('\n', K_ENTER)),
"Escape" => Some(('\x1B', K_ESC)),
"F1" => Some(('\0', K_F1)),
"F2" => Some(('\0', K_F2)),
"F3" => Some(('\0', K_F3)),
"F4" => Some(('\0', K_F4)),
"F5" => Some(('\0', K_F5)),
"F6" => Some(('\0', K_F6)),
"F7" => Some(('\0', K_F7)),
"F8" => Some(('\0', K_F8)),
"F9" => Some(('\0', K_F9)),
"F10" => Some(('\0', K_F10)),
"OSLeft" => Some(('\0', K_HOME)),
"OSRight" => Some(('\0', K_HOME)),
"ArrowUp" => Some(('\0', K_UP)),
"PageUp" => Some(('\0', K_PGUP)),
"ArrowLeft" => Some(('\0', K_LEFT)),
"ArrowRight" => Some(('\0', K_RIGHT)),
"End" => Some(('\0', K_END)),
"ArrowDown" => Some(('\0', K_DOWN)),
"PageDown" => Some(('\0', K_PGDN)),
"Delete" => Some(('\0', K_DEL)),
"F11" => Some(('\0', K_F11)),
"F12" => Some(('\0', K_F12)),
"ShiftLeft" => Some(('\0', K_LEFT_SHIFT)),
"ShiftRight" => Some(('\0', K_RIGHT_SHIFT)),
_ => None,
}
}
pub fn window() -> Result<WebWindow, String> {
web_sys::window().ok_or("body: no global `windows` exists.".to_string())
}
pub fn document() -> Result<Document, String> {
window()?
.document()
.ok_or("body: should have a document on window.".to_string())
}
pub fn body() -> Result<HtmlElement, String> {
document()?
.body()
.ok_or("body: document should have a body.".to_string())
}
pub fn canvas(id: &str) -> Result<HtmlCanvasElement, String> {
let canvas = {
if let Some(canvas) = document()?.get_element_by_id(id) {
Some(canvas)
} else {
if let Ok(canvas) = document()?.create_element("canvas") {
canvas.set_id(id);
if let Err(_) = body()?.append_child(&canvas) {
return Err("canvas: Could not add canvas to body.".to_string());
}
Some(canvas)
} else {
None
}
}
};
canvas.map_or(Err("canvas: Could not create canvas.".to_string()), |c| {
c.dyn_into::<HtmlCanvasElement>()
.map_err(|_| "canvas: Could not convert canvas.".to_string())
})
}
pub fn context_2d(canvas: &HtmlCanvasElement) -> Result<web_sys::CanvasRenderingContext2d, String> {
canvas
.get_context("2d")
.map_err(|_| "context_2d: Could not get 2d context of canvas".to_string())?
.ok_or("context_2d: Could not get 2d context of canvas".to_string())?
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.map_err(|_| "CanvasDisplay::new: Could not convert canvas context_2.".to_string())
}
pub fn log(msg: impl Into<JsValue>) {
web_sys::console::log_1(&msg.into());
}
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
if let Ok(window) = window() {
window
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK");
}
}
pub fn animation_loop<R: FnMut() -> bool + 'static>(mut run: R) {
let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
if !run() {
return;
}
request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
request_animation_frame(g.borrow().as_ref().unwrap());
}
fn connect_event_handlers(
mouse_relative: Rc<RefCell<bool>>,
canvas: &HtmlCanvasElement,
events: &Rc<RefCell<Vec<Event>>>,
button_state: &Rc<RefCell<(bool, bool, bool)>>,
drop_content: &Rc<RefCell<Option<String>>>,
) {
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| {
if *mouse_relative.borrow() {
events.borrow_mut().push(
MouseEvent {
x: e.movement_x(),
y: e.movement_y(),
}
.to_event(),
);
} else {
events.borrow_mut().push(
MouseEvent {
x: e.offset_x(),
y: e.offset_y(),
}
.to_event(),
);
}
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let button_state = button_state.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| {
*button_state.borrow_mut() = (e.button() == 0, e.button() == 1, e.button() == 2);
events.borrow_mut().push(
ButtonEvent {
left: e.button() == 0,
middle: e.button() == 1,
right: e.button() == 2,
}
.to_event(),
);
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let button_state = button_state.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| {
*button_state.borrow_mut() = (
!e.button() == 0 && button_state.borrow().0,
!e.button() == 1 && button_state.borrow().1,
!e.button() == 2 && button_state.borrow().2,
);
events.borrow_mut().push(
ButtonEvent {
left: button_state.borrow().0,
middle: button_state.borrow().1,
right: button_state.borrow().2,
}
.to_event(),
);
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::KeyboardEvent| {
if !e.repeat() && e.key().len() <= 2 {
events.borrow_mut().push(
TextInputEvent {
character: e.key().chars().nth(0).unwrap(),
}
.to_event(),
);
}
if let Some(code) = convert_code(e.code(), e.shift_key()) {
events.borrow_mut().push(
KeyEvent {
character: code.0,
scancode: code.1,
pressed: true,
}
.to_event(),
);
}
}) as Box<dyn FnMut(_)>);
document()
.unwrap()
.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::KeyboardEvent| {
if let Some(code) = convert_code(e.code(), e.shift_key()) {
events.borrow_mut().push(
KeyEvent {
character: code.0,
scancode: code.1,
pressed: false,
}
.to_event(),
);
}
}) as Box<dyn FnMut(_)>);
document()
.unwrap()
.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::WheelEvent| {
events.borrow_mut().push(
ScrollEvent {
x: e.delta_x() as i32,
y: e.delta_y() as i32,
}
.to_event(),
)
}) as Box<dyn FnMut(_)>);
document()
.unwrap()
.add_event_listener_with_callback("wheel", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::FocusEvent| {
events
.borrow_mut()
.push(FocusEvent { focused: true }.to_event())
}) as Box<dyn FnMut(_)>);
document()
.unwrap()
.add_event_listener_with_callback("focus", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::FocusEvent| {
events
.borrow_mut()
.push(FocusEvent { focused: false }.to_event())
}) as Box<dyn FnMut(_)>);
document()
.unwrap()
.add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::MouseEvent| {
events
.borrow_mut()
.push(HoverEvent { entered: true }.to_event());
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("mouseenter", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::MouseEvent| {
events
.borrow_mut()
.push(HoverEvent { entered: false }.to_event());
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("mouseleave", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let drop_content = drop_content.clone();
let closure = Closure::wrap(Box::new(move |e: web_sys::DragEvent| {
if let Some(data_transfer) = e.data_transfer() {
if let Ok(text) = data_transfer.get_data("text") {
*drop_content.borrow_mut() = Some(text);
events
.borrow_mut()
.push(DropEvent { kind: DROP_TEXT }.to_event());
}
}
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("drop", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
{
let events = events.clone();
let canvas_clone = canvas.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::UiEvent| {
events.borrow_mut().push(
ResizeEvent {
width: canvas_clone.width(),
height: canvas_clone.height(),
}
.to_event(),
);
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
}