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