summaryrefslogtreecommitdiffstats
path: root/examples/game_of_life/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'examples/game_of_life/src/main.rs')
-rw-r--r--examples/game_of_life/src/main.rs408
1 files changed, 408 insertions, 0 deletions
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<u64>,
+ 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<Message>) {
+ (
+ Self {
+ speed: 1,
+ ..Self::default()
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Game of Life - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ 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<Message> {
+ if self.is_playing {
+ time::every(Duration::from_millis(1000 / self.speed))
+ .map(Message::Tick)
+ } else {
+ Subscription::none()
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ 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<Message> for Grid {
+ fn update(
+ &mut self,
+ event: Event,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> Option<Message> {
+ 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<Geometry> {
+ 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(),
+ }
+ }
+ }
+}