diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 01ab692..29061f8 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -6,11 +6,15 @@ use mini_gl_fb::glutin::event::{VirtualKeyCode, MouseButton}; use mini_gl_fb::glutin::event_loop::EventLoop; use mini_gl_fb::glutin::dpi::LogicalSize; -use std::time::SystemTime; +use std::time::{Instant, Duration}; +use mini_gl_fb::breakout::Wakeup; const WIDTH: usize = 200; const HEIGHT: usize = 200; +const NORMAL_SPEED: u64 = 500; +const TURBO_SPEED: u64 = 20; + fn main() { let mut event_loop = EventLoop::new(); let mut fb = mini_gl_fb::get_fancy(config! { @@ -35,38 +39,62 @@ fn main() { cells[52 * WIDTH + 50] = true; cells[52 * WIDTH + 51] = true; - let mut previous = SystemTime::now(); - let mut extra_delay: f64 = 0.0; + // ID of the Wakeup which means we should update the board + let mut update_id: Option = None; fb.glutin_handle_basic_input(&mut event_loop, |fb, input| { - let elapsed = previous.elapsed().unwrap(); - let seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; + // We're going to use wakeups to update the grid + input.wait = true; + + if update_id.is_none() { + update_id = Some(input.schedule_wakeup(Instant::now() + Duration::from_millis(500))) + } else if let Some(mut wakeup) = input.wakeup { + if Some(wakeup.id) == update_id { + // Time to update our grid + calculate_neighbors(&mut cells, &mut neighbors); + make_some_babies(&mut cells, &mut neighbors); + fb.update_buffer(&cells); + + // Reschedule another update + wakeup.when = Instant::now() + Duration::from_millis( + if input.key_is_down(VirtualKeyCode::LShift) { + TURBO_SPEED + } else { + NORMAL_SPEED + } + ); + + input.reschedule_wakeup(wakeup); + } + + // We will get called again after all wakeups are handled + return true; + } if input.key_is_down(VirtualKeyCode::Escape) { return false; } - if input.mouse_is_down(MouseButton::Left) { + if input.mouse_is_down(MouseButton::Left) || input.mouse_is_down(MouseButton::Right) { // Mouse was pressed let (x, y) = input.mouse_pos; let x = x.min(WIDTH as f64 - 0.0001).max(0.0).floor() as usize; let y = y.min(HEIGHT as f64 - 0.0001).max(0.0).floor() as usize; - cells[y * WIDTH + x] = true; + cells[y * WIDTH + x] = input.mouse_is_down(MouseButton::Left); fb.update_buffer(&cells); // Give the user extra time to make something pretty each time they click - previous = SystemTime::now(); - extra_delay = (extra_delay + 0.5).min(2.0); + if !input.key_is_down(VirtualKeyCode::LShift) { + input.adjust_wakeup(update_id.unwrap(), Wakeup::after_millis(2000)); + } } - // Each generation should stay on screen for half a second - if seconds > 0.5 + extra_delay { - previous = SystemTime::now(); - calculate_neighbors(&mut cells, &mut neighbors); - make_some_babies(&mut cells, &mut neighbors); - fb.update_buffer(&cells); - extra_delay = 0.0; - } else if input.resized { - fb.redraw(); + // TODO support right shift. Probably by querying modifiers somehow. (modifiers support) + if input.key_pressed(VirtualKeyCode::LShift) { + // immediately update + input.adjust_wakeup(update_id.unwrap(), Wakeup::after_millis(0)); + } else if input.key_released(VirtualKeyCode::LShift) { + // immediately stop updating + input.adjust_wakeup(update_id.unwrap(), Wakeup::after_millis(NORMAL_SPEED)); } true diff --git a/src/breakout.rs b/src/breakout.rs index ba7d4aa..630818c 100644 --- a/src/breakout.rs +++ b/src/breakout.rs @@ -6,6 +6,7 @@ use core::Framebuffer; use std::collections::HashMap; use glutin::event::{MouseButton, VirtualKeyCode, ModifiersState}; +use std::time::{Instant, Duration}; /// `GlutinBreakout` is useful when you are growing out of the basic input methods and synchronous /// nature of [`MiniGlFb`][crate::MiniGlFb], since it's more powerful than the the higher-level @@ -215,6 +216,37 @@ impl GlutinBreakout { } } +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Wakeup { + /// The [`Instant`] at which this wakeup is scheduled to happen. If the [`Instant`] is in the + /// past, the wakeup will happen instantly. + pub when: Instant, + + /// A numeric identifier that can be used to determine which wakeup your callback is being run + /// for. + pub id: u32, +} + +impl Wakeup { + /// Returns [`Instant::now`]`() + duration`. + pub fn after(duration: Duration) -> Instant { + Instant::now() + duration + } + + /// The same as [`Wakeup::after`], but constructs a [`Duration`] from a number of milliseconds, + /// since [`Duration`] methods are so long... + pub fn after_millis(millis: u64) -> Instant { + Self::after(Duration::from_millis(millis)) + } + + /// Modifies this wakeup to trigger after `duration` has passed from [`Instant::now`], + /// calculated via [`Wakeup::after`]. + pub fn trigger_after(&mut self, duration: Duration) { + self.when = Self::after(duration); + } +} + /// Used for [`MiniGlFb::glutin_handle_basic_input`][crate::MiniGlFb::glutin_handle_basic_input]. /// Contains the current state of the window in a polling-like fashion. #[non_exhaustive] @@ -245,6 +277,24 @@ pub struct BasicInput { /// If this is set to `true` by your callback, it will not be called as fast as possible, but /// rather only when the input changes. pub wait: bool, + /// A record of all the [`Wakeup`]s that are scheduled to happen. If your callback is being + /// called because of a wakeup, [`BasicInput::wakeup`] will be set to `Some(id)` where `id` is + /// the unique identifier of the [`Wakeup`]. + /// + /// Wakeups can be scheduled using [`BasicInput::schedule_wakeup`]. Wakeups can be cancelled + /// using [`BasicInput::cancel_wakeup`], or by removing the item from the [`Vec`]. + // NOTE: THIS VEC IS SUPPOSED TO ALWAYS BE SORTED BY SOONEST WAKEUP FIRST! + // This contract MUST be upheld at all times, or else weird behavior will result. Only the + // wakeup at index 0 is ever checked at a time, no other wakeups will be queued if it is not due + // yet. DO NOT IGNORE THIS WARNING! + pub wakeups: Vec, + /// Indicates to your callback which [`Wakeup`] it should be handling. Normally, it's okay to + /// ignore this, as it will always be [`None`] unless you manually schedule wakeups using + /// [`BasicInput::schedule_wakeup`]. + pub wakeup: Option, + // Internal variable used to keep track of what the next wakeup ID should be. Doesn't need to be + // `pub`; `BasicInput` is already `#[non_exhaustive]`. + _next_wakeup_id: u32, } impl BasicInput { @@ -287,4 +337,41 @@ impl BasicInput { pub fn key_released(&self, button: VirtualKeyCode) -> bool { &(true, false) == self.keys.get(&button).unwrap_or(&(false, false)) } + + /// Given an [`Instant`] in the future (or in the past, in which case it will be triggered + /// immediately), schedules a wakeup to be triggered then. Returns the ID of the wakeup, which + /// will be the ID of [`BasicInput::wakeup`] if your callback is getting called by the wakeup. + pub fn schedule_wakeup(&mut self, when: Instant) -> u32 { + let wakeup = Wakeup { when, id: self._next_wakeup_id }; + self._next_wakeup_id += 1; + self.reschedule_wakeup(wakeup); + wakeup.id + } + + /// Reschedules a wakeup. It is perfectly valid to re-use IDs of wakeups that have already been + /// triggered; that is why [`BasicInput::wakeup`] is a [`Wakeup`] and not just a [`u32`]. + pub fn reschedule_wakeup(&mut self, wakeup: Wakeup) { + let at = self.wakeups.iter().position(|o| o.when > wakeup.when).unwrap_or(self.wakeups.len()); + self.wakeups.insert(at, wakeup); + } + + /// Cancels a previously scheduled [`Wakeup`] by its ID. Returns the [`Wakeup`] if it is found, + /// otherwise returns [`None`]. + pub fn cancel_wakeup(&mut self, id: u32) -> Option { + Some(self.wakeups.remove(self.wakeups.iter().position(|w| w.id == id)?)) + } + + /// Changing the time of an upcoming wakeup is common enough that there's a utility method to do + /// it for you. Given an ID and an [`Instant`], finds the [`Wakeup`] with the given ID and sets + /// its time to `when`. Returns `true` if a wakeup was found, `false` otherwise. + pub fn adjust_wakeup(&mut self, id: u32, when: Instant) -> bool { + if let Some(mut wakeup) = self.cancel_wakeup(id) { + // Put it back in the queue; this is important because it might end up somewhere else + wakeup.when = when; + self.reschedule_wakeup(wakeup); + true + } else { + false + } + } } diff --git a/src/core.rs b/src/core.rs index 545656d..dc21a98 100644 --- a/src/core.rs +++ b/src/core.rs @@ -12,7 +12,8 @@ use std::mem::size_of_val; use glutin::window::WindowBuilder; use glutin::event_loop::{EventLoop, ControlFlow}; use glutin::platform::run_return::EventLoopExtRunReturn; -use glutin::event::{Event, WindowEvent, VirtualKeyCode, ElementState, KeyboardInput}; +use glutin::event::{Event, WindowEvent, VirtualKeyCode, ElementState, KeyboardInput, StartCause}; +use std::time::Instant; /// Create a context using glutin given a configuration. pub fn init_glutin_context( @@ -215,7 +216,7 @@ impl Internal { }); } - pub fn glutin_handle_basic_input bool>( + pub fn glutin_handle_basic_input bool>( &mut self, event_loop: &mut EventLoop, mut handler: F ) { let mut previous_input: Option = None; @@ -234,9 +235,12 @@ impl Internal { val.0 = val.1; } - match event { + match &event { Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => *flow = ControlFlow::Exit, + WindowEvent::CloseRequested => { + *flow = ControlFlow::Exit; + return; + }, WindowEvent::KeyboardInput { input: KeyboardInput { virtual_keycode: Some(vk), @@ -245,23 +249,23 @@ impl Internal { }, .. } => { - let key = input.keys.entry(vk) + let key = input.keys.entry(*vk) .or_insert((false, false)); - key.1 = state == ElementState::Pressed; + key.1 = *state == ElementState::Pressed; } WindowEvent::CursorMoved { position, .. } => { - new_mouse_pos = Some(position); + new_mouse_pos = Some(*position); } WindowEvent::MouseInput { state, button, .. } => { - let button = input.mouse.entry(button) + let button = input.mouse.entry(*button) .or_insert((false, false)); - button.1 = state == ElementState::Pressed; + button.1 = *state == ElementState::Pressed; } WindowEvent::ModifiersChanged(modifiers) => { - input.modifiers = modifiers; + input.modifiers = *modifiers; } WindowEvent::Resized(logical_size) => { - new_size = Some(logical_size); + new_size = Some(*logical_size); } _ => {} }, @@ -270,7 +274,7 @@ impl Internal { if let Some(size) = new_size { self.resize_viewport(size.width, size.height); - input.resized = false; + input.resized = true; } if let Some(pos) = new_mouse_pos { @@ -289,18 +293,40 @@ impl Internal { input.mouse_pos = mouse_pos; } + while let Some(wakeup) = input.wakeups.get(0) { + if wakeup.when > Instant::now() { break; } + + input.wakeup = Some(*wakeup); + input.wakeups.remove(0); + + if !handler(&mut self.fb, &mut input) { + *flow = ControlFlow::Exit; + return; + } + } + + input.wakeup = None; + if input.wait { - *flow = ControlFlow::Wait; + if let Some(wakeup) = input.wakeups.get(0) { + *flow = ControlFlow::WaitUntil(wakeup.when) + } else { + *flow = ControlFlow::Wait; + } // handler only wants to be notified when the input changes if previous_input.as_ref().map_or(true, |p| *p != input) { - if !handler(&mut self.fb, &input) { - *flow = ControlFlow::Exit; + // wakeups have already been handled + if let Event::NewEvents(StartCause::ResumeTimeReached { .. }) = &event { + } else { + if !handler(&mut self.fb, &mut input) { + *flow = ControlFlow::Exit; + } } } } else { // handler wants to be notified regardless - if !handler(&mut self.fb, &input) { + if !handler(&mut self.fb, &mut input) { *flow = ControlFlow::Exit; } else { *flow = ControlFlow::Poll; diff --git a/src/lib.rs b/src/lib.rs index 1f1e7a4..a79889e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -352,7 +352,7 @@ impl MiniGlFb { /// You can cause the handler to exit by returning false from it. This does not kill the /// window, so as long as you still have it in scope, you can actually keep using it and, /// for example, resume handling input but with a different handler callback. - pub fn glutin_handle_basic_input bool>( + pub fn glutin_handle_basic_input bool>( &mut self, event_loop: &mut EventLoop, handler: F ) { self.internal.glutin_handle_basic_input(event_loop, handler);