1mod input;
2
3use grid_2d::Coord;
4pub use grid_2d::Size;
5use js_sys::Function;
6use prototty_app::App;
7use prototty_audio::{AudioPlayer, AudioProperties};
8pub use prototty_input;
9pub use prototty_input::{Input, MouseInput};
10use prototty_input::{MouseButton, ScrollDirection};
11pub use prototty_render;
12use prototty_render::{Buffer, Rgb24, ViewContext};
13use prototty_storage::*;
14use std::cell::RefCell;
15use std::rc::Rc;
16pub use std::time::Duration;
17use wasm_bindgen::prelude::*;
18use wasm_bindgen::JsCast;
19use web_sys::{Element, HtmlElement, KeyboardEvent, MouseEvent, Node, WheelEvent};
20
21fn rgb24_to_web_colour(Rgb24 { r, g, b }: Rgb24) -> String {
22 format!("rgb({},{},{})", r, g, b)
23}
24
25struct ElementCell {
26 element: HtmlElement,
27 character: char,
28 bold: bool,
29 underline: bool,
30 foreground_colour: Rgb24,
31 background_colour: Rgb24,
32}
33
34impl ElementCell {
35 fn with_element(element: HtmlElement) -> Self {
36 element.set_inner_html(" ");
37 let element_style = element.style();
38 element_style.set_property("color", "rgb(255,255,255)").unwrap();
39 element_style.set_property("background-color", "rgb(0,0,0)").unwrap();
40 Self {
41 element,
42 character: ' ',
43 bold: false,
44 underline: false,
45 foreground_colour: Rgb24::new_grey(0),
46 background_colour: Rgb24::new_grey(0),
47 }
48 }
49}
50
51#[derive(Debug)]
52struct ElementDisplayInfo {
53 container_x: f64,
54 container_y: f64,
55 cell_width: f64,
56 cell_height: f64,
57}
58
59impl ElementDisplayInfo {
60 fn mouse_coord(&self, x: i32, y: i32) -> Coord {
61 let x = (x - self.container_x as i32) / self.cell_width as i32;
62 let y = (y - self.container_y as i32) / self.cell_height as i32;
63 Coord::new(x, y)
64 }
65}
66
67pub struct Context {
68 element_grid: grid_2d::Grid<ElementCell>,
69 buffer: Buffer,
70 container_element: Element,
71}
72
73impl Context {
74 fn element_display_info(&self) -> ElementDisplayInfo {
75 let container_rect = self.container_element.get_bounding_client_rect();
76 let (container_x, container_y) = (container_rect.x(), container_rect.y());
77 let cell_element = self
78 .element_grid
79 .get_index_checked(0)
80 .element
81 .dyn_ref::<Element>()
82 .unwrap();
83 let cell_rect = cell_element.get_bounding_client_rect();
84 let (cell_width, cell_height) = (cell_rect.width(), cell_rect.height());
85 ElementDisplayInfo {
86 container_x,
87 container_y,
88 cell_width,
89 cell_height,
90 }
91 }
92 pub fn new(size: Size, container: &str) -> Self {
93 if size.width() == 0 || size.height() == 0 {
94 panic!("Size must not be zero");
95 }
96 let window = web_sys::window().unwrap();
97 let document = window.document().unwrap();
98 let container_node = document
99 .get_element_by_id(container)
100 .unwrap()
101 .dyn_into::<Node>()
102 .unwrap();
103 let element_grid = grid_2d::Grid::new_fn(size, |_| {
104 let element = document
105 .create_element("span")
106 .unwrap()
107 .dyn_into::<HtmlElement>()
108 .unwrap();
109 ElementCell::with_element(element)
110 });
111 for y in 0..size.height() {
112 for x in 0..size.width() {
113 container_node
114 .append_child(&element_grid.get_checked(Coord::new(x as i32, y as i32)).element)
115 .unwrap();
116 }
117 container_node
118 .append_child(document.create_element("br").unwrap().dyn_ref::<HtmlElement>().unwrap())
119 .unwrap();
120 }
121 let buffer = Buffer::new(size);
122 Self {
123 element_grid,
124 buffer,
125 container_element: document.get_element_by_id(container).unwrap(),
126 }
127 }
128
129 fn render_internal(&mut self) {
130 for (prototty_cell, element_cell) in self.buffer.iter().zip(self.element_grid.iter_mut()) {
131 if element_cell.character != prototty_cell.character {
132 element_cell.character = prototty_cell.character;
133 let string = match prototty_cell.character {
134 ' ' => " ".to_string(),
135 other => other.to_string(),
136 };
137 element_cell.element.set_inner_html(&string);
138 }
139 let element_style = element_cell.element.style();
140 if element_cell.foreground_colour != prototty_cell.foreground_colour {
141 element_cell.foreground_colour = prototty_cell.foreground_colour;
142 element_style
143 .set_property("color", &rgb24_to_web_colour(prototty_cell.foreground_colour))
144 .unwrap();
145 }
146 if element_cell.background_colour != prototty_cell.background_colour {
147 element_cell.background_colour = prototty_cell.background_colour;
148 element_style
149 .set_property(
150 "background-color",
151 &rgb24_to_web_colour(prototty_cell.background_colour),
152 )
153 .unwrap();
154 }
155 if element_cell.underline != prototty_cell.underline {
156 element_cell.underline = prototty_cell.underline;
157 if prototty_cell.underline {
158 element_style.set_property("text-decoration", "underline").unwrap();
159 } else {
160 element_style.remove_property("text-decoration").unwrap();
161 }
162 }
163 if element_cell.bold != prototty_cell.bold {
164 element_cell.bold = prototty_cell.bold;
165 if prototty_cell.bold {
166 element_style.set_property("font-weight", "bold").unwrap();
167 } else {
168 element_style.remove_property("font-weight").unwrap();
169 }
170 }
171 }
172 }
173
174 pub fn run_app<A>(self, app: A)
175 where
176 A: App + 'static,
177 {
178 let app = Rc::new(RefCell::new(app));
179 let context = Rc::new(RefCell::new(self));
180 run_app_frame(app.clone(), context.clone());
181 run_app_input(app, context);
182 }
183}
184
185fn run_app_frame<A: App + 'static>(app: Rc<RefCell<A>>, context: Rc<RefCell<Context>>) {
186 let window = web_sys::window().unwrap();
187 let performance = window.performance().unwrap();
188 let f: Rc<RefCell<Option<Closure<_>>>> = Rc::new(RefCell::new(None));
189 let g = f.clone();
190 let mut last_frame_time_stamp = performance.now();
191 *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
192 let frame_time_stamp = performance.now();
193 let since_last_frame = frame_time_stamp - last_frame_time_stamp;
194 last_frame_time_stamp = frame_time_stamp;
195 let mut context = context.borrow_mut();
196 context.buffer.clear();
197 let view_context = ViewContext::default_with_size(context.buffer.size());
198 app.borrow_mut().on_frame(
199 Duration::from_millis(since_last_frame as u64),
200 view_context,
201 &mut context.buffer,
202 );
203 context.render_internal();
204 window
205 .request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
206 .unwrap();
207 }) as Box<dyn FnMut()>));
208 g.borrow()
209 .as_ref()
210 .unwrap()
211 .as_ref()
212 .unchecked_ref::<Function>()
213 .call0(&JsValue::NULL)
214 .unwrap();
215}
216
217mod buttons {
218 pub fn has_left(buttons: u16) -> bool {
219 buttons & 1 != 0
220 }
221 pub fn has_right(buttons: u16) -> bool {
222 buttons & 2 != 0
223 }
224 pub fn has_middle(buttons: u16) -> bool {
225 buttons & 4 != 0
226 }
227 pub fn has_none(buttons: u16) -> bool {
228 buttons == 0
229 }
230}
231
232mod button {
233 use prototty_input::MouseButton;
234 const LEFT: i16 = 0;
235 const MIDDLE: i16 = 1;
236 const RIGHT: i16 = 2;
237 pub fn to_mouse_button(button: i16) -> Option<MouseButton> {
238 match button {
239 LEFT => Some(MouseButton::Left),
240 MIDDLE => Some(MouseButton::Middle),
241 RIGHT => Some(MouseButton::Right),
242 _ => None,
243 }
244 }
245}
246
247fn run_app_input<A: App + 'static>(app: Rc<RefCell<A>>, context: Rc<RefCell<Context>>) {
248 let window = web_sys::window().unwrap();
249 let handle_keydown = {
250 let app = app.clone();
251 Closure::wrap(Box::new(move |event: JsValue| {
252 let keyboard_event = event.unchecked_ref::<KeyboardEvent>();
253 if let Some(input) =
254 input::from_js_event_key_press(keyboard_event.key_code() as u8, keyboard_event.shift_key())
255 {
256 app.borrow_mut().on_input(input);
257 }
258 }) as Box<dyn FnMut(JsValue)>)
259 };
260 let handle_mouse_move = {
261 let app = app.clone();
262 let context = context.clone();
263 Closure::wrap(Box::new(move |event: JsValue| {
264 let mut app = app.borrow_mut();
265 let context = context.borrow_mut();
266 let element_display_info = context.element_display_info();
267 let mouse_event = event.unchecked_ref::<MouseEvent>();
268 let coord = element_display_info.mouse_coord(mouse_event.client_x(), mouse_event.client_y());
269 let buttons = mouse_event.buttons();
270 if buttons::has_none(buttons) {
271 app.on_input(Input::Mouse(MouseInput::MouseMove { button: None, coord }));
272 }
273 if buttons::has_left(buttons) {
274 app.on_input(Input::Mouse(MouseInput::MouseMove {
275 button: Some(MouseButton::Left),
276 coord,
277 }));
278 }
279 if buttons::has_right(buttons) {
280 app.on_input(Input::Mouse(MouseInput::MouseMove {
281 button: Some(MouseButton::Right),
282 coord,
283 }));
284 }
285 if buttons::has_middle(buttons) {
286 app.on_input(Input::Mouse(MouseInput::MouseMove {
287 button: Some(MouseButton::Middle),
288 coord,
289 }));
290 }
291 }) as Box<dyn FnMut(JsValue)>)
292 };
293 let handle_mouse_down = {
294 let app = app.clone();
295 let context = context.clone();
296 Closure::wrap(Box::new(move |event: JsValue| {
297 let mut app = app.borrow_mut();
298 let context = context.borrow_mut();
299 let element_display_info = context.element_display_info();
300 let mouse_event = event.unchecked_ref::<MouseEvent>();
301 let coord = element_display_info.mouse_coord(mouse_event.client_x(), mouse_event.client_y());
302 let button = mouse_event.button();
303 if let Some(button) = button::to_mouse_button(button) {
304 app.on_input(Input::Mouse(MouseInput::MousePress { button, coord }));
305 }
306 }) as Box<dyn FnMut(JsValue)>)
307 };
308 let handle_mouse_up = {
309 let app = app.clone();
310 let context = context.clone();
311 Closure::wrap(Box::new(move |event: JsValue| {
312 let mut app = app.borrow_mut();
313 let context = context.borrow_mut();
314 let element_display_info = context.element_display_info();
315 let mouse_event = event.unchecked_ref::<MouseEvent>();
316 let coord = element_display_info.mouse_coord(mouse_event.client_x(), mouse_event.client_y());
317 let button = mouse_event.button();
318 if let Some(button) = button::to_mouse_button(button) {
319 app.on_input(Input::Mouse(MouseInput::MouseRelease {
320 button: Ok(button),
321 coord,
322 }));
323 }
324 }) as Box<dyn FnMut(JsValue)>)
325 };
326 let handle_wheel = Closure::wrap(Box::new(move |event: JsValue| {
327 let context = context.borrow_mut();
328 let mut app = app.borrow_mut();
329 let element_display_info = context.element_display_info();
330 let wheel_event = event.unchecked_ref::<WheelEvent>();
331 let coord = element_display_info.mouse_coord(wheel_event.client_x(), wheel_event.client_y());
332 if wheel_event.delta_x() < 0. {
333 app.on_input(Input::Mouse(MouseInput::MouseScroll {
334 direction: ScrollDirection::Left,
335 coord,
336 }));
337 } else if wheel_event.delta_x() > 0. {
338 app.on_input(Input::Mouse(MouseInput::MouseScroll {
339 direction: ScrollDirection::Right,
340 coord,
341 }));
342 }
343 if wheel_event.delta_y() < 0. {
344 app.on_input(Input::Mouse(MouseInput::MouseScroll {
345 direction: ScrollDirection::Up,
346 coord,
347 }));
348 } else if wheel_event.delta_y() > 0. {
349 app.on_input(Input::Mouse(MouseInput::MouseScroll {
350 direction: ScrollDirection::Down,
351 coord,
352 }));
353 }
354 }) as Box<dyn FnMut(JsValue)>);
355 window
356 .add_event_listener_with_callback("keydown", handle_keydown.as_ref().unchecked_ref())
357 .unwrap();
358 window
359 .add_event_listener_with_callback("mousemove", handle_mouse_move.as_ref().unchecked_ref())
360 .unwrap();
361 window
362 .add_event_listener_with_callback("mousedown", handle_mouse_down.as_ref().unchecked_ref())
363 .unwrap();
364 window
365 .add_event_listener_with_callback("mouseup", handle_mouse_up.as_ref().unchecked_ref())
366 .unwrap();
367 window
368 .add_event_listener_with_callback("wheel", handle_wheel.as_ref().unchecked_ref())
369 .unwrap();
370 handle_keydown.forget();
371 handle_mouse_move.forget();
372 handle_mouse_down.forget();
373 handle_mouse_up.forget();
374 handle_wheel.forget();
375}
376
377pub struct LocalStorage {
378 local_storage: web_sys::Storage,
379}
380
381impl LocalStorage {
382 pub fn new() -> Self {
383 Self {
384 local_storage: web_sys::window().unwrap().local_storage().unwrap().unwrap(),
385 }
386 }
387}
388
389impl Default for LocalStorage {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395impl Storage for LocalStorage {
396 fn exists<K>(&self, key: K) -> bool
397 where
398 K: AsRef<str>,
399 {
400 self.local_storage.get_item(key.as_ref()).unwrap().is_some()
401 }
402
403 fn clear(&mut self) {
404 self.local_storage.clear().unwrap()
405 }
406
407 fn remove<K>(&mut self, key: K) -> Result<(), RemoveError>
408 where
409 K: AsRef<str>,
410 {
411 self.local_storage
412 .remove_item(key.as_ref())
413 .map_err(|_| RemoveError::IoError)
414 }
415
416 fn load_raw<K>(&self, key: K) -> Result<Vec<u8>, LoadRawError>
417 where
418 K: AsRef<str>,
419 {
420 let maybe_string = self
421 .local_storage
422 .get_item(key.as_ref())
423 .map_err(|_| LoadRawError::IoError)?;
424 let string = maybe_string.ok_or(LoadRawError::NoSuchKey)?;
425 serde_json::from_str(&string).map_err(|_| LoadRawError::IoError)
426 }
427
428 fn store_raw<K, V>(&mut self, key: K, value: V) -> Result<(), StoreRawError>
429 where
430 K: AsRef<str>,
431 V: AsRef<[u8]>,
432 {
433 let string = serde_json::to_string(value.as_ref()).map_err(|_| StoreRawError::IoError)?;
434 self.local_storage
435 .set_item(key.as_ref(), &string)
436 .map_err(|_| StoreRawError::IoError)
437 }
438}
439
440pub struct WebAudioPlayer {
441 mime: String,
442}
443
444impl WebAudioPlayer {
445 pub fn new_with_mime(mime: &str) -> Self {
446 Self { mime: mime.to_string() }
447 }
448 pub fn load_sound(&self, bytes: &'static [u8]) -> WebSound {
449 let base64_data = base64::encode(bytes);
450 let uri = format!("data:{};base64,{}", self.mime, base64_data);
451 WebSound { uri }
452 }
453}
454
455impl WebAudioPlayer {
456 pub fn play(&self, sound: &WebSound, properties: AudioProperties) {
457 let element = web_sys::HtmlAudioElement::new_with_src(sound.uri.as_str()).unwrap();
458 element.set_volume(properties.volume as f64);
459 let _ = element.play().unwrap();
460 }
461}
462
463pub struct WebSound {
464 uri: String,
465}
466
467impl AudioPlayer for WebAudioPlayer {
468 type Sound = WebSound;
469 fn play(&self, sound: &Self::Sound, properties: AudioProperties) {
470 WebAudioPlayer::play(self, sound, properties)
471 }
472 fn load_sound(&self, bytes: &'static [u8]) -> Self::Sound {
473 WebAudioPlayer::load_sound(self, bytes)
474 }
475}