geng_window/
lib.rs

1use batbox_la::*;
2use futures::prelude::*;
3use serde::{Deserialize, Serialize};
4use std::cell::{Cell, RefCell};
5use std::collections::HashSet;
6use std::rc::Rc;
7use ugli::Ugli;
8
9mod backend;
10
11mod cursor;
12mod events;
13
14pub use cursor::*;
15pub use events::*;
16
17#[derive(Debug, Clone, Serialize, Deserialize, clap::Args, Default)]
18#[group(id = "window")]
19pub struct CliArgs {
20    /// Turn vertical synchronization on/off
21    #[clap(long, value_name = "BOOL")]
22    pub vsync: Option<bool>,
23    /// Turn antialiasing on/off
24    #[clap(long, value_name = "BOOL")]
25    pub antialias: Option<bool>,
26    /// Start with given window width (also requires window-height)
27    #[clap(long = "window-width", value_name = "PIXELS")]
28    pub width: Option<usize>,
29    /// Start with given window height (also requires window-width)
30    #[clap(long = "window-height", value_name = "PIXELS")]
31    pub height: Option<usize>,
32    /// Start in fullscreen
33    #[clap(long, value_name = "BOOL")]
34    pub fullscreen: Option<bool>,
35}
36
37#[derive(Debug, Clone)]
38pub struct Options {
39    pub fullscreen: bool,
40    pub vsync: bool,
41    pub title: String,
42    pub antialias: bool,
43    pub transparency: bool,
44    pub mouse_passthrough: bool,
45    pub size: Option<vec2<usize>>,
46    pub auto_close: bool,
47    pub start_hidden: bool,
48}
49
50impl Options {
51    pub fn new(title: &str) -> Self {
52        Self {
53            title: title.to_owned(),
54            fullscreen: !cfg!(debug_assertions),
55            vsync: true,
56            antialias: true,
57            transparency: false,
58            mouse_passthrough: false,
59            size: None,
60            auto_close: true,
61            start_hidden: false,
62        }
63    }
64
65    pub fn with_cli(&mut self, args: &CliArgs) {
66        if let Some(vsync) = args.vsync {
67            self.vsync = vsync;
68        }
69        if let Some(antialias) = args.antialias {
70            self.antialias = antialias;
71        }
72        if let (Some(window_width), Some(window_height)) = (args.width, args.height) {
73            self.size = Some(vec2(window_width, window_height));
74        }
75        if let Some(fullscreen) = args.fullscreen {
76            self.fullscreen = fullscreen;
77        }
78    }
79}
80
81struct WindowImpl {
82    event_sender: async_broadcast::Sender<Event>,
83    event_receiver: RefCell<async_broadcast::Receiver<Event>>,
84    executor: async_executor::LocalExecutor<'static>,
85    backend: Rc<backend::Context>,
86    pressed_keys: Rc<RefCell<HashSet<Key>>>,
87    pressed_buttons: Rc<RefCell<HashSet<MouseButton>>>,
88    cursor_pos: Cell<Option<vec2<f64>>>,
89    cursor_type: Cell<CursorType>,
90    auto_close: Cell<bool>,
91}
92
93#[derive(Clone)]
94pub struct Window {
95    inner: Rc<WindowImpl>,
96}
97
98impl Window {
99    pub fn start_text_edit(&self, text: &str) {
100        self.inner.backend.start_text_edit(text);
101    }
102
103    pub fn stop_text_edit(&self) {
104        self.inner.backend.stop_text_edit();
105    }
106
107    pub fn is_editing_text(&self) -> bool {
108        self.inner.backend.is_editing_text()
109    }
110
111    pub fn real_size(&self) -> vec2<usize> {
112        self.inner.backend.real_size()
113    }
114    pub fn size(&self) -> vec2<usize> {
115        self.real_size().map(|x| x.max(1))
116    }
117
118    pub fn ugli(&self) -> &Ugli {
119        self.inner.backend.ugli()
120    }
121
122    pub fn is_key_pressed(&self, key: Key) -> bool {
123        self.inner.pressed_keys.borrow().contains(&key)
124    }
125
126    pub fn is_button_pressed(&self, button: MouseButton) -> bool {
127        self.inner.pressed_buttons.borrow().contains(&button)
128    }
129
130    pub fn pressed_keys(&self) -> HashSet<Key> {
131        self.inner.pressed_keys.borrow().clone()
132    }
133
134    pub fn pressed_buttons(&self) -> HashSet<MouseButton> {
135        self.inner.pressed_buttons.borrow().clone()
136    }
137
138    pub fn set_fullscreen(&self, fullscreen: bool) {
139        self.inner.backend.set_fullscreen(fullscreen);
140    }
141
142    pub fn is_fullscreen(&self) -> bool {
143        self.inner.backend.is_fullscreen()
144    }
145
146    pub fn toggle_fullscreen(&self) {
147        self.set_fullscreen(!self.is_fullscreen());
148    }
149
150    pub fn set_auto_close(&self, auto_close: bool) {
151        self.inner.auto_close.set(auto_close);
152    }
153
154    pub fn is_auto_close(&self) -> bool {
155        self.inner.auto_close.get()
156    }
157
158    #[cfg(not(target_arch = "wasm32"))]
159    pub fn set_icon(&self, path: impl AsRef<std::path::Path>) -> anyhow::Result<()> {
160        self.inner.backend.set_icon(path.as_ref())
161    }
162
163    pub fn spawn(
164        &self,
165        f: impl std::future::Future<Output = ()> + 'static,
166    ) -> async_executor::Task<()> {
167        self.inner.executor.spawn(f)
168    }
169
170    pub fn with_framebuffer<T>(&self, f: impl FnOnce(&mut ugli::Framebuffer) -> T) -> T {
171        self.inner.backend.with_framebuffer(f)
172    }
173
174    pub fn events(&self) -> impl futures::Stream<Item = Event> {
175        self.inner.event_receiver.borrow().clone()
176    }
177
178    pub fn show(&self) {
179        self.inner.backend.show();
180    }
181
182    pub async fn yield_now(&self) {
183        self.events()
184            .filter(|event| future::ready(matches!(event, Event::Draw)))
185            .next()
186            .await;
187    }
188}
189
190pub fn run<Fut>(options: &Options, f: impl 'static + FnOnce(Window) -> Fut)
191where
192    Fut: std::future::Future<Output = ()> + 'static,
193{
194    let options = options.clone();
195    backend::run(&options, move |backend| {
196        // channel capacity is 1 because events are supposed to be consumed immediately
197        let (mut event_sender, event_receiver) = async_broadcast::broadcast(1);
198        event_sender.set_overflow(true);
199        let window = Window {
200            inner: Rc::new(WindowImpl {
201                event_sender,
202                // We can't just not have this receiver since the channel will be closed then
203                event_receiver: RefCell::new(event_receiver),
204                executor: async_executor::LocalExecutor::new(),
205                backend,
206                pressed_keys: Rc::new(RefCell::new(HashSet::new())),
207                pressed_buttons: Rc::new(RefCell::new(HashSet::new())),
208                auto_close: Cell::new(options.auto_close),
209                cursor_pos: Cell::new(None),
210                cursor_type: Cell::new(CursorType::Default),
211            }),
212        };
213        if options.fullscreen {
214            window.set_fullscreen(true);
215        }
216        if !options.start_hidden {
217            window.show();
218        }
219
220        let f = f(window.clone());
221        let main_task = window.spawn(f);
222        while window.inner.executor.try_tick() {}
223        move |event| {
224            match event {
225                Event::KeyPress { key } => {
226                    if !window.inner.pressed_keys.borrow_mut().insert(key) {
227                        return std::ops::ControlFlow::Continue(());
228                    }
229                }
230                Event::KeyRelease { key } => {
231                    if !window.inner.pressed_keys.borrow_mut().remove(&key) {
232                        return std::ops::ControlFlow::Continue(());
233                    }
234                }
235                Event::MousePress { button } => {
236                    window.inner.pressed_buttons.borrow_mut().insert(button);
237                }
238                Event::MouseRelease { button } => {
239                    window.inner.pressed_buttons.borrow_mut().remove(&button);
240                }
241                Event::CursorMove { position } => {
242                    window.inner.cursor_pos.set(Some(position));
243                    if window.cursor_locked() {
244                        return std::ops::ControlFlow::Continue(());
245                    }
246                }
247                Event::RawMouseMove { .. } => {
248                    if !window.cursor_locked() {
249                        return std::ops::ControlFlow::Continue(());
250                    }
251                }
252                Event::CloseRequested => {
253                    if window.is_auto_close() {
254                        return std::ops::ControlFlow::Break(());
255                    }
256                }
257                _ => {}
258            }
259            if let Some(_removed) = window.inner.event_sender.try_broadcast(event).unwrap() {
260                // log::error!("Event has been ignored: {removed:?}");
261            }
262            window.inner.event_receiver.borrow_mut().try_recv().unwrap();
263            while window.inner.executor.try_tick() {
264                if main_task.is_finished() {
265                    return std::ops::ControlFlow::Break(());
266                }
267            }
268            std::ops::ControlFlow::Continue(())
269        }
270    });
271}