diff options
author | 2020-04-29 08:25:42 +0200 | |
---|---|---|
committer | 2020-04-29 08:25:42 +0200 | |
commit | 70f86f998b6db102d5b77f756750414efd53aa9e (patch) | |
tree | f3771264767d2c3f87c098ac41b35b113116ce6b | |
parent | afa0bca4fd1a5499fd24549eb49a44f9837597c6 (diff) | |
download | iced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.gz iced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.bz2 iced-70f86f998b6db102d5b77f756750414efd53aa9e.zip |
Add `game_of_life` example
RIP John Conway
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | examples/game_of_life/Cargo.toml | 12 | ||||
-rw-r--r-- | examples/game_of_life/README.md | 18 | ||||
-rw-r--r-- | examples/game_of_life/src/main.rs | 408 | ||||
-rw-r--r-- | examples/game_of_life/src/style.rs | 96 | ||||
-rw-r--r-- | examples/game_of_life/src/time.rs | 34 |
6 files changed, 569 insertions, 0 deletions
@@ -43,6 +43,7 @@ members = [ "examples/custom_widget", "examples/download_progress", "examples/events", + "examples/game_of_life", "examples/geometry", "examples/integration", "examples/pane_grid", diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..8855b3e8 --- /dev/null +++ b/examples/game_of_life/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["async-std", "canvas", "debug"] } +iced_native = { path = "../../native" } +async-std = { version = "1.0", features = ["unstable"] } +itertools = "0.9" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..ebbb12cc --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,18 @@ +## Bézier tool + +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/soulfulinfiniteantbear"> + <img src="https://thumbs.gfycat.com/SoulfulInfiniteAntbear-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package bezier_tool +``` + +[`main`]: 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<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(), + } + } + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..0becb5be --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,96 @@ +use iced::{button, container, slider, Background, Color}; + +const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, +); + +const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, +); + +pub struct Container; + +impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } +} + +pub struct Button; + +impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Slider; + +impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } +} diff --git a/examples/game_of_life/src/time.rs b/examples/game_of_life/src/time.rs new file mode 100644 index 00000000..7b475ecd --- /dev/null +++ b/examples/game_of_life/src/time.rs @@ -0,0 +1,34 @@ +use iced::futures; + +pub fn every( + duration: std::time::Duration, +) -> iced::Subscription<std::time::Instant> { + iced::Subscription::from_recipe(Every(duration)) +} + +struct Every(std::time::Duration); + +impl<H, I> iced_native::subscription::Recipe<H, I> for Every +where + H: std::hash::Hasher, +{ + type Output = std::time::Instant; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::<Self>().hash(state); + self.0.hash(state); + } + + fn stream( + self: Box<Self>, + _input: futures::stream::BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } +} |