Skip to main content

pebble/graphics/
window.rs

1use std::sync::{Arc, Mutex};
2
3use winit::{
4    event::{Event, WindowEvent},
5    event_loop::{ControlFlow, EventLoop},
6    window::{Fullscreen, Window as OsWindow, WindowBuilder},
7};
8use winit_input_helper::WinitInputHelper;
9
10use crate::{
11    ecs::plugin::Plugin,
12    graphics::types::{CursorGrabMode, CursorIcon, KeyCode, MouseButton},
13};
14
15pub struct WindowConfig {
16    pub title: String,
17    pub width: u32,
18    pub height: u32,
19}
20
21impl Default for WindowConfig {
22    fn default() -> Self {
23        Self {
24            title: "Pebble".to_string(),
25            width: 1280,
26            height: 720,
27        }
28    }
29}
30
31#[derive(Clone)]
32pub struct Window(Arc<OsWindow>);
33
34impl Window {
35    fn new(handle: Arc<OsWindow>) -> Self {
36        Self(handle)
37    }
38
39    pub(crate) fn raw(&self) -> Arc<OsWindow> {
40        self.0.clone()
41    }
42
43    pub fn set_title(&self, title: &str) {
44        self.0.set_title(title);
45    }
46
47    pub fn inner_size(&self) -> (u32, u32) {
48        let size = self.0.inner_size();
49        (size.width, size.height)
50    }
51
52    pub fn set_inner_size(&self, width: u32, height: u32) {
53        let _ = self.0.request_inner_size(winit::dpi::PhysicalSize::new(width, height));
54    }
55
56    pub fn set_resizable(&self, resizable: bool) {
57        self.0.set_resizable(resizable);
58    }
59
60    pub fn set_visible(&self, visible: bool) {
61        self.0.set_visible(visible);
62    }
63
64    pub fn set_minimized(&self, minimized: bool) {
65        self.0.set_minimized(minimized);
66    }
67
68    pub fn set_maximized(&self, maximized: bool) {
69        self.0.set_maximized(maximized);
70    }
71
72    pub fn set_decorations(&self, decorations: bool) {
73        self.0.set_decorations(decorations);
74    }
75
76    pub fn focus(&self) {
77        self.0.focus_window();
78    }
79
80    pub fn set_fullscreen(&self, fullscreen: bool) {
81        self.0.set_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None)));
82    }
83
84    pub fn is_fullscreen(&self) -> bool {
85        self.0.fullscreen().is_some()
86    }
87
88    pub fn set_cursor_icon(&self, icon: CursorIcon) {
89        self.0.set_cursor_icon(icon.into());
90    }
91
92    pub fn set_cursor_visible(&self, visible: bool) {
93        self.0.set_cursor_visible(visible);
94    }
95
96    pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> bool {
97        self.0.set_cursor_grab(mode.into()).is_ok()
98    }
99
100    pub fn request_redraw(&self) {
101        self.0.request_redraw();
102    }
103}
104
105struct InputState {
106    helper: WinitInputHelper,
107}
108
109#[derive(Clone)]
110pub struct Input(Arc<Mutex<InputState>>);
111
112impl Input {
113    fn new() -> Self {
114        Self(Arc::new(Mutex::new(InputState {
115            helper: WinitInputHelper::new(),
116        })))
117    }
118
119    fn update(&self, event: &Event<()>) -> bool {
120        self.0.lock().unwrap().helper.update(event)
121    }
122
123    pub fn key_pressed(&self, key: KeyCode) -> bool {
124        self.0.lock().unwrap().helper.key_pressed(key.into())
125    }
126
127    pub fn key_released(&self, key: KeyCode) -> bool {
128        self.0.lock().unwrap().helper.key_released(key.into())
129    }
130
131    pub fn key_held(&self, key: KeyCode) -> bool {
132        self.0.lock().unwrap().helper.key_held(key.into())
133    }
134
135    pub fn mouse_pressed(&self, button: MouseButton) -> bool {
136        self.0.lock().unwrap().helper.mouse_pressed(button.into())
137    }
138
139    pub fn mouse_released(&self, button: MouseButton) -> bool {
140        self.0.lock().unwrap().helper.mouse_released(button.into())
141    }
142
143    pub fn mouse_held(&self, button: MouseButton) -> bool {
144        self.0.lock().unwrap().helper.mouse_held(button.into())
145    }
146
147    pub fn cursor(&self) -> Option<(f32, f32)> {
148        self.0.lock().unwrap().helper.cursor()
149    }
150
151    pub fn cursor_diff(&self) -> (f32, f32) {
152        self.0.lock().unwrap().helper.cursor_diff()
153    }
154
155    pub fn mouse_diff(&self) -> (f32, f32) {
156        self.0.lock().unwrap().helper.mouse_diff()
157    }
158
159    pub fn scroll_diff(&self) -> (f32, f32) {
160        self.0.lock().unwrap().helper.scroll_diff()
161    }
162
163    pub fn close_requested(&self) -> bool {
164        self.0.lock().unwrap().helper.close_requested()
165    }
166
167    pub fn resolution(&self) -> Option<(u32, u32)> {
168        self.0.lock().unwrap().helper.resolution()
169    }
170}
171
172pub struct WindowPlugin {
173    config: WindowConfig,
174}
175
176impl WindowPlugin {
177    pub fn new(config: WindowConfig) -> Self {
178        Self { config }
179    }
180}
181
182impl Default for WindowPlugin {
183    fn default() -> Self {
184        Self::new(WindowConfig::default())
185    }
186}
187
188impl Plugin for WindowPlugin {
189    fn build(self, app: crate::app::App) -> crate::app::App {
190        let event_loop = EventLoop::new().unwrap();
191        event_loop.set_control_flow(ControlFlow::Poll);
192
193        #[allow(unused_mut)]
194        let mut builder = WindowBuilder::new()
195            .with_title(self.config.title)
196            .with_inner_size(winit::dpi::PhysicalSize::new(self.config.width, self.config.height));
197
198        // winit doesn't insert the canvas into the page on its own — ask it
199        // to, so a window actually shows up without hand-rolled web_sys/DOM
200        // code
201        #[cfg(target_arch = "wasm32")]
202        {
203            use winit::platform::web::WindowBuilderExtWebSys;
204            builder = builder.with_append(true);
205        }
206
207        let os_window = Arc::new(builder.build(&event_loop).unwrap());
208
209        let window = Window::new(os_window);
210        let input = Input::new();
211
212        app.insert_resource(window)
213            .insert_resource(input.clone())
214            .set_runner(move |mut app| {
215                let handler = move |event, elwt: &winit::event_loop::EventLoopWindowTarget<()>| {
216                    let stepped = input.update(&event);
217
218                    if let Event::WindowEvent {
219                        event: WindowEvent::CloseRequested,
220                        ..
221                    } = &event
222                    {
223                        elwt.exit();
224                        return;
225                    }
226
227                    if stepped {
228                        app.update();
229                        if app.should_exit() {
230                            elwt.exit();
231                        }
232                    }
233                };
234
235                // `run` blocks forever natively; on wasm it only works via an
236                // internal exception-unwinding trick and isn't always
237                // available — `spawn` is the purpose-built non-blocking wasm
238                // equivalent, same closure, just returns immediately after
239                // registering it with the browser
240                #[cfg(not(target_arch = "wasm32"))]
241                event_loop.run(handler).unwrap();
242
243                #[cfg(target_arch = "wasm32")]
244                {
245                    use winit::platform::web::EventLoopExtWebSys;
246                    event_loop.spawn(handler);
247                }
248            })
249    }
250}