Skip to main content

limelight_yew/
lib.rs

1mod key_event;
2
3use anyhow::Result;
4use gloo_events::{EventListener, EventListenerOptions};
5use gloo_render::{request_animation_frame, AnimationFrame};
6pub use key_event::KeyCode;
7use limelight::renderer::Renderer;
8use std::{cell::RefCell, marker::PhantomData, rc::Rc};
9use wasm_bindgen::JsCast;
10use web_sys::{window, HtmlCanvasElement, WebGl2RenderingContext};
11use yew::{html, Component, KeyboardEvent, MouseEvent, NodeRef, Properties, WheelEvent};
12
13pub type ShouldRequestAnimationFrame = bool;
14
15#[allow(unused_variables)]
16pub trait LimelightController: 'static {
17    fn draw(&mut self, renderer: &mut Renderer, ts: f64) -> Result<ShouldRequestAnimationFrame>;
18
19    fn handle_key_down(&mut self, key: KeyCode) -> ShouldRequestAnimationFrame {
20        false
21    }
22
23    fn handle_key_up(&mut self, key: KeyCode) -> ShouldRequestAnimationFrame {
24        false
25    }
26
27    fn handle_drag(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame {
28        false
29    }
30
31    fn handle_mousemove(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame {
32        false
33    }
34
35    fn handle_scroll(
36        &mut self,
37        x_amount: f32,
38        y_amount: f32,
39        x_position: f32,
40        y_position: f32,
41    ) -> ShouldRequestAnimationFrame {
42        false
43    }
44
45    fn handle_pinch(&mut self, amount: f32, x: f32, y: f32) -> ShouldRequestAnimationFrame {
46        false
47    }
48}
49
50pub struct LimelightComponent<Controller: LimelightController> {
51    canvas_ref: NodeRef,
52    renderer: Option<Renderer>,
53    render_handle: Option<AnimationFrame>,
54    keydown_handler: Option<EventListener>,
55    keyup_handler: Option<EventListener>,
56    drag_origin: Option<(i32, i32)>,
57    _ph: PhantomData<Controller>,
58}
59
60#[derive(Debug)]
61pub enum Msg {
62    Render(f64),
63    MouseMove(MouseEvent),
64    MouseDown(MouseEvent),
65    MouseUp(MouseEvent),
66    MouseWheel(WheelEvent),
67    KeyDown(KeyboardEvent),
68    KeyUp(KeyboardEvent),
69}
70
71#[derive(Properties)]
72pub struct ControllerProps<Controller: LimelightController> {
73    controller: Rc<RefCell<Controller>>,
74    height: i32,
75    width: i32,
76}
77
78impl<Controller: LimelightController> Default for ControllerProps<Controller>
79where
80    Controller: Default,
81{
82    fn default() -> Self {
83        Self {
84            controller: Rc::new(RefCell::new(Controller::default())),
85            width: 600,
86            height: 600,
87        }
88    }
89}
90
91impl<Controller: LimelightController> PartialEq for ControllerProps<Controller> {
92    fn eq(&self, other: &Self) -> bool {
93        Rc::ptr_eq(&self.controller, &other.controller)
94    }
95}
96
97impl<Controller: LimelightController> LimelightComponent<Controller> {
98    fn request_render(&mut self, ctx: &yew::Context<Self>) {
99        let render_callback = ctx.link().callback(Msg::Render);
100        self.render_handle = Some(request_animation_frame(move |ts| render_callback.emit(ts)));
101    }
102}
103
104impl<Controller: LimelightController> Component for LimelightComponent<Controller> {
105    type Message = Msg;
106
107    type Properties = ControllerProps<Controller>;
108
109    fn create(_ctx: &yew::Context<Self>) -> Self {
110        Self {
111            canvas_ref: NodeRef::default(),
112            renderer: None,
113            render_handle: None,
114            keydown_handler: None,
115            keyup_handler: None,
116            drag_origin: None,
117            _ph: PhantomData::default(),
118        }
119    }
120
121    fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
122        match msg {
123            Msg::Render(ts) => {
124                if let Some(renderer) = &mut self.renderer {
125                    let should_render = (*ctx.props().controller)
126                        .borrow_mut()
127                        .draw(renderer, ts)
128                        .unwrap();
129
130                    if should_render {
131                        self.request_render(ctx);
132                    }
133                }
134            }
135            Msg::KeyDown(event) => {
136                let should_render = (*ctx.props().controller)
137                    .borrow_mut()
138                    .handle_key_down(event.key().as_str().into());
139                if should_render {
140                    self.request_render(ctx);
141                }
142            }
143            Msg::KeyUp(event) => {
144                let should_render = (*ctx.props().controller)
145                    .borrow_mut()
146                    .handle_key_up(event.key().as_str().into());
147                if should_render {
148                    self.request_render(ctx);
149                }
150            }
151            Msg::MouseDown(e) => {
152                self.drag_origin = Some((e.offset_x(), e.offset_y()));
153            }
154            Msg::MouseUp(_) => {
155                self.drag_origin = None;
156            }
157            Msg::MouseMove(e) => {
158                let (new_x, new_y) = (e.offset_x(), e.offset_y());
159
160                if let Some((origin_x, origin_y)) = self.drag_origin {
161                    let should_render = (*ctx.props().controller).borrow_mut().handle_drag(
162                        2. * (new_x - origin_x) as f32 / ctx.props().width as f32,
163                        2. * -(new_y - origin_y) as f32 / ctx.props().height as f32,
164                    );
165
166                    if should_render {
167                        self.request_render(ctx);
168                    }
169
170                    self.drag_origin = Some((new_x, new_y));
171                } else {
172                    let should_render = (*ctx.props().controller).borrow_mut().handle_mousemove(
173                        2. * new_x as f32 / ctx.props().width as f32,
174                        2. * -new_y as f32 / ctx.props().height as f32,
175                    );
176
177                    if should_render {
178                        self.request_render(ctx);
179                    }
180                }
181            }
182            Msg::MouseWheel(e) => {
183                let scroll_amount_y = e.delta_y() as f32;
184                let scroll_amount_x = e.delta_x() as f32;
185
186                let pin_x = (2 * e.offset_x()) as f32 / ctx.props().width as f32 - 1.;
187                let pin_y = -((2 * e.offset_y()) as f32 / ctx.props().height as f32 - 1.);
188
189                let should_render = if e.ctrl_key() {
190                    (*ctx.props().controller).borrow_mut().handle_pinch(
191                        -scroll_amount_y,
192                        pin_x,
193                        pin_y,
194                    )
195                } else {
196                    (*ctx.props().controller).borrow_mut().handle_scroll(
197                        -scroll_amount_x as f32 * 2. / ctx.props().width as f32,
198                        scroll_amount_y as f32 * 2. / ctx.props().height as f32,
199                        pin_x,
200                        pin_y,
201                    )
202                };
203
204                if should_render {
205                    self.request_render(ctx);
206                }
207
208                e.prevent_default();
209            }
210        }
211
212        false
213    }
214
215    fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
216        let props = ctx.props();
217        let link = ctx.link();
218        let device_pixel_ratio = window().unwrap().device_pixel_ratio();
219
220        html! {
221            <canvas
222                height={(props.height as f64 * device_pixel_ratio).to_string()}
223                width={(props.width as f64 * device_pixel_ratio).to_string()}
224                style={format!("width: {}px; height: {}px;", props.width, props.height)}
225                onmousedown={link.callback(Msg::MouseDown)}
226                onmousemove={link.callback(Msg::MouseMove)}
227                onmouseup={link.callback(Msg::MouseUp)}
228                onwheel={link.callback(Msg::MouseWheel)}
229                onkeydown={link.callback(Msg::KeyDown)}
230                ref={self.canvas_ref.clone()} />
231        }
232    }
233
234    fn rendered(&mut self, ctx: &yew::Context<Self>, first_render: bool) {
235        if first_render {
236            let canvas = self.canvas_ref.cast::<HtmlCanvasElement>().unwrap();
237            let gl: WebGl2RenderingContext = canvas
238                .get_context("webgl2")
239                .unwrap()
240                .unwrap()
241                .dyn_into()
242                .unwrap();
243
244            let options = EventListenerOptions::enable_prevent_default();
245            {
246                let callback = ctx.link().callback(Msg::KeyDown);
247                self.keydown_handler = Some(EventListener::new_with_options(
248                    &window().unwrap(),
249                    "keydown",
250                    options,
251                    move |event| {
252                        event.prevent_default();
253                        callback.emit(event.clone().dyn_into().unwrap())
254                    },
255                ));
256            }
257            {
258                let callback = ctx.link().callback(Msg::KeyUp);
259                self.keyup_handler = Some(EventListener::new_with_options(
260                    &window().unwrap(),
261                    "keyup",
262                    options,
263                    move |event| {
264                        event.prevent_default();
265                        callback.emit(event.clone().dyn_into().unwrap())
266                    },
267                ));
268            }
269
270            self.renderer = Some(Renderer::new(gl));
271
272            self.request_render(ctx);
273        }
274    }
275}