summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2020-04-29 08:25:42 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2020-04-29 08:25:42 +0200
commit70f86f998b6db102d5b77f756750414efd53aa9e (patch)
treef3771264767d2c3f87c098ac41b35b113116ce6b
parentafa0bca4fd1a5499fd24549eb49a44f9837597c6 (diff)
downloadiced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.gz
iced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.bz2
iced-70f86f998b6db102d5b77f756750414efd53aa9e.zip
Add `game_of_life` example
RIP John Conway
-rw-r--r--Cargo.toml1
-rw-r--r--examples/game_of_life/Cargo.toml12
-rw-r--r--examples/game_of_life/README.md18
-rw-r--r--examples/game_of_life/src/main.rs408
-rw-r--r--examples/game_of_life/src/style.rs96
-rw-r--r--examples/game_of_life/src/time.rs34
6 files changed, 569 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 9c52ea8f..d394d516 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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()
+ }
+}