use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::{Duration, Instant}; use ahash::AHashSet; use intentional::Assert; use kludgine::app::winit::event::{ElementState, KeyEvent, MouseButton}; use kludgine::app::winit::keyboard::Key; use kludgine::figures::units::Px; use kludgine::figures::Point; use crate::context::WidgetContext; use crate::utils::{IgnorePoison, UnwindsafeCondvar}; use crate::value::Dynamic; use crate::widget::{EventHandling, HANDLED, IGNORED}; /// A fixed-rate callback that provides access to tracked input on its /// associated widget. #[derive(Clone, Debug)] #[must_use] pub struct Tick { data: Arc, handled_keys: AHashSet, } impl Tick { /// Signals that this widget has been redrawn. pub fn rendered(&self, context: &WidgetContext<'_, '_>) { context.redraw_when_changed(&self.data.tick_number); self.data.sync.notify_one(); } /// Processes `input`. /// /// If the event matches a key that has been marked as handled, [`HANDLED`] /// will be returned. Otherwise, [`IGNORED`] will be returned, #[must_use] pub fn key_input(&self, input: &KeyEvent) -> EventHandling { let mut state = self.data.state(); if input.state.is_pressed() { state.input.keys.insert(input.logical_key.clone()); } else { state.input.keys.remove(&input.logical_key); } drop(state); if self.handled_keys.contains(&input.logical_key) { HANDLED } else { IGNORED } } /// Sets the cursor position. pub fn set_cursor_position(&self, pos: Option>) { let mut state = self.data.state(); match pos { Some(pos) => { if state.input.mouse.is_none() { state.input.mouse = Some(Mouse::default()); } state .input .mouse .as_mut() .assert("always initialized") .position = pos; } None => { state.input.mouse = None; } } } /// Processes a mouse button event. pub fn mouse_button(&self, button: MouseButton, button_state: ElementState) { let mut state = self.data.state(); if let Some(mouse) = &mut state.input.mouse { if button_state.is_pressed() { mouse.buttons.insert(button); } else { mouse.buttons.remove(&button); } } } /// Returns a new tick that invokes `tick`, aiming to repeat at the given /// duration. pub fn new(tick_every: Duration, tick: F) -> Self where F: FnMut(Duration, &InputState) + Send + 'static, { let now = Instant::now(); let data = Arc::new(TickData { state: Mutex::new(TickState { last_time: now, next_target: now, keep_running: true, frame: 0, input: InputState::default(), }), period: tick_every, sync: UnwindsafeCondvar::new(), rendered_frame: AtomicUsize::new(0), tick_number: Dynamic::default(), }); std::thread::spawn({ let data = data.clone(); move || tick_loop(&data, tick) }); Self { data, handled_keys: AHashSet::new(), } } /// Returns a new tick that invokes `tick` at a target number of times per /// second. pub fn times_per_second(times_per_second: u32, tick: F) -> Self where F: FnMut(Duration, &InputState) + Send + 'static, { Self::new(Duration::from_secs(1) / times_per_second, tick) } /// Returns a new tick that redraws its associated widget at a target rate /// of `x times_per_second`. pub fn redraws_per_second(times_per_second: u32) -> Self { Self::times_per_second(times_per_second, |_, _| {}) } /// Adds the collection of [`Key`]s to the list that are handled, and /// returns self. /// /// The list of keys provided will be prevented from propagating. pub fn handled_keys(mut self, keys: impl IntoIterator) -> Self { self.handled_keys.extend(keys); self } } /// The current state of input during the execution of a [`Tick`]. #[derive(Default, Debug)] pub struct InputState { /// A collection of all keys currently pressed. pub keys: AHashSet, /// The state of the mouse cursor and any buttons pressed. pub mouse: Option, } #[derive(Debug, Default)] pub struct Mouse { pub position: Point, pub buttons: AHashSet, } #[derive(Debug)] struct TickData { state: Mutex, period: Duration, sync: UnwindsafeCondvar, rendered_frame: AtomicUsize, tick_number: Dynamic, } impl TickData { fn state(&self) -> MutexGuard<'_, TickState> { self.state.lock().ignore_poison() } } #[derive(Debug)] struct TickState { last_time: Instant, next_target: Instant, keep_running: bool, frame: usize, input: InputState, } fn tick_loop(data: &TickData, mut tick: F) where F: FnMut(Duration, &InputState), { let mut state = data.state(); while state.keep_running { let mut now = Instant::now(); match state.next_target.checked_duration_since(now) { Some(remaining) if remaining > Duration::ZERO => { drop(state); std::thread::sleep(remaining); state = data.state(); now = Instant::now(); } _ => {} } let elapsed = now .checked_duration_since(state.last_time) .expect("instant never decreases"); state.frame += 1; tick(elapsed, &state.input); state.next_target = (state.next_target + data.period).max(now); state.last_time = now; // Signal that we have a new frame, which will cause the widget to // redraw. data.tick_number.map_mut(|tick| *tick += 1); // Wait for a frame to be rendered. while state.keep_running { let current_frame = data.rendered_frame.load(Ordering::Acquire); if state.frame == current_frame { state = data.sync.wait(state).ignore_poison(); } else { break; } } } }