summaryrefslogtreecommitdiffstats
path: root/examples/game_of_life
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2020-05-02 07:01:27 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2020-05-02 07:01:27 +0200
commit916a1bfc7049867669b81f446e711021d92a4132 (patch)
treee27add840d62fc8c60824f5833f24c746d2a581f /examples/game_of_life
parent8fa9e4c94eb9d6b6e13b45fd6a99209536880a2d (diff)
downloadiced-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.toml1
-rw-r--r--examples/game_of_life/src/main.rs260
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,