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}