diff options
author | 2020-05-02 07:01:27 +0200 | |
---|---|---|
committer | 2020-05-02 07:01:27 +0200 | |
commit | 916a1bfc7049867669b81f446e711021d92a4132 (patch) | |
tree | e27add840d62fc8c60824f5833f24c746d2a581f /examples/game_of_life | |
parent | 8fa9e4c94eb9d6b6e13b45fd6a99209536880a2d (diff) | |
download | iced-916a1bfc7049867669b81f446e711021d92a4132.tar.gz iced-916a1bfc7049867669b81f446e711021d92a4132.tar.bz2 iced-916a1bfc7049867669b81f446e711021d92a4132.zip |
Run ticks in a background thread in `game_of_life`
Diffstat (limited to 'examples/game_of_life')
-rw-r--r-- | examples/game_of_life/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/game_of_life/src/main.rs | 260 |
2 files changed, 210 insertions, 51 deletions
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 2b945c4c..b9bb7f2a 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -7,5 +7,6 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +tokio = { version = "0.2", features = ["blocking"] } itertools = "0.9" rustc-hash = "1.1" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 88fd3a93..b8cabf24 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,7 +19,7 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, - is_playing: bool, + state: State, speed: u64, next_speed: Option<u64>, toggle_button: button::State, @@ -28,6 +28,17 @@ struct GameOfLife { speed_slider: slider::State, } +enum State { + Paused, + Playing { last_tick: Instant }, +} + +impl Default for State { + fn default() -> State { + State::Paused + } +} + #[derive(Debug, Clone)] enum Message { Grid(grid::Message), @@ -62,37 +73,61 @@ impl Application for GameOfLife { Message::Grid(message) => { self.grid.update(message); } - Message::Tick(_) | Message::Next => { - self.grid.tick(); + Message::Tick(_) | Message::Next => match &mut self.state { + State::Paused => { + if let Some(task) = self.grid.tick(1) { + return Command::perform(task, Message::Grid); + } + } + State::Playing { last_tick } => { + let seconds_elapsed = + last_tick.elapsed().as_millis() as f32 / 1000.0; + + let needed_ticks = + (self.speed as f32 * seconds_elapsed).ceil() as usize; + + if let Some(task) = self.grid.tick(needed_ticks) { + *last_tick = Instant::now(); - if let Some(speed) = self.next_speed.take() { - self.speed = speed; + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + return Command::perform(task, Message::Grid); + } } - } + }, Message::Toggle => { - self.is_playing = !self.is_playing; + self.state = match self.state { + State::Paused => State::Playing { + last_tick: Instant::now(), + }, + State::Playing { .. } => State::Paused, + }; } Message::Clear => { - self.grid = Grid::default(); + self.grid.clear(); } - Message::SpeedChanged(speed) => { - if self.is_playing { - self.next_speed = Some(speed.round() as u64); - } else { + Message::SpeedChanged(speed) => match self.state { + State::Paused => { self.speed = speed.round() as u64; } - } + State::Playing { .. } => { + self.next_speed = Some(speed.round() as u64); + } + }, } Command::none() } fn subscription(&self) -> Subscription<Message> { - if self.is_playing { - time::every(Duration::from_millis(1000 / self.speed)) - .map(Message::Tick) - } else { - Subscription::none() + match self.state { + State::Paused => Subscription::none(), + State::Playing { .. } => { + time::every(Duration::from_millis(1000 / self.speed)) + .map(Message::Tick) + } } } @@ -102,7 +137,11 @@ impl Application for GameOfLife { .push( Button::new( &mut self.toggle_button, - Text::new(if self.is_playing { "Pause" } else { "Play" }), + Text::new(if let State::Paused = self.state { + "Play" + } else { + "Pause" + }), ) .on_press(Message::Toggle) .style(style::Button), @@ -111,11 +150,6 @@ impl Application for GameOfLife { Button::new(&mut self.next_button, Text::new("Next")) .on_press(Message::Next) .style(style::Button), - ) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Button), ); let selected_speed = self.next_speed.unwrap_or(self.speed); @@ -138,10 +172,14 @@ impl Application for GameOfLife { .padding(10) .spacing(20) .push(playback_controls) - .push(speed_controls); + .push(speed_controls) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Button), + ); let content = Column::new() - .spacing(10) .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -160,28 +198,40 @@ mod grid { mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; pub struct Grid { - life: Life, + state: State, interaction: Interaction, cache: Cache, translation: Vector, scaling: f32, + version: usize, } - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone)] pub enum Message { Populate(Cell), + Ticked { + result: Result<Life, TickError>, + version: usize, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, } impl Default for Grid { fn default() -> Self { Self { - life: Life::default(), + state: State::default(), interaction: Interaction::None, cache: Cache::default(), translation: Vector::default(), scaling: 1.0, + version: 0, } } } @@ -190,17 +240,44 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; - pub fn tick(&mut self) { - self.life.tick(); - self.cache.clear() + pub fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Message>> { + use iced::futures::FutureExt; + + let version = self.version; + let tick = self.state.tick(amount)?; + + Some(tick.map(move |result| Message::Ticked { result, version })) + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.cache.clear(); } pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { - self.life.populate(cell); + self.state.populate(cell); + self.cache.clear() + } + Message::Ticked { + result: Ok(life), + version, + } if version == self.version => { + self.state.update(life); self.cache.clear() } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + Message::Ticked { .. } => {} } } @@ -211,11 +288,11 @@ mod grid { .into() } - pub fn visible_region(&self, size: Size) -> Rectangle { + pub fn visible_region(&self, size: Size) -> Region { let width = size.width / self.scaling; let height = size.height / self.scaling; - Rectangle { + Region { x: -self.translation.x - width / 2.0, y: -self.translation.y - height / 2.0, width, @@ -247,7 +324,7 @@ mod grid { let cursor_position = cursor.position_in(&bounds)?; let cell = Cell::at(self.project(cursor_position, bounds.size())); - let populate = if self.life.contains(&cell) { + let populate = if self.state.contains(&cell) { None } else { Some(Message::Populate(cell)) @@ -339,7 +416,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in self.life.within(region) { + for cell in region.view(self.state.cells()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -394,6 +471,63 @@ mod grid { } #[derive(Default)] + struct State { + life: Life, + births: FxHashSet<Cell>, + is_ticking: bool, + } + + impl State { + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator<Item = &Cell> { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Result<Life, TickError>>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } + } + + #[derive(Clone, Default)] pub struct Life { cells: FxHashSet<Cell>, } @@ -433,21 +567,16 @@ mod grid { self.cells.insert(cell); } - fn within(&self, region: Rectangle) -> impl Iterator<Item = &Cell> { - let first_row = (region.y / Cell::SIZE as f32).floor() as isize; - let first_column = (region.x / Cell::SIZE as f32).floor() as isize; - - let visible_rows = - (region.height / Cell::SIZE as f32).ceil() as isize; - let visible_columns = - (region.width / Cell::SIZE as f32).ceil() as isize; - - let rows = first_row..=first_row + visible_rows; - let columns = first_column..=first_column + visible_columns; + pub fn iter(&self) -> impl Iterator<Item = &Cell> { + self.cells.iter() + } + } - self.cells.iter().filter(move |cell| { - rows.contains(&cell.i) && columns.contains(&cell.j) - }) + impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() } } @@ -484,6 +613,35 @@ mod grid { } } + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn view<'a>( + &self, + cells: impl Iterator<Item = &'a Cell>, + ) -> impl Iterator<Item = &'a Cell> { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + let rows = first_row..=first_row + visible_rows; + let columns = first_column..=first_column + visible_columns; + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + enum Interaction { None, Drawing, |