prototty_web/
lib.rs

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                    ' ' => "&nbsp;".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}