Add game_of_life example

RIP John Conway
This commit is contained in:
Héctor Ramón Jiménez 2020-04-29 08:25:42 +02:00
parent afa0bca4fd
commit 70f86f998b
6 changed files with 569 additions and 0 deletions

View File

@ -43,6 +43,7 @@ members = [
"examples/custom_widget",
"examples/download_progress",
"examples/events",
"examples/game_of_life",
"examples/geometry",
"examples/integration",
"examples/pane_grid",

View File

@ -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"

View File

@ -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

View File

@ -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(),
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}