diff --git a/Cargo.lock b/Cargo.lock index 8fa80ad..8117e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,6 +435,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etagere" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "euclid" version = "0.22.9" @@ -830,18 +840,17 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#713200714daa16a273eed9fcaee476613e894116" dependencies = [ "ahash", "alot", "appit", "bytemuck", "cosmic-text", + "etagere", "figures", "image", "lyon_tessellation", "pollster", - "shelf-packer", "smallvec", "wgpu", ] @@ -1493,14 +1502,6 @@ dependencies = [ "syn", ] -[[package]] -name = "shelf-packer" -version = "0.1.0" -source = "git+https://github.com/khonsulabs/shelf-packer#06246cb4f5ccd473c1d8ae49c8c07f2f61798df4" -dependencies = [ - "figures", -] - [[package]] name = "slotmap" version = "1.0.6" @@ -1578,6 +1579,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "svg_fmt" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" + [[package]] name = "swash" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 9edaf76..7350bd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ alot = "0.3" interner = "0.2.1" # appit = { git = "https://github.com/khonsulabs/appit" } -# [patch."https://github.com/khonsulabs/kludgine"] -# kludgine = { path = "../kludgine2" } +[patch."https://github.com/khonsulabs/kludgine"] +kludgine = { path = "../kludgine2" } # [patch."https://github.com/khonsulabs/figures"] # figures = { path = "../figures" } diff --git a/examples/tilemap.rs b/examples/tilemap.rs index 5dd214a..ba36298 100644 --- a/examples/tilemap.rs +++ b/examples/tilemap.rs @@ -4,11 +4,9 @@ use gooey::kludgine::figures::units::Px; use gooey::kludgine::figures::{Point, Rect, Size}; use gooey::kludgine::render::Renderer; use gooey::kludgine::shapes::Shape; -use gooey::kludgine::tilemap::{ - Object, ObjectId, ObjectLayer, TileKind, TileMapFocus, Tiles, TILE_SIZE, -}; +use gooey::kludgine::tilemap::{Object, ObjectLayer, TileKind, TileMapFocus, Tiles, TILE_SIZE}; use gooey::kludgine::Color; -use gooey::widget::{EventHandling, HANDLED, UNHANDLED}; +use gooey::tick::Tick; use gooey::widgets::TileMap; use gooey::{EventLoopError, Run}; @@ -35,7 +33,7 @@ fn main() -> Result<(), EventLoopError> { let myself = characters.push(Player { color: Color::RED, - position: Point::new(TILE_SIZE * 1, TILE_SIZE * 1), + position: Point::new(TILE_SIZE.0 as f32, TILE_SIZE.0 as f32), }); let layers = Dynamic::new((Tiles::new(8, 8, TILES), characters)); @@ -45,37 +43,43 @@ fn main() -> Result<(), EventLoopError> { layer: 1, id: myself, }) - .on_key(move |key| handle_key(key, myself, &layers)) + .tick(Tick::fps(60, move |elapsed, input| { + // println!("Ticking {input:?}"); + let mut direction = Point::new(0., 0.); + if input.keys.contains(&Key::ArrowDown) { + direction.y += 1.0; + } + if input.keys.contains(&Key::ArrowUp) { + direction.y -= 1.0; + } + if input.keys.contains(&Key::ArrowRight) { + direction.x += 1.0; + } + if input.keys.contains(&Key::ArrowLeft) { + direction.x -= 1.0; + } + + let one_second_movement = direction * TILE_SIZE.0 as f32; + + layers.map_mut(|layers| { + layers.1[myself].position += Point::new( + one_second_movement.x * elapsed.as_secs_f32(), + one_second_movement.y * elapsed.as_secs_f32(), + ) + }); + })) .run() } -fn handle_key( - key: Key, - player: ObjectId, - layers: &Dynamic<(Tiles, ObjectLayer)>, -) -> EventHandling { - let offset = match key { - Key::ArrowDown => Point::new(Px(0), Px(1)), - Key::ArrowUp => Point::new(Px(0), Px(-1)), - Key::ArrowLeft => Point::new(Px(-1), Px(0)), - Key::ArrowRight => Point::new(Px(1), Px(0)), - _ => return UNHANDLED, - }; - - layers.map_mut(|layers| layers.1[player].position += offset); - - HANDLED -} - #[derive(Debug)] struct Player { color: Color, - position: Point, + position: Point, } impl Object for Player { fn position(&self) -> Point { - self.position + self.position.cast() } fn render(&self, center: Point, zoom: f32, context: &mut Renderer<'_, '_>) { diff --git a/src/lib.rs b/src/lib.rs index 478370e..e770e31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod dynamic; pub mod graphics; pub mod names; pub mod styles; +pub mod tick; mod tree; mod utils; pub mod widget; diff --git a/src/tick.rs b/src/tick.rs new file mode 100644 index 0000000..98c47f1 --- /dev/null +++ b/src/tick.rs @@ -0,0 +1,159 @@ +use std::collections::HashSet; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}; +use std::time::{Duration, Instant}; + +use kludgine::app::winit::event::KeyEvent; +use kludgine::app::winit::keyboard::Key; + +use crate::widget::{EventHandling, HANDLED, UNHANDLED}; + +#[derive(Clone, Debug)] +#[must_use] +pub struct Tick { + data: Arc, + handled_keys: HashSet, +} + +impl Tick { + pub fn rendered(&self) { + self.data.rendered_frame.fetch_add(1, Ordering::AcqRel); + + self.data.sync.notify_one(); + } + + #[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 { + UNHANDLED + } + } +} + +#[derive(Default, Debug)] +pub struct WatchedInput { + pub keys: HashSet, +} + +#[derive(Debug)] +struct TickData { + state: Mutex, + period: Duration, + sync: Condvar, + rendered_frame: AtomicUsize, +} + +impl TickData { + fn state(&self) -> MutexGuard<'_, TickState> { + self.state + .lock() + .map_or_else(PoisonError::into_inner, |g| g) + } +} + +#[derive(Debug)] +struct TickState { + last_time: Instant, + next_target: Instant, + keep_running: bool, + frame: usize, + input: WatchedInput, +} + +impl Tick { + pub fn new(tick_every: Duration, tick: F) -> Self + where + F: FnMut(Duration, &WatchedInput) + 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: WatchedInput::default(), + }), + period: tick_every, + sync: Condvar::new(), + rendered_frame: AtomicUsize::new(0), + }); + + std::thread::spawn({ + let data = data.clone(); + move || tick_loop(&data, tick) + }); + + Self { + data, + handled_keys: HashSet::new(), + } + } + + pub fn fps(frames_per_second: u32, tick: F) -> Self + where + F: FnMut(Duration, &WatchedInput) + Send + 'static, + { + Self::new(Duration::from_secs(1) / frames_per_second, tick) + } + + pub fn handled_keys(mut self, keys: impl IntoIterator) -> Self { + self.handled_keys.extend(keys); + self + } +} + +fn tick_loop(data: &TickData, mut tick: F) +where + F: FnMut(Duration, &WatchedInput), +{ + 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; + // TODO we need a way to batch updates for a context so that during a + // tick, no changed values trigger a redraw until we are done with the + // tick. Otherwise, a frame may start being rendered while we're still + // evaluating the tick since it's in its own thread. + tick(elapsed, &state.input); + state.next_target = (state.next_target + data.period).max(now); + state.last_time = now; + + // 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) + .map_or_else(PoisonError::into_inner, |g| g); + } else { + break; + } + } + } +} diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index 5c3b6cb..bbaf0c4 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -11,6 +11,7 @@ use crate::kludgine::figures::units::UPx; use crate::kludgine::figures::Size; use crate::kludgine::tilemap; use crate::kludgine::tilemap::TileMapFocus; +use crate::tick::Tick; use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; use crate::ConstraintLimit; @@ -21,6 +22,7 @@ pub struct TileMap { focus: Value, key: Option>, zoom: f32, + tick: Option, } impl TileMap { @@ -30,6 +32,7 @@ impl TileMap { focus: Value::Constant(TileMapFocus::default()), zoom: 1., key: None, + tick: None, } } @@ -53,6 +56,11 @@ impl TileMap { self.key = Some(Callback::new(key)); self } + + pub fn tick(mut self, tick: Tick) -> Self { + self.tick = Some(tick); + self + } } impl Widget for TileMap @@ -66,6 +74,10 @@ where let focus = self.focus.get(); self.layers .map(|layers| tilemap::draw(layers, focus, self.zoom, &mut context.graphics)); + + if let Some(tick) = &self.tick { + tick.rendered(); + } } fn measure( @@ -99,35 +111,18 @@ where _device_id: DeviceId, input: KeyEvent, _is_synthetic: bool, - context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_, '_>, ) -> EventHandling { + if let Some(tick) = &self.tick { + tick.key_input(&input)?; + } if !input.state.is_pressed() { return UNHANDLED; } if let Some(on_key) = &mut self.key { on_key.invoke(input.logical_key.clone())?; } - self.focus.map_mut(|focus| { - if let TileMapFocus::Point(focus) = focus { - match input.logical_key { - Key::ArrowLeft => { - focus.x -= 1; - } - Key::ArrowRight => { - focus.x += 1; - } - Key::ArrowUp => { - focus.y -= 1; - } - Key::ArrowDown => { - focus.y += 1; - } - _ => {} - } - } - }); - context.set_needs_redraw(); - HANDLED + UNHANDLED } } diff --git a/src/window.rs b/src/window.rs index c8cce2e..59b0537 100644 --- a/src/window.rs +++ b/src/window.rs @@ -237,9 +237,11 @@ where .is_some(); drop(target); - if !handled && !input.state.is_pressed() { + if !handled { match input.physical_key { - KeyCode::KeyW if window.modifiers().state().primary() => { + KeyCode::KeyW + if window.modifiers().state().primary() && dbg!(input.state).is_pressed() => + { if self.request_close(&mut window) { window.set_needs_redraw(); }