From 70f86f998b6db102d5b77f756750414efd53aa9e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 08:25:42 +0200 Subject: Add `game_of_life` example RIP John Conway --- examples/game_of_life/src/main.rs | 408 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 examples/game_of_life/src/main.rs (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..3e6848df --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,408 @@ +//! This example showcases an interactive version of the Game of Life, invented +//! by John Conway. It leverages a `Canvas` together with other widgets. +mod style; +mod time; + +use grid::Grid; +use iced::{ + button::{self, Button}, + executor, + slider::{self, Slider}, + Align, Application, Column, Command, Container, Element, Length, Row, + Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + is_playing: bool, + speed: u64, + next_speed: Option, + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message), + Tick(Instant), + Toggle, + Next, + Clear, + SpeedChanged(f32), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + ( + Self { + speed: 1, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Grid(message) => { + self.grid.update(message); + } + Message::Tick(_) | Message::Next => { + self.grid.tick(); + + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + } + Message::Toggle => { + self.is_playing = !self.is_playing; + } + Message::Clear => { + self.grid = Grid::default(); + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as u64); + } else { + self.speed = speed.round() as u64; + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if self.is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::Toggle) + .style(style::Button), + ) + .push( + 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); + let speed_controls = Row::new() + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=20.0, + selected_speed as f32, + Message::SpeedChanged, + ) + .width(Length::Units(200)) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", selected_speed)).size(16)) + .align_items(Align::Center); + + let controls = Row::new() + .spacing(20) + .push(playback_controls) + .push(speed_controls); + + let content = Column::new() + .spacing(10) + .padding(10) + .align_items(Align::Center) + .push(self.grid.view().map(Message::Grid)) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +mod grid { + use iced::{ + canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, + mouse, ButtonState, Color, Element, Length, MouseCursor, Point, + Rectangle, Size, Vector, + }; + + const SIZE: usize = 32; + + #[derive(Default)] + pub struct Grid { + cells: [[Cell; SIZE]; SIZE], + mouse_pressed: bool, + cache: canvas::Cache, + } + + impl Grid { + pub fn tick(&mut self) { + let mut populated_neighbors: [[usize; SIZE]; SIZE] = + [[0; SIZE]; SIZE]; + + for (i, row) in self.cells.iter().enumerate() { + for (j, _) in row.iter().enumerate() { + populated_neighbors[i][j] = self.populated_neighbors(i, j); + } + } + + for (i, row) in populated_neighbors.iter().enumerate() { + for (j, amount) in row.iter().enumerate() { + let is_populated = self.cells[i][j] == Cell::Populated; + + self.cells[i][j] = match amount { + 2 if is_populated => Cell::Populated, + 3 => Cell::Populated, + _ => Cell::Unpopulated, + }; + } + } + + self.cache.clear() + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate { i, j } => { + self.cells[i][j] = Cell::Populated; + self.cache.clear() + } + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn populated_neighbors(&self, row: usize, column: usize) -> usize { + use itertools::Itertools; + + let rows = row.saturating_sub(1)..=row + 1; + let columns = column.saturating_sub(1)..=column + 1; + + let is_inside_bounds = |i: usize, j: usize| i < SIZE && j < SIZE; + let is_neighbor = |i: usize, j: usize| i != row || j != column; + + let is_populated = + |i: usize, j: usize| self.cells[i][j] == Cell::Populated; + + rows.cartesian_product(columns) + .filter(|&(i, j)| { + is_inside_bounds(i, j) + && is_neighbor(i, j) + && is_populated(i, j) + }) + .count() + } + + fn region(&self, size: Size) -> Rectangle { + let side = size.width.min(size.height); + + Rectangle { + x: (size.width - side) / 2.0, + y: (size.height - side) / 2.0, + width: side, + height: side, + } + } + + fn cell_at( + &self, + region: Rectangle, + position: Point, + ) -> Option<(usize, usize)> { + if region.contains(position) { + let cell_size = region.width / SIZE as f32; + + let i = ((position.y - region.y) / cell_size).ceil() as usize; + let j = ((position.x - region.x) / cell_size).ceil() as usize; + + Some((i.saturating_sub(1), j.saturating_sub(1))) + } else { + None + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Cell { + Unpopulated, + Populated, + } + + impl Default for Cell { + fn default() -> Cell { + Cell::Unpopulated + } + } + + #[derive(Debug, Clone, Copy)] + pub enum Message { + Populate { i: usize, j: usize }, + } + + impl<'a> canvas::Program for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + if let Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + state, + }) = event + { + self.mouse_pressed = state == ButtonState::Pressed; + } + + let cursor_position = cursor.internal_position(&bounds)?; + + let region = self.region(bounds.size()); + let (i, j) = self.cell_at(region, cursor_position)?; + + let populate = if self.cells[i][j] != Cell::Populated { + Some(Message::Populate { i, j }) + } else { + None + }; + + match event { + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + .. + }) if self.mouse_pressed => populate, + Event::Mouse(mouse::Event::CursorMoved { .. }) + if self.mouse_pressed => + { + populate + } + _ => None, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + let region = self.region(bounds.size()); + let cell_size = Size::new(1.0, 1.0); + + let life = self.cache.draw(bounds.size(), |frame| { + let background = + Path::rectangle(region.position(), region.size()); + frame.fill( + &background, + Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ), + ); + + frame.with_save(|frame| { + frame.translate(Vector::new(region.x, region.y)); + frame.scale(region.width / SIZE as f32); + + let cells = Path::new(|p| { + for (i, row) in self.cells.iter().enumerate() { + for (j, cell) in row.iter().enumerate() { + if *cell == Cell::Populated { + p.rectangle( + Point::new(j as f32, i as f32), + cell_size, + ); + } + } + } + }); + frame.fill(&cells, Color::WHITE); + }); + }); + + let hovered_cell = { + let mut frame = Frame::new(bounds.size()); + + frame.translate(Vector::new(region.x, region.y)); + frame.scale(region.width / SIZE as f32); + + if let Some(cursor_position) = cursor.internal_position(&bounds) + { + if let Some((i, j)) = self.cell_at(region, cursor_position) + { + let interaction = Path::rectangle( + Point::new(j as f32, i as f32), + cell_size, + ); + + frame.fill( + &interaction, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + } + } + + frame.into_geometry() + }; + + vec![life, hovered_cell] + } + + fn mouse_cursor( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> MouseCursor { + let region = self.region(bounds.size()); + + match cursor.internal_position(&bounds) { + Some(position) if region.contains(position) => { + MouseCursor::Crosshair + } + _ => MouseCursor::default(), + } + } + } +} -- cgit From 5d12e194f45b4a01034f3f52fae16c10bc0192dd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 20:58:59 +0200 Subject: Rename `Cursor::*_position` methods in `canvas` --- examples/game_of_life/src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3e6848df..a2628594 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -298,7 +298,7 @@ mod grid { self.mouse_pressed = state == ButtonState::Pressed; } - let cursor_position = cursor.internal_position(&bounds)?; + let cursor_position = cursor.position_in(&bounds)?; let region = self.region(bounds.size()); let (i, j) = self.cell_at(region, cursor_position)?; @@ -365,8 +365,7 @@ mod grid { frame.translate(Vector::new(region.x, region.y)); frame.scale(region.width / SIZE as f32); - if let Some(cursor_position) = cursor.internal_position(&bounds) - { + if let Some(cursor_position) = cursor.position_in(&bounds) { if let Some((i, j)) = self.cell_at(region, cursor_position) { let interaction = Path::rectangle( @@ -397,7 +396,7 @@ mod grid { ) -> MouseCursor { let region = self.region(bounds.size()); - match cursor.internal_position(&bounds) { + match cursor.position_in(&bounds) { Some(position) if region.contains(position) => { MouseCursor::Crosshair } -- cgit From 5e014a70e864964570e4c1568926b8c647f73c59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 23:50:15 +0200 Subject: Use sparse grid representation in `game_of_life` --- examples/game_of_life/src/main.rs | 137 +++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 68 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index a2628594..fb12afa1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -14,10 +14,7 @@ use iced::{ use std::time::{Duration, Instant}; pub fn main() { - GameOfLife::run(Settings { - antialiasing: true, - ..Settings::default() - }) + GameOfLife::run(Settings::default()) } #[derive(Default)] @@ -164,36 +161,55 @@ mod grid { mouse, ButtonState, Color, Element, Length, MouseCursor, Point, Rectangle, Size, Vector, }; + use std::collections::{HashMap, HashSet}; - const SIZE: usize = 32; + const CELL_SIZE: usize = 20; #[derive(Default)] pub struct Grid { - cells: [[Cell; SIZE]; SIZE], + alive_cells: HashSet<(usize, usize)>, mouse_pressed: bool, cache: canvas::Cache, } impl Grid { - pub fn tick(&mut self) { - let mut populated_neighbors: [[usize; SIZE]; SIZE] = - [[0; SIZE]; SIZE]; + fn with_neighbors( + i: usize, + j: usize, + ) -> impl Iterator { + use itertools::Itertools; - for (i, row) in self.cells.iter().enumerate() { - for (j, _) in row.iter().enumerate() { - populated_neighbors[i][j] = self.populated_neighbors(i, j); - } - } + let rows = i.saturating_sub(1)..=i.saturating_add(1); + let columns = j.saturating_sub(1)..=j.saturating_add(1); - for (i, row) in populated_neighbors.iter().enumerate() { - for (j, amount) in row.iter().enumerate() { - let is_populated = self.cells[i][j] == Cell::Populated; + rows.cartesian_product(columns) + } - self.cells[i][j] = match amount { - 2 if is_populated => Cell::Populated, - 3 => Cell::Populated, - _ => Cell::Unpopulated, - }; + pub fn tick(&mut self) { + use itertools::Itertools; + + let populated_neighbors: HashMap<(usize, usize), usize> = self + .alive_cells + .iter() + .flat_map(|&(i, j)| Self::with_neighbors(i, j)) + .unique() + .map(|(i, j)| ((i, j), self.populated_neighbors(i, j))) + .collect(); + + for (&(i, j), amount) in populated_neighbors.iter() { + let is_populated = self.alive_cells.contains(&(i, j)); + + match amount { + 2 if is_populated => {} + 3 => { + if !is_populated { + self.alive_cells.insert((i, j)); + } + } + _ if is_populated => { + self.alive_cells.remove(&(i, j)); + } + _ => {} } } @@ -202,8 +218,8 @@ mod grid { pub fn update(&mut self, message: Message) { match message { - Message::Populate { i, j } => { - self.cells[i][j] = Cell::Populated; + Message::Populate { cell } => { + self.alive_cells.insert(cell); self.cache.clear() } } @@ -217,34 +233,28 @@ mod grid { } fn populated_neighbors(&self, row: usize, column: usize) -> usize { - use itertools::Itertools; + let with_neighbors = Self::with_neighbors(row, column); - let rows = row.saturating_sub(1)..=row + 1; - let columns = column.saturating_sub(1)..=column + 1; - - let is_inside_bounds = |i: usize, j: usize| i < SIZE && j < SIZE; let is_neighbor = |i: usize, j: usize| i != row || j != column; - let is_populated = - |i: usize, j: usize| self.cells[i][j] == Cell::Populated; + |i: usize, j: usize| self.alive_cells.contains(&(i, j)); - rows.cartesian_product(columns) - .filter(|&(i, j)| { - is_inside_bounds(i, j) - && is_neighbor(i, j) - && is_populated(i, j) - }) + with_neighbors + .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) .count() } fn region(&self, size: Size) -> Rectangle { - let side = size.width.min(size.height); + let width = + (size.width / CELL_SIZE as f32).floor() * CELL_SIZE as f32; + let height = + (size.height / CELL_SIZE as f32).floor() * CELL_SIZE as f32; Rectangle { - x: (size.width - side) / 2.0, - y: (size.height - side) / 2.0, - width: side, - height: side, + x: (size.width - width) / 2.0, + y: (size.height - height) / 2.0, + width, + height, } } @@ -254,10 +264,10 @@ mod grid { position: Point, ) -> Option<(usize, usize)> { if region.contains(position) { - let cell_size = region.width / SIZE as f32; - - let i = ((position.y - region.y) / cell_size).ceil() as usize; - let j = ((position.x - region.x) / cell_size).ceil() as usize; + let i = ((position.y - region.y) / CELL_SIZE as f32).ceil() + as usize; + let j = ((position.x - region.x) / CELL_SIZE as f32).ceil() + as usize; Some((i.saturating_sub(1), j.saturating_sub(1))) } else { @@ -266,21 +276,9 @@ mod grid { } } - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - enum Cell { - Unpopulated, - Populated, - } - - impl Default for Cell { - fn default() -> Cell { - Cell::Unpopulated - } - } - #[derive(Debug, Clone, Copy)] pub enum Message { - Populate { i: usize, j: usize }, + Populate { cell: (usize, usize) }, } impl<'a> canvas::Program for Grid { @@ -301,12 +299,12 @@ mod grid { let cursor_position = cursor.position_in(&bounds)?; let region = self.region(bounds.size()); - let (i, j) = self.cell_at(region, cursor_position)?; + let cell = self.cell_at(region, cursor_position)?; - let populate = if self.cells[i][j] != Cell::Populated { - Some(Message::Populate { i, j }) - } else { + let populate = if self.alive_cells.contains(&cell) { None + } else { + Some(Message::Populate { cell }) }; match event { @@ -339,14 +337,17 @@ mod grid { ), ); + let visible_rows = region.height as usize / CELL_SIZE; + let visible_columns = region.width as usize / CELL_SIZE; + frame.with_save(|frame| { frame.translate(Vector::new(region.x, region.y)); - frame.scale(region.width / SIZE as f32); + frame.scale(CELL_SIZE as f32); let cells = Path::new(|p| { - for (i, row) in self.cells.iter().enumerate() { - for (j, cell) in row.iter().enumerate() { - if *cell == Cell::Populated { + for i in 0..visible_rows { + for j in 0..visible_columns { + if self.alive_cells.contains(&(i, j)) { p.rectangle( Point::new(j as f32, i as f32), cell_size, @@ -363,7 +364,7 @@ mod grid { let mut frame = Frame::new(bounds.size()); frame.translate(Vector::new(region.x, region.y)); - frame.scale(region.width / SIZE as f32); + frame.scale(CELL_SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { if let Some((i, j)) = self.cell_at(region, cursor_position) -- cgit From 611d9e399c95268a3daf41bd6cbcc55391ccff87 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 23:55:15 +0200 Subject: Clarify `tick` logic in `game_of_life` --- examples/game_of_life/src/main.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index fb12afa1..3989e3ea 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -200,14 +200,12 @@ mod grid { let is_populated = self.alive_cells.contains(&(i, j)); match amount { - 2 if is_populated => {} + 2 | 3 if is_populated => {} 3 => { - if !is_populated { - self.alive_cells.insert((i, j)); - } + let _ = self.alive_cells.insert((i, j)); } _ if is_populated => { - self.alive_cells.remove(&(i, j)); + let _ = self.alive_cells.remove(&(i, j)); } _ => {} } -- cgit From af95d3972e4ab6bf4ace54ddd44379ffcebbcff6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 04:12:13 +0200 Subject: Implement camera panning in `game_of_life` example --- examples/game_of_life/src/main.rs | 165 +++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 75 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3989e3ea..983d6cb4 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -136,13 +136,13 @@ impl Application for GameOfLife { .align_items(Align::Center); let controls = Row::new() + .padding(10) .spacing(20) .push(playback_controls) .push(speed_controls); let content = Column::new() .spacing(10) - .padding(10) .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -167,16 +167,27 @@ mod grid { #[derive(Default)] pub struct Grid { - alive_cells: HashSet<(usize, usize)>, - mouse_pressed: bool, + alive_cells: HashSet<(isize, isize)>, + interaction: Option, cache: canvas::Cache, + translation: Vector, + } + + #[derive(Debug, Clone, Copy)] + pub enum Message { + Populate { cell: (isize, isize) }, + } + + enum Interaction { + Drawing, + Panning { translation: Vector, start: Point }, } impl Grid { fn with_neighbors( - i: usize, - j: usize, - ) -> impl Iterator { + i: isize, + j: isize, + ) -> impl Iterator { use itertools::Itertools; let rows = i.saturating_sub(1)..=i.saturating_add(1); @@ -188,7 +199,7 @@ mod grid { pub fn tick(&mut self) { use itertools::Itertools; - let populated_neighbors: HashMap<(usize, usize), usize> = self + let populated_neighbors: HashMap<(isize, isize), usize> = self .alive_cells .iter() .flat_map(|&(i, j)| Self::with_neighbors(i, j)) @@ -230,55 +241,26 @@ mod grid { .into() } - fn populated_neighbors(&self, row: usize, column: usize) -> usize { + fn populated_neighbors(&self, row: isize, column: isize) -> usize { let with_neighbors = Self::with_neighbors(row, column); - let is_neighbor = |i: usize, j: usize| i != row || j != column; + let is_neighbor = |i: isize, j: isize| i != row || j != column; let is_populated = - |i: usize, j: usize| self.alive_cells.contains(&(i, j)); + |i: isize, j: isize| self.alive_cells.contains(&(i, j)); with_neighbors .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) .count() } - fn region(&self, size: Size) -> Rectangle { - let width = - (size.width / CELL_SIZE as f32).floor() * CELL_SIZE as f32; - let height = - (size.height / CELL_SIZE as f32).floor() * CELL_SIZE as f32; - - Rectangle { - x: (size.width - width) / 2.0, - y: (size.height - height) / 2.0, - width, - height, - } - } + fn cell_at(&self, position: Point) -> Option<(isize, isize)> { + let i = (position.y / CELL_SIZE as f32).ceil() as isize; + let j = (position.x / CELL_SIZE as f32).ceil() as isize; - fn cell_at( - &self, - region: Rectangle, - position: Point, - ) -> Option<(usize, usize)> { - if region.contains(position) { - let i = ((position.y - region.y) / CELL_SIZE as f32).ceil() - as usize; - let j = ((position.x - region.x) / CELL_SIZE as f32).ceil() - as usize; - - Some((i.saturating_sub(1), j.saturating_sub(1))) - } else { - None - } + Some((i.saturating_sub(1), j.saturating_sub(1))) } } - #[derive(Debug, Clone, Copy)] - pub enum Message { - Populate { cell: (usize, usize) }, - } - impl<'a> canvas::Program for Grid { fn update( &mut self, @@ -287,17 +269,15 @@ mod grid { cursor: Cursor, ) -> Option { if let Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, + state: ButtonState::Released, + .. }) = event { - self.mouse_pressed = state == ButtonState::Pressed; + self.interaction = None; } let cursor_position = cursor.position_in(&bounds)?; - - let region = self.region(bounds.size()); - let cell = self.cell_at(region, cursor_position)?; + let cell = self.cell_at(cursor_position - self.translation)?; let populate = if self.alive_cells.contains(&cell) { None @@ -306,26 +286,53 @@ mod grid { }; match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - .. - }) if self.mouse_pressed => populate, - Event::Mouse(mouse::Event::CursorMoved { .. }) - if self.mouse_pressed => - { - populate - } - _ => None, + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::Input { + button, + state: ButtonState::Pressed, + } => match button { + mouse::Button::Left => { + self.interaction = Some(Interaction::Drawing); + + populate + } + mouse::Button::Right => { + self.interaction = Some(Interaction::Panning { + translation: self.translation, + start: cursor_position, + }); + + None + } + _ => None, + }, + mouse::Event::CursorMoved { .. } => { + match self.interaction { + Some(Interaction::Drawing) => populate, + Some(Interaction::Panning { + translation, + start, + }) => { + self.translation = + translation + (cursor_position - start); + + self.cache.clear(); + + None + } + _ => None, + } + } + _ => None, + }, } } fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { - let region = self.region(bounds.size()); let cell_size = Size::new(1.0, 1.0); let life = self.cache.draw(bounds.size(), |frame| { - let background = - Path::rectangle(region.position(), region.size()); + let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill( &background, Color::from_rgb( @@ -335,16 +342,25 @@ mod grid { ), ); - let visible_rows = region.height as usize / CELL_SIZE; - let visible_columns = region.width as usize / CELL_SIZE; + let first_row = + (-self.translation.y / CELL_SIZE as f32).floor() as isize; + let first_column = + (-self.translation.x / CELL_SIZE as f32).floor() as isize; + + let visible_rows = + (frame.height() / CELL_SIZE as f32).ceil() as isize; + let visible_columns = + (frame.width() / CELL_SIZE as f32).ceil() as isize; frame.with_save(|frame| { - frame.translate(Vector::new(region.x, region.y)); + frame.translate(self.translation); frame.scale(CELL_SIZE as f32); let cells = Path::new(|p| { - for i in 0..visible_rows { - for j in 0..visible_columns { + for i in first_row..=(first_row + visible_rows) { + for j in + first_column..=(first_column + visible_columns) + { if self.alive_cells.contains(&(i, j)) { p.rectangle( Point::new(j as f32, i as f32), @@ -361,11 +377,12 @@ mod grid { let hovered_cell = { let mut frame = Frame::new(bounds.size()); - frame.translate(Vector::new(region.x, region.y)); + frame.translate(self.translation); frame.scale(CELL_SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - if let Some((i, j)) = self.cell_at(region, cursor_position) + if let Some((i, j)) = + self.cell_at(cursor_position - self.translation) { let interaction = Path::rectangle( Point::new(j as f32, i as f32), @@ -393,12 +410,10 @@ mod grid { bounds: Rectangle, cursor: Cursor, ) -> MouseCursor { - let region = self.region(bounds.size()); - - match cursor.position_in(&bounds) { - Some(position) if region.contains(position) => { - MouseCursor::Crosshair - } + match self.interaction { + Some(Interaction::Drawing) => MouseCursor::Crosshair, + Some(Interaction::Panning { .. }) => MouseCursor::Grabbing, + None if cursor.is_over(&bounds) => MouseCursor::Crosshair, _ => MouseCursor::default(), } } -- cgit From e55cd9652e7c7aea4dc2c6ccb83769246d1a808e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 04:53:15 +0200 Subject: Split `Input` mouse event by `ButtonState` --- examples/game_of_life/src/main.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 983d6cb4..9fb4c3e7 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -158,8 +158,8 @@ impl Application for GameOfLife { mod grid { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, ButtonState, Color, Element, Length, MouseCursor, Point, - Rectangle, Size, Vector, + mouse, Color, Element, Length, MouseCursor, Point, Rectangle, Size, + Vector, }; use std::collections::{HashMap, HashSet}; @@ -268,11 +268,7 @@ mod grid { bounds: Rectangle, cursor: Cursor, ) -> Option { - if let Event::Mouse(mouse::Event::Input { - state: ButtonState::Released, - .. - }) = event - { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { self.interaction = None; } @@ -287,10 +283,7 @@ mod grid { match event { Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::Input { - button, - state: ButtonState::Pressed, - } => match button { + mouse::Event::ButtonPressed(button) => match button { mouse::Button::Left => { self.interaction = Some(Interaction::Drawing); -- cgit From e2076612cb98d04a8a48add14d0068c2974d5653 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 05:37:44 +0200 Subject: Implement `time::every` in `iced_futures` --- examples/game_of_life/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9fb4c3e7..5a58a8cb 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,14 +1,13 @@ //! This example showcases an interactive version of the Game of Life, invented //! by John Conway. It leverages a `Canvas` together with other widgets. mod style; -mod time; use grid::Grid; use iced::{ button::{self, Button}, executor, slider::{self, Slider}, - Align, Application, Column, Command, Container, Element, Length, Row, + time, Align, Application, Column, Command, Container, Element, Length, Row, Settings, Subscription, Text, }; use std::time::{Duration, Instant}; -- cgit From 98bc8cf2a7c4944d762a0148ca9f615d6ccc0d6e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 08:16:38 +0200 Subject: Rename `MouseCursor` to `mouse::Interaction` --- examples/game_of_life/src/main.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 5a58a8cb..f0891db1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -157,8 +157,7 @@ impl Application for GameOfLife { mod grid { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, Color, Element, Length, MouseCursor, Point, Rectangle, Size, - Vector, + mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use std::collections::{HashMap, HashSet}; @@ -397,16 +396,20 @@ mod grid { vec![life, hovered_cell] } - fn mouse_cursor( + fn mouse_interaction( &self, bounds: Rectangle, cursor: Cursor, - ) -> MouseCursor { + ) -> mouse::Interaction { match self.interaction { - Some(Interaction::Drawing) => MouseCursor::Crosshair, - Some(Interaction::Panning { .. }) => MouseCursor::Grabbing, - None if cursor.is_over(&bounds) => MouseCursor::Crosshair, - _ => MouseCursor::default(), + Some(Interaction::Drawing) => mouse::Interaction::Crosshair, + Some(Interaction::Panning { .. }) => { + mouse::Interaction::Grabbing + } + None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), } } } -- cgit From ee97887409849395ecfd63e499c5d5372b121aa3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 00:50:40 +0200 Subject: Introduce `Cell` type in `game_of_life` --- examples/game_of_life/src/main.rs | 182 ++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 85 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index f0891db1..9f43b56a 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -161,11 +161,9 @@ mod grid { }; use std::collections::{HashMap, HashSet}; - const CELL_SIZE: usize = 20; - #[derive(Default)] pub struct Grid { - alive_cells: HashSet<(isize, isize)>, + life: HashSet, interaction: Option, cache: canvas::Cache, translation: Vector, @@ -173,7 +171,7 @@ mod grid { #[derive(Debug, Clone, Copy)] pub enum Message { - Populate { cell: (isize, isize) }, + Populate(Cell), } enum Interaction { @@ -182,41 +180,26 @@ mod grid { } impl Grid { - fn with_neighbors( - i: isize, - j: isize, - ) -> impl Iterator { - use itertools::Itertools; - - let rows = i.saturating_sub(1)..=i.saturating_add(1); - let columns = j.saturating_sub(1)..=j.saturating_add(1); - - rows.cartesian_product(columns) - } - pub fn tick(&mut self) { use itertools::Itertools; - let populated_neighbors: HashMap<(isize, isize), usize> = self - .alive_cells + let populated_neighbors: HashMap = self + .life .iter() - .flat_map(|&(i, j)| Self::with_neighbors(i, j)) + .flat_map(Cell::cluster) .unique() - .map(|(i, j)| ((i, j), self.populated_neighbors(i, j))) + .map(|cell| (cell, self.count_adjacent_life(cell))) .collect(); - for (&(i, j), amount) in populated_neighbors.iter() { - let is_populated = self.alive_cells.contains(&(i, j)); - + for (cell, amount) in populated_neighbors.iter() { match amount { - 2 | 3 if is_populated => {} + 2 => {} 3 => { - let _ = self.alive_cells.insert((i, j)); + let _ = self.life.insert(*cell); } - _ if is_populated => { - let _ = self.alive_cells.remove(&(i, j)); + _ => { + let _ = self.life.remove(cell); } - _ => {} } } @@ -225,8 +208,8 @@ mod grid { pub fn update(&mut self, message: Message) { match message { - Message::Populate { cell } => { - self.alive_cells.insert(cell); + Message::Populate(cell) => { + self.life.insert(cell); self.cache.clear() } } @@ -239,24 +222,16 @@ mod grid { .into() } - fn populated_neighbors(&self, row: isize, column: isize) -> usize { - let with_neighbors = Self::with_neighbors(row, column); + fn count_adjacent_life(&self, cell: Cell) -> usize { + let cluster = Cell::cluster(&cell); - let is_neighbor = |i: isize, j: isize| i != row || j != column; - let is_populated = - |i: isize, j: isize| self.alive_cells.contains(&(i, j)); + let is_neighbor = |candidate| candidate != cell; + let is_populated = |cell| self.life.contains(&cell); - with_neighbors - .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) + cluster + .filter(|&cell| is_neighbor(cell) && is_populated(cell)) .count() } - - fn cell_at(&self, position: Point) -> Option<(isize, isize)> { - let i = (position.y / CELL_SIZE as f32).ceil() as isize; - let j = (position.x / CELL_SIZE as f32).ceil() as isize; - - Some((i.saturating_sub(1), j.saturating_sub(1))) - } } impl<'a> canvas::Program for Grid { @@ -271,12 +246,12 @@ mod grid { } let cursor_position = cursor.position_in(&bounds)?; - let cell = self.cell_at(cursor_position - self.translation)?; + let cell = Cell::at(cursor_position - self.translation); - let populate = if self.alive_cells.contains(&cell) { + let populate = if self.life.contains(&cell) { None } else { - Some(Message::Populate { cell }) + Some(Message::Populate(cell)) }; match event { @@ -333,31 +308,24 @@ mod grid { ), ); - let first_row = - (-self.translation.y / CELL_SIZE as f32).floor() as isize; - let first_column = - (-self.translation.x / CELL_SIZE as f32).floor() as isize; - - let visible_rows = - (frame.height() / CELL_SIZE as f32).ceil() as isize; - let visible_columns = - (frame.width() / CELL_SIZE as f32).ceil() as isize; - frame.with_save(|frame| { frame.translate(self.translation); - frame.scale(CELL_SIZE as f32); + frame.scale(Cell::SIZE as f32); let cells = Path::new(|p| { - for i in first_row..=(first_row + visible_rows) { - for j in - first_column..=(first_column + visible_columns) - { - if self.alive_cells.contains(&(i, j)) { - p.rectangle( - Point::new(j as f32, i as f32), - cell_size, - ); - } + let region = Rectangle { + x: -self.translation.x, + y: -self.translation.y, + width: frame.width(), + height: frame.height(), + }; + + for cell in Cell::all_visible_in(region) { + if self.life.contains(&cell) { + p.rectangle( + Point::new(cell.j as f32, cell.i as f32), + cell_size, + ); } } }); @@ -369,25 +337,23 @@ mod grid { let mut frame = Frame::new(bounds.size()); frame.translate(self.translation); - frame.scale(CELL_SIZE as f32); + frame.scale(Cell::SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - if let Some((i, j)) = - self.cell_at(cursor_position - self.translation) - { - let interaction = Path::rectangle( - Point::new(j as f32, i as f32), - cell_size, - ); - - frame.fill( - &interaction, - Color { - a: 0.5, - ..Color::BLACK - }, - ); - } + let cell = Cell::at(cursor_position - self.translation); + + let interaction = Path::rectangle( + Point::new(cell.j as f32, cell.i as f32), + cell_size, + ); + + frame.fill( + &interaction, + Color { + a: 0.5, + ..Color::BLACK + }, + ); } frame.into_geometry() @@ -413,4 +379,50 @@ mod grid { } } } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: &Cell) -> impl Iterator { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn all_visible_in(region: Rectangle) -> impl Iterator { + use itertools::Itertools; + + 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; + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + } } -- cgit From 71323c51bbdcb7dcccd6249fcd4ea3b1df589a9b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 00:54:43 +0200 Subject: Simplify `Interaction` handling in `game_of_life` --- examples/game_of_life/src/main.rs | 42 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9f43b56a..8a841c91 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -164,7 +164,7 @@ mod grid { #[derive(Default)] pub struct Grid { life: HashSet, - interaction: Option, + interaction: Interaction, cache: canvas::Cache, translation: Vector, } @@ -174,11 +174,6 @@ mod grid { Populate(Cell), } - enum Interaction { - Drawing, - Panning { translation: Vector, start: Point }, - } - impl Grid { pub fn tick(&mut self) { use itertools::Itertools; @@ -242,7 +237,7 @@ mod grid { cursor: Cursor, ) -> Option { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - self.interaction = None; + self.interaction = Interaction::None; } let cursor_position = cursor.position_in(&bounds)?; @@ -258,15 +253,15 @@ mod grid { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(button) => match button { mouse::Button::Left => { - self.interaction = Some(Interaction::Drawing); + self.interaction = Interaction::Drawing; populate } mouse::Button::Right => { - self.interaction = Some(Interaction::Panning { + self.interaction = Interaction::Panning { translation: self.translation, start: cursor_position, - }); + }; None } @@ -274,11 +269,8 @@ mod grid { }, mouse::Event::CursorMoved { .. } => { match self.interaction { - Some(Interaction::Drawing) => populate, - Some(Interaction::Panning { - translation, - start, - }) => { + Interaction::Drawing => populate, + Interaction::Panning { translation, start } => { self.translation = translation + (cursor_position - start); @@ -368,11 +360,9 @@ mod grid { cursor: Cursor, ) -> mouse::Interaction { match self.interaction { - Some(Interaction::Drawing) => mouse::Interaction::Crosshair, - Some(Interaction::Panning { .. }) => { - mouse::Interaction::Grabbing - } - None if cursor.is_over(&bounds) => { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { mouse::Interaction::Crosshair } _ => mouse::Interaction::default(), @@ -425,4 +415,16 @@ mod grid { rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) } } + + enum Interaction { + None, + Drawing, + Panning { translation: Vector, start: Point }, + } + + impl Default for Interaction { + fn default() -> Interaction { + Interaction::None + } + } } -- cgit From a6db1e1fb3e512f86be076e70eff92abb11fd457 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 01:08:39 +0200 Subject: Introduce `Life` type in `game_of_life` --- examples/game_of_life/src/main.rs | 88 +++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 35 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8a841c91..b539247b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -163,7 +163,7 @@ mod grid { #[derive(Default)] pub struct Grid { - life: HashSet, + life: Life, interaction: Interaction, cache: canvas::Cache, translation: Vector, @@ -176,35 +176,14 @@ mod grid { impl Grid { pub fn tick(&mut self) { - use itertools::Itertools; - - let populated_neighbors: HashMap = self - .life - .iter() - .flat_map(Cell::cluster) - .unique() - .map(|cell| (cell, self.count_adjacent_life(cell))) - .collect(); - - for (cell, amount) in populated_neighbors.iter() { - match amount { - 2 => {} - 3 => { - let _ = self.life.insert(*cell); - } - _ => { - let _ = self.life.remove(cell); - } - } - } - + self.life.tick(); self.cache.clear() } pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { - self.life.insert(cell); + self.life.populate(cell); self.cache.clear() } } @@ -216,17 +195,6 @@ mod grid { .height(Length::Fill) .into() } - - fn count_adjacent_life(&self, cell: Cell) -> usize { - let cluster = Cell::cluster(&cell); - - let is_neighbor = |candidate| candidate != cell; - let is_populated = |cell| self.life.contains(&cell); - - cluster - .filter(|&cell| is_neighbor(cell) && is_populated(cell)) - .count() - } } impl<'a> canvas::Program for Grid { @@ -370,6 +338,56 @@ mod grid { } } + #[derive(Default)] + pub struct Life { + cells: HashSet, + } + + impl Life { + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn tick(&mut self) { + use itertools::Itertools; + + let populated_neighbors: HashMap = self + .cells + .iter() + .flat_map(Cell::cluster) + .unique() + .map(|cell| (cell, self.count_adjacent(cell))) + .collect(); + + for (cell, amount) in populated_neighbors.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + fn count_adjacent(&self, cell: Cell) -> usize { + let cluster = Cell::cluster(&cell); + + let is_neighbor = |candidate| candidate != cell; + let is_populated = |cell| self.cells.contains(&cell); + + cluster + .filter(|&cell| is_neighbor(cell) && is_populated(cell)) + .count() + } + } + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Cell { i: isize, -- cgit From 377ead93d6d506e7fe1e49d4b8b54c0f1d4c5e14 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 01:24:31 +0200 Subject: Improve tick performance in `game_of_life` --- examples/game_of_life/src/main.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b539247b..3b37dc34 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -353,17 +353,19 @@ mod grid { } fn tick(&mut self) { - use itertools::Itertools; + let mut adjacent_life = HashMap::new(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); - let populated_neighbors: HashMap = self - .cells - .iter() - .flat_map(Cell::cluster) - .unique() - .map(|cell| (cell, self.count_adjacent(cell))) - .collect(); + *amount += 1; + } + } - for (cell, amount) in populated_neighbors.iter() { + for (cell, amount) in adjacent_life.iter() { match amount { 2 => {} 3 => { @@ -375,17 +377,6 @@ mod grid { } } } - - fn count_adjacent(&self, cell: Cell) -> usize { - let cluster = Cell::cluster(&cell); - - let is_neighbor = |candidate| candidate != cell; - let is_populated = |cell| self.cells.contains(&cell); - - cluster - .filter(|&cell| is_neighbor(cell) && is_populated(cell)) - .count() - } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -407,7 +398,7 @@ mod grid { } } - fn cluster(cell: &Cell) -> impl Iterator { + fn cluster(cell: Cell) -> impl Iterator { use itertools::Itertools; let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); @@ -416,6 +407,10 @@ mod grid { rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) } + fn neighbors(cell: Cell) -> impl Iterator { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + fn all_visible_in(region: Rectangle) -> impl Iterator { use itertools::Itertools; -- cgit From 404122e0b17300aa46cdb5ec5f0366f24b8ea931 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:35:59 +0200 Subject: Implement zooming for `game_of_life` example --- examples/game_of_life/src/main.rs | 157 +++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 60 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3b37dc34..d9ff5a14 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -156,17 +156,17 @@ impl Application for GameOfLife { mod grid { use iced::{ - canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, + canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use std::collections::{HashMap, HashSet}; - #[derive(Default)] pub struct Grid { life: Life, interaction: Interaction, - cache: canvas::Cache, + cache: Cache, translation: Vector, + scaling: f32, } #[derive(Debug, Clone, Copy)] @@ -174,6 +174,18 @@ mod grid { Populate(Cell), } + impl Default for Grid { + fn default() -> Self { + Self { + life: Life::default(), + interaction: Interaction::default(), + cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + } + } + } + impl Grid { pub fn tick(&mut self) { self.life.tick(); @@ -195,6 +207,27 @@ mod grid { .height(Length::Fill) .into() } + + pub fn visible_region(&self, size: Size) -> Rectangle { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Rectangle { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + pub fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } } impl<'a> canvas::Program for Grid { @@ -209,7 +242,7 @@ mod grid { } let cursor_position = cursor.position_in(&bounds)?; - let cell = Cell::at(cursor_position - self.translation); + let cell = Cell::at(self.project(cursor_position, bounds.size())); let populate = if self.life.contains(&cell) { None @@ -239,8 +272,9 @@ mod grid { match self.interaction { Interaction::Drawing => populate, Interaction::Panning { translation, start } => { - self.translation = - translation + (cursor_position - start); + self.translation = translation + + (cursor_position - start) + * (1.0 / self.scaling); self.cache.clear(); @@ -249,62 +283,65 @@ mod grid { _ => None, } } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y > 0.0 && self.scaling < 2.0 + || y < 0.0 && self.scaling > 0.25 + { + self.scaling += y / 30.0; + + self.cache.clear(); + } + + None + } + }, _ => None, }, } } fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { - let cell_size = Size::new(1.0, 1.0); + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); let life = self.cache.draw(bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill( - &background, - Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ), - ); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); frame.translate(self.translation); frame.scale(Cell::SIZE as f32); - let cells = Path::new(|p| { - let region = Rectangle { - x: -self.translation.x, - y: -self.translation.y, - width: frame.width(), - height: frame.height(), - }; - - for cell in Cell::all_visible_in(region) { - if self.life.contains(&cell) { - p.rectangle( - Point::new(cell.j as f32, cell.i as f32), - cell_size, - ); - } - } - }); - frame.fill(&cells, Color::WHITE); + let region = self.visible_region(frame.size()); + + for cell in self.life.visible_in(region) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } }); }); let hovered_cell = { let mut frame = Frame::new(bounds.size()); + frame.translate(center); + frame.scale(self.scaling); frame.translate(self.translation); frame.scale(Cell::SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - let cell = Cell::at(cursor_position - self.translation); + let cell = + Cell::at(self.project(cursor_position, frame.size())); let interaction = Path::rectangle( Point::new(cell.j as f32, cell.i as f32), - cell_size, + Size::UNIT, ); frame.fill( @@ -344,14 +381,6 @@ mod grid { } impl Life { - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - fn tick(&mut self) { let mut adjacent_life = HashMap::new(); @@ -377,6 +406,31 @@ mod grid { } } } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn visible_in(&self, region: Rectangle) -> impl Iterator { + 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; + + self.cells.iter().filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -410,23 +464,6 @@ mod grid { fn neighbors(cell: Cell) -> impl Iterator { Cell::cluster(cell).filter(move |candidate| *candidate != cell) } - - fn all_visible_in(region: Rectangle) -> impl Iterator { - use itertools::Itertools; - - 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; - - rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) - } } enum Interaction { -- cgit From f9227546ca975bf3bf7f293d10e79004935b8645 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:41:04 +0200 Subject: Use `fill_rectangle` for cursor in `game_of_life` --- examples/game_of_life/src/main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index d9ff5a14..ef040263 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -339,13 +339,9 @@ mod grid { let cell = Cell::at(self.project(cursor_position, frame.size())); - let interaction = Path::rectangle( + frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, - ); - - frame.fill( - &interaction, Color { a: 0.5, ..Color::BLACK -- cgit From c23995ecb4ddc0c9cc33b0d50404a478b8b5e659 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:48:26 +0200 Subject: Increase speed limit to `200` in `game_of_life` --- examples/game_of_life/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ef040263..8a140ed4 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -124,7 +124,7 @@ impl Application for GameOfLife { .push( Slider::new( &mut self.speed_slider, - 1.0..=20.0, + 1.0..=200.0, selected_speed as f32, Message::SpeedChanged, ) -- cgit From 0a5f1bb676f89a26711a8885935ffe94a370c261 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 05:19:05 +0200 Subject: Improve zooming logic in `game_of_life` --- examples/game_of_life/src/main.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8a140ed4..92500309 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -178,7 +178,7 @@ mod grid { fn default() -> Self { Self { life: Life::default(), - interaction: Interaction::default(), + interaction: Interaction::None, cache: Cache::default(), translation: Vector::default(), scaling: 1.0, @@ -187,6 +187,9 @@ mod grid { } impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + pub fn tick(&mut self) { self.life.tick(); self.cache.clear() @@ -286,10 +289,12 @@ mod grid { mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - if y > 0.0 && self.scaling < 2.0 - || y < 0.0 && self.scaling > 0.25 + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING { - self.scaling += y / 30.0; + self.scaling = (self.scaling + y / 30.0) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); self.cache.clear(); } @@ -467,10 +472,4 @@ mod grid { Drawing, Panning { translation: Vector, start: Point }, } - - impl Default for Interaction { - fn default() -> Interaction { - Interaction::None - } - } } -- cgit From ffbe59f8129c80afb3e86eae67efaa5370fbfa8e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 05:42:07 +0200 Subject: Zoom to cursor in `game_of_life` example --- examples/game_of_life/src/main.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 92500309..c818d99f 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -292,7 +292,21 @@ mod grid { if y < 0.0 && self.scaling > Self::MIN_SCALING || y > 0.0 && self.scaling < Self::MAX_SCALING { - self.scaling = (self.scaling + y / 30.0) + let factor = y / 30.0; + + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + self.translation = self.translation + - Vector::new( + cursor_to_center.x * factor + / (self.scaling * self.scaling), + cursor_to_center.y * factor + / (self.scaling * self.scaling), + ); + } + + self.scaling = (self.scaling + factor) .max(Self::MIN_SCALING) .min(Self::MAX_SCALING); -- cgit From 1833c77312ede2e5d47b62df0eea771f6fa559e9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 06:23:05 +0200 Subject: Improve scrolling smoothness in `game_of_life` --- examples/game_of_life/src/main.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index c818d99f..44ab4da6 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -292,24 +292,27 @@ mod grid { if y < 0.0 && self.scaling > Self::MIN_SCALING || y > 0.0 && self.scaling < Self::MAX_SCALING { - let factor = y / 30.0; + let old_scaling = self.scaling; + + self.scaling = (self.scaling + * (1.0 + y / 30.0)) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); if let Some(cursor_to_center) = cursor.position_from(bounds.center()) { + let factor = self.scaling - old_scaling; + self.translation = self.translation - Vector::new( cursor_to_center.x * factor - / (self.scaling * self.scaling), + / (old_scaling * old_scaling), cursor_to_center.y * factor - / (self.scaling * self.scaling), + / (old_scaling * old_scaling), ); } - self.scaling = (self.scaling + factor) - .max(Self::MIN_SCALING) - .min(Self::MAX_SCALING); - self.cache.clear(); } -- cgit From e7e8e76c28e5bc8eac0c98d6d72c7e49d65468fc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 06:23:30 +0200 Subject: Change speed limit to `100` in `game_of_life` --- examples/game_of_life/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 44ab4da6..17b4090d 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -124,7 +124,7 @@ impl Application for GameOfLife { .push( Slider::new( &mut self.speed_slider, - 1.0..=200.0, + 1.0..=100.0, selected_speed as f32, Message::SpeedChanged, ) -- cgit From 4fd8e47737e82817d652d86b306400da663f7a98 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 03:31:31 +0200 Subject: Use `rustc_hash` for hashing in `game_of_life` This seems to produce a 2x speedup. --- examples/game_of_life/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 17b4090d..fb4b5b75 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -159,7 +159,7 @@ mod grid { canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; - use std::collections::{HashMap, HashSet}; + use rustc_hash::{FxHashMap, FxHashSet}; pub struct Grid { life: Life, @@ -395,12 +395,12 @@ mod grid { #[derive(Default)] pub struct Life { - cells: HashSet, + cells: FxHashSet, } impl Life { fn tick(&mut self) { - let mut adjacent_life = HashMap::new(); + let mut adjacent_life = FxHashMap::default(); for cell in &self.cells { let _ = adjacent_life.entry(*cell).or_insert(0); -- cgit From 8fa9e4c94eb9d6b6e13b45fd6a99209536880a2d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 03:37:20 +0200 Subject: Rename `visible_in` to `within` in `game_of_life` --- examples/game_of_life/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index fb4b5b75..88fd3a93 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -339,7 +339,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in self.life.visible_in(region) { + for cell in self.life.within(region) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -433,7 +433,7 @@ mod grid { self.cells.insert(cell); } - fn visible_in(&self, region: Rectangle) -> impl Iterator { + fn within(&self, region: Rectangle) -> impl Iterator { let first_row = (region.y / Cell::SIZE as f32).floor() as isize; let first_column = (region.x / Cell::SIZE as f32).floor() as isize; -- cgit From 916a1bfc7049867669b81f446e711021d92a4132 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 07:01:27 +0200 Subject: Run ticks in a background thread in `game_of_life` --- examples/game_of_life/src/main.rs | 260 ++++++++++++++++++++++++++++++-------- 1 file changed, 209 insertions(+), 51 deletions(-) (limited to 'examples/game_of_life/src/main.rs') 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, 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 { - 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, + 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> { + 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, + is_ticking: bool, + } + + impl State { + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator { + 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>> { + 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, } @@ -433,21 +567,16 @@ mod grid { self.cells.insert(cell); } - fn within(&self, region: Rectangle) -> impl Iterator { - 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 { + 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, + ) -> impl Iterator { + 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, -- cgit From 0025b8c3f8f029d6fb7b8b5a599cc6450248aad6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 09:27:49 +0200 Subject: Display some statistics in `game_of_life` --- examples/game_of_life/src/main.rs | 104 +++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 23 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b8cabf24..b77d06ea 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -26,6 +26,8 @@ struct GameOfLife { next_button: button::State, clear_button: button::State, speed_slider: slider::State, + tick_duration: Duration, + tick_amount: usize, } enum State { @@ -71,7 +73,12 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - self.grid.update(message); + if let Some((tick_duration, tick_amount)) = + self.grid.update(message) + { + self.tick_duration = tick_duration; + self.tick_amount = tick_amount; + } } Message::Tick(_) | Message::Next => match &mut self.state { State::Paused => { @@ -86,7 +93,7 @@ impl Application for GameOfLife { let needed_ticks = (self.speed as f32 * seconds_elapsed).ceil() as usize; - if let Some(task) = self.grid.tick(needed_ticks) { + if let Some(task) = self.grid.tick(needed_ticks.max(1)) { *last_tick = Instant::now(); if let Some(speed) = self.next_speed.take() { @@ -154,33 +161,49 @@ impl Application for GameOfLife { let selected_speed = self.next_speed.unwrap_or(self.speed); let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) .spacing(10) .push( Slider::new( &mut self.speed_slider, - 1.0..=100.0, + 1.0..=1000.0, selected_speed as f32, Message::SpeedChanged, ) - .width(Length::Units(200)) .style(style::Slider), ) - .push(Text::new(format!("x{}", selected_speed)).size(16)) - .align_items(Align::Center); + .push(Text::new(format!("x{}", selected_speed)).size(16)); + + let stats = Column::new() + .width(Length::Units(150)) + .align_items(Align::Center) + .spacing(2) + .push( + Text::new(format!("{} cells", self.grid.cell_count())).size(14), + ) + .push( + Text::new(format!( + "{:?} ({})", + self.tick_duration, self.tick_amount + )) + .size(14), + ); let controls = Row::new() .padding(10) .spacing(20) + .align_items(Align::Center) .push(playback_controls) .push(speed_controls) + .push(stats) .push( Button::new(&mut self.clear_button, Text::new("Clear")) .on_press(Message::Clear) - .style(style::Button), + .style(style::Clear), ); let content = Column::new() - .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -199,6 +222,7 @@ mod grid { }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; + use std::time::{Duration, Instant}; pub struct Grid { state: State, @@ -214,6 +238,8 @@ mod grid { Populate(Cell), Ticked { result: Result, + tick_duration: Duration, + tick_amount: usize, version: usize, }, } @@ -240,16 +266,29 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; + pub fn cell_count(&self) -> usize { + self.state.cell_count() + } + pub fn tick( &mut self, amount: usize, ) -> Option> { - use iced::futures::FutureExt; - let version = self.version; let tick = self.state.tick(amount)?; - Some(tick.map(move |result| Message::Ticked { result, version })) + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + version, + tick_duration, + tick_amount: amount, + } + }) } pub fn clear(&mut self) { @@ -259,25 +298,36 @@ mod grid { self.cache.clear(); } - pub fn update(&mut self, message: Message) { + pub fn update( + &mut self, + message: Message, + ) -> Option<(Duration, usize)> { match message { Message::Populate(cell) => { self.state.populate(cell); - self.cache.clear() + self.cache.clear(); + + None } Message::Ticked { result: Ok(life), version, + tick_duration, + tick_amount, } if version == self.version => { self.state.update(life); - self.cache.clear() + self.cache.clear(); + + Some((tick_duration, tick_amount)) } Message::Ticked { result: Err(error), .. } => { dbg!(error); + + None } - Message::Ticked { .. } => {} + Message::Ticked { .. } => None, } } @@ -478,6 +528,10 @@ mod grid { } impl State { + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + fn contains(&self, cell: &Cell) -> bool { self.life.contains(cell) || self.births.contains(cell) } @@ -533,6 +587,18 @@ mod grid { } impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + fn tick(&mut self) { let mut adjacent_life = FxHashMap::default(); @@ -559,14 +625,6 @@ mod grid { } } - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - pub fn iter(&self) -> impl Iterator { self.cells.iter() } -- cgit From cc8f5b6fc82e253466f7fab3a9285b0b7531f189 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 10:48:42 +0200 Subject: Simplify logic and limit ticks in `game_of_life` --- examples/game_of_life/src/main.rs | 106 ++++++++++++-------------------------- 1 file changed, 33 insertions(+), 73 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b77d06ea..1b9bad44 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,26 +19,16 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, - state: State, - speed: u64, - next_speed: Option, + is_playing: bool, + speed: usize, + next_speed: Option, toggle_button: button::State, next_button: button::State, clear_button: button::State, speed_slider: slider::State, tick_duration: Duration, - tick_amount: usize, -} - -enum State { - Paused, - Playing { last_tick: Instant }, -} - -impl Default for State { - fn default() -> State { - State::Paused - } + queued_ticks: usize, + last_ticks: usize, } #[derive(Debug, Clone)] @@ -73,68 +63,48 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - if let Some((tick_duration, tick_amount)) = - self.grid.update(message) - { + if let Some(tick_duration) = self.grid.update(message) { self.tick_duration = tick_duration; - self.tick_amount = tick_amount; } } - 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.max(1)) { - *last_tick = Instant::now(); - - if let Some(speed) = self.next_speed.take() { - self.speed = speed; - } + Message::Tick(_) | Message::Next => { + if let Some(task) = self.grid.tick(self.queued_ticks + 1) { + self.last_ticks = self.queued_ticks; + self.queued_ticks = 0; - return Command::perform(task, Message::Grid); + if let Some(speed) = self.next_speed.take() { + self.speed = speed; } + + return Command::perform(task, Message::Grid); + } else { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); } - }, + } Message::Toggle => { - self.state = match self.state { - State::Paused => State::Playing { - last_tick: Instant::now(), - }, - State::Playing { .. } => State::Paused, - }; + self.is_playing = !self.is_playing; } Message::Clear => { self.grid.clear(); } - Message::SpeedChanged(speed) => match self.state { - State::Paused => { - self.speed = speed.round() as u64; + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; } - State::Playing { .. } => { - self.next_speed = Some(speed.round() as u64); - } - }, + } } Command::none() } fn subscription(&self) -> Subscription { - match self.state { - State::Paused => Subscription::none(), - State::Playing { .. } => { - time::every(Duration::from_millis(1000 / self.speed)) - .map(Message::Tick) - } + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() } } @@ -144,11 +114,7 @@ impl Application for GameOfLife { .push( Button::new( &mut self.toggle_button, - Text::new(if let State::Paused = self.state { - "Play" - } else { - "Pause" - }), + Text::new(if self.is_playing { "Pause" } else { "Play" }), ) .on_press(Message::Toggle) .style(style::Button), @@ -185,7 +151,7 @@ impl Application for GameOfLife { .push( Text::new(format!( "{:?} ({})", - self.tick_duration, self.tick_amount + self.tick_duration, self.last_ticks )) .size(14), ); @@ -239,7 +205,6 @@ mod grid { Ticked { result: Result, tick_duration: Duration, - tick_amount: usize, version: usize, }, } @@ -286,7 +251,6 @@ mod grid { result, version, tick_duration, - tick_amount: amount, } }) } @@ -298,10 +262,7 @@ mod grid { self.cache.clear(); } - pub fn update( - &mut self, - message: Message, - ) -> Option<(Duration, usize)> { + pub fn update(&mut self, message: Message) -> Option { match message { Message::Populate(cell) => { self.state.populate(cell); @@ -313,12 +274,11 @@ mod grid { result: Ok(life), version, tick_duration, - tick_amount, } if version == self.version => { self.state.update(life); self.cache.clear(); - Some((tick_duration, tick_amount)) + Some(tick_duration) } Message::Ticked { result: Err(error), .. -- cgit From a43fb42428cbcef3d80e0ec21ec92c6db506353d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 00:08:41 +0200 Subject: Reorganize `view` code in `game_of_life` --- examples/game_of_life/src/main.rs | 173 ++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 71 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 1b9bad44..0e66c237 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,16 +19,12 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, + controls: Controls, + statistics: Statistics, is_playing: bool, + queued_ticks: usize, speed: usize, next_speed: Option, - toggle_button: button::State, - next_button: button::State, - clear_button: button::State, - speed_slider: slider::State, - tick_duration: Duration, - queued_ticks: usize, - last_ticks: usize, } #[derive(Debug, Clone)] @@ -64,21 +60,21 @@ impl Application for GameOfLife { match message { Message::Grid(message) => { if let Some(tick_duration) = self.grid.update(message) { - self.tick_duration = tick_duration; + self.statistics.tick_duration = tick_duration; } } Message::Tick(_) | Message::Next => { - if let Some(task) = self.grid.tick(self.queued_ticks + 1) { - self.last_ticks = self.queued_ticks; - self.queued_ticks = 0; + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + if let Some(task) = self.grid.tick(self.queued_ticks) { if let Some(speed) = self.next_speed.take() { self.speed = speed; } + self.statistics.last_queued_ticks = self.queued_ticks; + self.queued_ticks = 0; + return Command::perform(task, Message::Grid); - } else { - self.queued_ticks = (self.queued_ticks + 1).min(self.speed); } } Message::Toggle => { @@ -109,65 +105,13 @@ impl Application for GameOfLife { } fn view(&mut self) -> Element { - let playback_controls = Row::new() - .spacing(10) - .push( - Button::new( - &mut self.toggle_button, - Text::new(if self.is_playing { "Pause" } else { "Play" }), - ) - .on_press(Message::Toggle) - .style(style::Button), - ) - .push( - Button::new(&mut self.next_button, Text::new("Next")) - .on_press(Message::Next) - .style(style::Button), - ); - let selected_speed = self.next_speed.unwrap_or(self.speed); - let speed_controls = Row::new() - .width(Length::Fill) - .align_items(Align::Center) - .spacing(10) - .push( - Slider::new( - &mut self.speed_slider, - 1.0..=1000.0, - selected_speed as f32, - Message::SpeedChanged, - ) - .style(style::Slider), - ) - .push(Text::new(format!("x{}", selected_speed)).size(16)); - - let stats = Column::new() - .width(Length::Units(150)) - .align_items(Align::Center) - .spacing(2) - .push( - Text::new(format!("{} cells", self.grid.cell_count())).size(14), - ) - .push( - Text::new(format!( - "{:?} ({})", - self.tick_duration, self.last_ticks - )) - .size(14), - ); - - let controls = Row::new() - .padding(10) - .spacing(20) - .align_items(Align::Center) - .push(playback_controls) - .push(speed_controls) - .push(stats) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Clear), - ); + let controls = self.controls.view( + &self.grid, + &self.statistics, + self.is_playing, + selected_speed, + ); let content = Column::new() .push(self.grid.view().map(Message::Grid)) @@ -666,3 +610,90 @@ mod grid { Panning { translation: Vector, start: Point }, } } + +#[derive(Default)] +struct Controls { + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +impl Controls { + fn view<'a>( + &'a mut self, + grid: &Grid, + statistics: &'a Statistics, + is_playing: bool, + speed: usize, + ) -> Element<'a, Message> { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::Toggle) + .style(style::Button), + ) + .push( + Button::new(&mut self.next_button, Text::new("Next")) + .on_press(Message::Next) + .style(style::Button), + ); + + let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=1000.0, + speed as f32, + Message::SpeedChanged, + ) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", speed)).size(16)); + + Row::new() + .padding(10) + .spacing(20) + .align_items(Align::Center) + .push(playback_controls) + .push(speed_controls) + .push(statistics.view(grid)) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Clear), + ) + .into() + } +} + +#[derive(Default)] +struct Statistics { + tick_duration: Duration, + last_queued_ticks: usize, +} + +impl Statistics { + fn view(&self, grid: &Grid) -> Element { + Column::new() + .width(Length::Units(150)) + .align_items(Align::Center) + .spacing(2) + .push(Text::new(format!("{} cells", grid.cell_count())).size(14)) + .push( + Text::new(format!( + "{:?} ({})", + self.tick_duration, self.last_queued_ticks + )) + .size(14), + ) + .into() + } +} -- cgit From c3c5161386cb527bf6d0fe34e5f4103392733599 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 00:57:15 +0200 Subject: Draw grid in `game_of_life` --- examples/game_of_life/src/main.rs | 91 +++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 18 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 0e66c237..52b04696 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -13,7 +13,10 @@ use iced::{ use std::time::{Duration, Instant}; pub fn main() { - GameOfLife::run(Settings::default()) + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) } #[derive(Default)] @@ -132,12 +135,14 @@ mod grid { }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; + use std::ops::RangeInclusive; use std::time::{Duration, Instant}; pub struct Grid { state: State, interaction: Interaction, - cache: Cache, + life_cache: Cache, + grid_cache: Cache, translation: Vector, scaling: f32, version: usize, @@ -163,7 +168,8 @@ mod grid { Self { state: State::default(), interaction: Interaction::None, - cache: Cache::default(), + life_cache: Cache::default(), + grid_cache: Cache::default(), translation: Vector::default(), scaling: 1.0, version: 0, @@ -203,14 +209,14 @@ mod grid { self.state = State::default(); self.version += 1; - self.cache.clear(); + self.life_cache.clear(); } pub fn update(&mut self, message: Message) -> Option { match message { Message::Populate(cell) => { self.state.populate(cell); - self.cache.clear(); + self.life_cache.clear(); None } @@ -220,7 +226,7 @@ mod grid { tick_duration, } if version == self.version => { self.state.update(life); - self.cache.clear(); + self.life_cache.clear(); Some(tick_duration) } @@ -310,7 +316,8 @@ mod grid { + (cursor_position - start) * (1.0 / self.scaling); - self.cache.clear(); + self.life_cache.clear(); + self.grid_cache.clear(); None } @@ -344,7 +351,8 @@ mod grid { ); } - self.cache.clear(); + self.life_cache.clear(); + self.grid_cache.clear(); } None @@ -358,7 +366,7 @@ mod grid { fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - let life = self.cache.draw(bounds.size(), |frame| { + let life = self.life_cache.draw(bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); @@ -370,7 +378,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in region.view(self.state.cells()) { + for cell in region.cull(self.state.cells()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -405,7 +413,44 @@ mod grid { frame.into_geometry() }; - vec![life, hovered_cell] + if self.scaling < 0.2 { + vec![life, hovered_cell] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, hovered_cell] + } } fn mouse_interaction( @@ -583,20 +628,30 @@ mod grid { } impl Region { - fn view<'a>( - &self, - cells: impl Iterator, - ) -> impl Iterator { + fn rows(&self) -> RangeInclusive { 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; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive { + let first_column = (self.x / Cell::SIZE as f32).floor() 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; + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator, + ) -> impl Iterator { + let rows = self.rows(); + let columns = self.columns(); cells.filter(move |cell| { rows.contains(&cell.i) && columns.contains(&cell.j) -- cgit From 5aaaea7c8824fb65bac35307cdf760c57f2bf5df Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 01:53:45 +0200 Subject: Render stats as an overlay in `game_of_life` Also allow toggling the grid lines --- examples/game_of_life/src/main.rs | 185 +++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 82 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 52b04696..018ebc50 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -7,8 +7,8 @@ use iced::{ button::{self, Button}, executor, slider::{self, Slider}, - time, Align, Application, Column, Command, Container, Element, Length, Row, - Settings, Subscription, Text, + time, Align, Application, Checkbox, Column, Command, Container, Element, + Length, Row, Settings, Subscription, Text, }; use std::time::{Duration, Instant}; @@ -23,7 +23,6 @@ pub fn main() { struct GameOfLife { grid: Grid, controls: Controls, - statistics: Statistics, is_playing: bool, queued_ticks: usize, speed: usize, @@ -34,7 +33,8 @@ struct GameOfLife { enum Message { Grid(grid::Message), Tick(Instant), - Toggle, + TogglePlayback, + ToggleGrid(bool), Next, Clear, SpeedChanged(f32), @@ -62,9 +62,7 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - if let Some(tick_duration) = self.grid.update(message) { - self.statistics.tick_duration = tick_duration; - } + self.grid.update(message); } Message::Tick(_) | Message::Next => { self.queued_ticks = (self.queued_ticks + 1).min(self.speed); @@ -74,15 +72,17 @@ impl Application for GameOfLife { self.speed = speed; } - self.statistics.last_queued_ticks = self.queued_ticks; self.queued_ticks = 0; return Command::perform(task, Message::Grid); } } - Message::Toggle => { + Message::TogglePlayback => { self.is_playing = !self.is_playing; } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } Message::Clear => { self.grid.clear(); } @@ -110,9 +110,8 @@ impl Application for GameOfLife { fn view(&mut self) -> Element { let selected_speed = self.next_speed.unwrap_or(self.speed); let controls = self.controls.view( - &self.grid, - &self.statistics, self.is_playing, + self.grid.are_lines_visible(), selected_speed, ); @@ -130,8 +129,11 @@ impl Application for GameOfLife { mod grid { use iced::{ - canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, Color, Element, Length, Point, Rectangle, Size, Vector, + canvas::{ + self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, + }, + mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, + Size, Vector, VerticalAlignment, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -145,6 +147,9 @@ mod grid { grid_cache: Cache, translation: Vector, scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, version: usize, } @@ -172,6 +177,9 @@ mod grid { grid_cache: Cache::default(), translation: Vector::default(), scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, version: 0, } } @@ -181,10 +189,6 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; - pub fn cell_count(&self) -> usize { - self.state.cell_count() - } - pub fn tick( &mut self, amount: usize, @@ -192,6 +196,8 @@ mod grid { let version = self.version; let tick = self.state.tick(amount)?; + self.last_queued_ticks = amount; + Some(async move { let start = Instant::now(); let result = tick.await; @@ -205,20 +211,11 @@ mod grid { }) } - pub fn clear(&mut self) { - self.state = State::default(); - self.version += 1; - - self.life_cache.clear(); - } - - pub fn update(&mut self, message: Message) -> Option { + pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); - - None } Message::Ticked { result: Ok(life), @@ -228,16 +225,14 @@ mod grid { self.state.update(life); self.life_cache.clear(); - Some(tick_duration) + self.last_tick_duration = tick_duration; } Message::Ticked { result: Err(error), .. } => { dbg!(error); - - None } - Message::Ticked { .. } => None, + Message::Ticked { .. } => {} } } @@ -248,7 +243,22 @@ mod grid { .into() } - pub fn visible_region(&self, size: Size) -> Region { + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.life_cache.clear(); + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { let width = size.width / self.scaling; let height = size.height / self.scaling; @@ -260,7 +270,7 @@ mod grid { } } - pub fn project(&self, position: Point, size: Size) -> Point { + fn project(&self, position: Point, size: Size) -> Point { let region = self.visible_region(size); Point::new( @@ -388,33 +398,64 @@ mod grid { }); }); - let hovered_cell = { + let overlay = { let mut frame = Frame::new(bounds.size()); - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - if let Some(cursor_position) = cursor.position_in(&bounds) { - let cell = - Cell::at(self.project(cursor_position, frame.size())); - - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color { - a: 0.5, - ..Color::BLACK - }, - ); + let hovered_cell = + cursor.position_in(&bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); + + if let Some(cell) = hovered_cell { + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.i, cell.j), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); } + frame.fill_text(Text { + content: format!( + "{} cells @ {:?} ({})", + self.state.cell_count(), + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + frame.into_geometry() }; - if self.scaling < 0.2 { - vec![life, hovered_cell] + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] } else { let grid = self.grid_cache.draw(bounds.size(), |frame| { frame.translate(center); @@ -449,7 +490,7 @@ mod grid { } }); - vec![life, grid, hovered_cell] + vec![life, grid, overlay] } } @@ -677,9 +718,8 @@ struct Controls { impl Controls { fn view<'a>( &'a mut self, - grid: &Grid, - statistics: &'a Statistics, is_playing: bool, + is_grid_enabled: bool, speed: usize, ) -> Element<'a, Message> { let playback_controls = Row::new() @@ -689,7 +729,7 @@ impl Controls { &mut self.toggle_button, Text::new(if is_playing { "Pause" } else { "Play" }), ) - .on_press(Message::Toggle) + .on_press(Message::TogglePlayback) .style(style::Button), ) .push( @@ -719,7 +759,12 @@ impl Controls { .align_items(Align::Center) .push(playback_controls) .push(speed_controls) - .push(statistics.view(grid)) + .push( + Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + ) .push( Button::new(&mut self.clear_button, Text::new("Clear")) .on_press(Message::Clear) @@ -728,27 +773,3 @@ impl Controls { .into() } } - -#[derive(Default)] -struct Statistics { - tick_duration: Duration, - last_queued_ticks: usize, -} - -impl Statistics { - fn view(&self, grid: &Grid) -> Element { - Column::new() - .width(Length::Units(150)) - .align_items(Align::Center) - .spacing(2) - .push(Text::new(format!("{} cells", grid.cell_count())).size(14)) - .push( - Text::new(format!( - "{:?} ({})", - self.tick_duration, self.last_queued_ticks - )) - .size(14), - ) - .into() - } -} -- cgit From 4417a34edb7d002276f0419a5f62c6eee4a3af87 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 02:15:11 +0200 Subject: Fix "1 cells" overlay in `game_of_life` --- examples/game_of_life/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 018ebc50..c2f80dfc 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -435,16 +435,19 @@ mod grid { if let Some(cell) = hovered_cell { frame.fill_text(Text { - content: format!("({}, {})", cell.i, cell.j), + content: format!("({}, {})", cell.j, cell.i), position: text.position - Vector::new(0.0, 16.0), ..text }); } + let cell_count = self.state.cell_count(); + frame.fill_text(Text { content: format!( - "{} cells @ {:?} ({})", - self.state.cell_count(), + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, self.last_tick_duration, self.last_queued_ticks ), -- cgit From 917199197f9719bbb3f6f98c63985cb64dfd147c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 02:43:20 +0200 Subject: Allow erasing cells in `game_of_life` --- examples/game_of_life/src/main.rs | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) (limited to 'examples/game_of_life/src/main.rs') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index c2f80dfc..080d55c0 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -156,6 +156,7 @@ mod grid { #[derive(Debug, Clone)] pub enum Message { Populate(Cell), + Unpopulate(Cell), Ticked { result: Result, tick_duration: Duration, @@ -217,6 +218,10 @@ mod grid { self.state.populate(cell); self.life_cache.clear(); } + Message::Unpopulate(cell) => { + self.state.unpopulate(&cell); + self.life_cache.clear(); + } Message::Ticked { result: Ok(life), version, @@ -293,20 +298,25 @@ mod grid { let cursor_position = cursor.position_in(&bounds)?; let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); - let populate = if self.state.contains(&cell) { - None + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) } else { - Some(Message::Populate(cell)) + (Some(Message::Populate(cell)), None) }; match event { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(button) => match button { mouse::Button::Left => { - self.interaction = Interaction::Drawing; + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; - populate + populate.or(unpopulate) } mouse::Button::Right => { self.interaction = Interaction::Panning { @@ -321,6 +331,7 @@ mod grid { mouse::Event::CursorMoved { .. } => { match self.interaction { Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { self.translation = translation + (cursor_position - start) @@ -504,6 +515,7 @@ mod grid { ) -> mouse::Interaction { match self.interaction { Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, Interaction::Panning { .. } => mouse::Interaction::Grabbing, Interaction::None if cursor.is_over(&bounds) => { mouse::Interaction::Crosshair @@ -541,6 +553,14 @@ mod grid { } } + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + fn update(&mut self, mut life: Life) { self.births.drain().for_each(|cell| life.populate(cell)); @@ -592,6 +612,10 @@ mod grid { self.cells.insert(cell); } + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + fn tick(&mut self) { let mut adjacent_life = FxHashMap::default(); @@ -706,6 +730,7 @@ mod grid { enum Interaction { None, Drawing, + Erasing, Panning { translation: Vector, start: Point }, } } -- cgit