summaryrefslogtreecommitdiffstats
path: root/examples/game_of_life
diff options
context:
space:
mode:
Diffstat (limited to 'examples/game_of_life')
-rw-r--r--examples/game_of_life/Cargo.toml2
-rw-r--r--examples/game_of_life/README.md4
-rw-r--r--examples/game_of_life/src/main.rs192
-rw-r--r--examples/game_of_life/src/preset.rs142
-rw-r--r--examples/game_of_life/src/style.rs68
5 files changed, 343 insertions, 65 deletions
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml
index b9bb7f2a..9c4172c4 100644
--- a/examples/game_of_life/Cargo.toml
+++ b/examples/game_of_life/Cargo.toml
@@ -7,6 +7,6 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
-tokio = { version = "0.2", features = ["blocking"] }
+tokio = { version = "0.3", features = ["sync"] }
itertools = "0.9"
rustc-hash = "1.1"
diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md
index 1aeb1455..aa39201c 100644
--- a/examples/game_of_life/README.md
+++ b/examples/game_of_life/README.md
@@ -7,8 +7,8 @@ It runs a simulation in a background thread while allowing interaction with a `C
The __[`main`]__ file contains the relevant code of the example.
<div align="center">
- <a href="https://gfycat.com/briefaccurateaardvark">
- <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif">
+ <a href="https://gfycat.com/WhichPaltryChick">
+ <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif">
</a>
</div>
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 080d55c0..e18bd6e0 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -1,18 +1,22 @@
//! This example showcases an interactive version of the Game of Life, invented
//! by John Conway. It leverages a `Canvas` together with other widgets.
+mod preset;
mod style;
use grid::Grid;
+use iced::button::{self, Button};
+use iced::executor;
+use iced::pick_list::{self, PickList};
+use iced::slider::{self, Slider};
+use iced::time;
use iced::{
- button::{self, Button},
- executor,
- slider::{self, Slider},
- time, Align, Application, Checkbox, Column, Command, Container, Element,
- Length, Row, Settings, Subscription, Text,
+ Align, Application, Checkbox, Column, Command, Container, Element, Length,
+ Row, Settings, Subscription, Text,
};
+use preset::Preset;
use std::time::{Duration, Instant};
-pub fn main() {
+pub fn main() -> iced::Result {
GameOfLife::run(Settings {
antialiasing: true,
..Settings::default()
@@ -27,17 +31,19 @@ struct GameOfLife {
queued_ticks: usize,
speed: usize,
next_speed: Option<usize>,
+ version: usize,
}
#[derive(Debug, Clone)]
enum Message {
- Grid(grid::Message),
+ Grid(grid::Message, usize),
Tick(Instant),
TogglePlayback,
ToggleGrid(bool),
Next,
Clear,
SpeedChanged(f32),
+ PresetPicked(Preset),
}
impl Application for GameOfLife {
@@ -48,7 +54,7 @@ impl Application for GameOfLife {
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Self {
- speed: 1,
+ speed: 5,
..Self::default()
},
Command::none(),
@@ -61,8 +67,10 @@ impl Application for GameOfLife {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
- Message::Grid(message) => {
- self.grid.update(message);
+ Message::Grid(message, version) => {
+ if version == self.version {
+ self.grid.update(message);
+ }
}
Message::Tick(_) | Message::Next => {
self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
@@ -74,7 +82,11 @@ impl Application for GameOfLife {
self.queued_ticks = 0;
- return Command::perform(task, Message::Grid);
+ let version = self.version;
+
+ return Command::perform(task, move |message| {
+ Message::Grid(message, version)
+ });
}
}
Message::TogglePlayback => {
@@ -85,6 +97,7 @@ impl Application for GameOfLife {
}
Message::Clear => {
self.grid.clear();
+ self.version += 1;
}
Message::SpeedChanged(speed) => {
if self.is_playing {
@@ -93,6 +106,10 @@ impl Application for GameOfLife {
self.speed = speed.round() as usize;
}
}
+ Message::PresetPicked(new_preset) => {
+ self.grid = Grid::from_preset(new_preset);
+ self.version += 1;
+ }
}
Command::none()
@@ -108,15 +125,21 @@ impl Application for GameOfLife {
}
fn view(&mut self) -> Element<Message> {
+ let version = self.version;
let selected_speed = self.next_speed.unwrap_or(self.speed);
let controls = self.controls.view(
self.is_playing,
self.grid.are_lines_visible(),
selected_speed,
+ self.grid.preset(),
);
let content = Column::new()
- .push(self.grid.view().map(Message::Grid))
+ .push(
+ self.grid
+ .view()
+ .map(move |message| Message::Grid(message, version)),
+ )
.push(controls);
Container::new(content)
@@ -128,10 +151,10 @@ impl Application for GameOfLife {
}
mod grid {
+ use crate::Preset;
use iced::{
- canvas::{
- self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text,
- },
+ canvas::event::{self, Event},
+ canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text},
mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle,
Size, Vector, VerticalAlignment,
};
@@ -142,6 +165,7 @@ mod grid {
pub struct Grid {
state: State,
+ preset: Preset,
interaction: Interaction,
life_cache: Cache,
grid_cache: Cache,
@@ -150,7 +174,6 @@ mod grid {
show_lines: bool,
last_tick_duration: Duration,
last_queued_ticks: usize,
- version: usize,
}
#[derive(Debug, Clone)]
@@ -160,7 +183,6 @@ mod grid {
Ticked {
result: Result<Life, TickError>,
tick_duration: Duration,
- version: usize,
},
}
@@ -171,8 +193,24 @@ mod grid {
impl Default for Grid {
fn default() -> Self {
+ Self::from_preset(Preset::default())
+ }
+ }
+
+ impl Grid {
+ const MIN_SCALING: f32 = 0.1;
+ const MAX_SCALING: f32 = 2.0;
+
+ pub fn from_preset(preset: Preset) -> Self {
Self {
- state: State::default(),
+ state: State::with_life(
+ preset
+ .life()
+ .into_iter()
+ .map(|(i, j)| Cell { i, j })
+ .collect(),
+ ),
+ preset,
interaction: Interaction::None,
life_cache: Cache::default(),
grid_cache: Cache::default(),
@@ -181,20 +219,13 @@ mod grid {
show_lines: true,
last_tick_duration: Duration::default(),
last_queued_ticks: 0,
- version: 0,
}
}
- }
-
- impl Grid {
- const MIN_SCALING: f32 = 0.1;
- const MAX_SCALING: f32 = 2.0;
pub fn tick(
&mut self,
amount: usize,
) -> Option<impl Future<Output = Message>> {
- let version = self.version;
let tick = self.state.tick(amount)?;
self.last_queued_ticks = amount;
@@ -206,7 +237,6 @@ mod grid {
Message::Ticked {
result,
- version,
tick_duration,
}
})
@@ -217,16 +247,19 @@ mod grid {
Message::Populate(cell) => {
self.state.populate(cell);
self.life_cache.clear();
+
+ self.preset = Preset::Custom;
}
Message::Unpopulate(cell) => {
self.state.unpopulate(&cell);
self.life_cache.clear();
+
+ self.preset = Preset::Custom;
}
Message::Ticked {
result: Ok(life),
- version,
tick_duration,
- } if version == self.version => {
+ } => {
self.state.update(life);
self.life_cache.clear();
@@ -237,7 +270,6 @@ mod grid {
} => {
dbg!(error);
}
- Message::Ticked { .. } => {}
}
}
@@ -250,11 +282,15 @@ mod grid {
pub fn clear(&mut self) {
self.state = State::default();
- self.version += 1;
+ self.preset = Preset::Custom;
self.life_cache.clear();
}
+ pub fn preset(&self) -> Preset {
+ self.preset
+ }
+
pub fn toggle_lines(&mut self, enabled: bool) {
self.show_lines = enabled;
}
@@ -291,12 +327,18 @@ mod grid {
event: Event,
bounds: Rectangle,
cursor: Cursor,
- ) -> Option<Message> {
+ ) -> (event::Status, Option<Message>) {
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
self.interaction = Interaction::None;
}
- let cursor_position = cursor.position_in(&bounds)?;
+ let cursor_position =
+ if let Some(position) = cursor.position_in(&bounds) {
+ position
+ } else {
+ return (event::Status::Ignored, None);
+ };
+
let cell = Cell::at(self.project(cursor_position, bounds.size()));
let is_populated = self.state.contains(&cell);
@@ -308,28 +350,32 @@ mod grid {
match event {
Event::Mouse(mouse_event) => match mouse_event {
- mouse::Event::ButtonPressed(button) => match button {
- mouse::Button::Left => {
- self.interaction = if is_populated {
- Interaction::Erasing
- } else {
- Interaction::Drawing
- };
-
- populate.or(unpopulate)
- }
- mouse::Button::Right => {
- self.interaction = Interaction::Panning {
- translation: self.translation,
- start: cursor_position,
- };
+ mouse::Event::ButtonPressed(button) => {
+ let message = match button {
+ mouse::Button::Left => {
+ self.interaction = if is_populated {
+ Interaction::Erasing
+ } else {
+ Interaction::Drawing
+ };
+
+ populate.or(unpopulate)
+ }
+ mouse::Button::Right => {
+ self.interaction = Interaction::Panning {
+ translation: self.translation,
+ start: cursor_position,
+ };
- None
- }
- _ => None,
- },
+ None
+ }
+ _ => None,
+ };
+
+ (event::Status::Captured, message)
+ }
mouse::Event::CursorMoved { .. } => {
- match self.interaction {
+ let message = match self.interaction {
Interaction::Drawing => populate,
Interaction::Erasing => unpopulate,
Interaction::Panning { translation, start } => {
@@ -343,7 +389,14 @@ mod grid {
None
}
_ => None,
- }
+ };
+
+ let event_status = match self.interaction {
+ Interaction::None => event::Status::Ignored,
+ _ => event::Status::Captured,
+ };
+
+ (event_status, message)
}
mouse::Event::WheelScrolled { delta } => match delta {
mouse::ScrollDelta::Lines { y, .. }
@@ -376,11 +429,12 @@ mod grid {
self.grid_cache.clear();
}
- None
+ (event::Status::Captured, None)
}
},
- _ => None,
+ _ => (event::Status::Ignored, None),
},
+ _ => (event::Status::Ignored, None),
}
}
@@ -533,6 +587,13 @@ mod grid {
}
impl State {
+ pub fn with_life(life: Life) -> Self {
+ Self {
+ life,
+ ..Self::default()
+ }
+ }
+
fn cell_count(&self) -> usize {
self.life.len() + self.births.len()
}
@@ -647,6 +708,14 @@ mod grid {
}
}
+ impl std::iter::FromIterator<Cell> for Life {
+ fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self {
+ Life {
+ cells: iter.into_iter().collect(),
+ }
+ }
+ }
+
impl std::fmt::Debug for Life {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Life")
@@ -741,6 +810,7 @@ struct Controls {
next_button: button::State,
clear_button: button::State,
speed_slider: slider::State,
+ preset_list: pick_list::State<Preset>,
}
impl Controls {
@@ -749,6 +819,7 @@ impl Controls {
is_playing: bool,
is_grid_enabled: bool,
speed: usize,
+ preset: Preset,
) -> Element<'a, Message> {
let playback_controls = Row::new()
.spacing(10)
@@ -794,6 +865,17 @@ impl Controls {
.text_size(16),
)
.push(
+ PickList::new(
+ &mut self.preset_list,
+ preset::ALL,
+ Some(preset),
+ Message::PresetPicked,
+ )
+ .padding(8)
+ .text_size(16)
+ .style(style::PickList),
+ )
+ .push(
Button::new(&mut self.clear_button, Text::new("Clear"))
.on_press(Message::Clear)
.style(style::Clear),
diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs
new file mode 100644
index 00000000..05157b6a
--- /dev/null
+++ b/examples/game_of_life/src/preset.rs
@@ -0,0 +1,142 @@
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Preset {
+ Custom,
+ XKCD,
+ Glider,
+ SmallExploder,
+ Exploder,
+ TenCellRow,
+ LightweightSpaceship,
+ Tumbler,
+ GliderGun,
+ Acorn,
+}
+
+pub static ALL: &[Preset] = &[
+ Preset::Custom,
+ Preset::XKCD,
+ Preset::Glider,
+ Preset::SmallExploder,
+ Preset::Exploder,
+ Preset::TenCellRow,
+ Preset::LightweightSpaceship,
+ Preset::Tumbler,
+ Preset::GliderGun,
+ Preset::Acorn,
+];
+
+impl Preset {
+ pub fn life(self) -> Vec<(isize, isize)> {
+ #[rustfmt::skip]
+ let cells = match self {
+ Preset::Custom => vec![],
+ Preset::XKCD => vec![
+ " xxx ",
+ " x x ",
+ " x x ",
+ " x ",
+ "x xxx ",
+ " x x x ",
+ " x x",
+ " x x ",
+ " x x ",
+ ],
+ Preset::Glider => vec![
+ " x ",
+ " x",
+ "xxx"
+ ],
+ Preset::SmallExploder => vec![
+ " x ",
+ "xxx",
+ "x x",
+ " x ",
+ ],
+ Preset::Exploder => vec![
+ "x x x",
+ "x x",
+ "x x",
+ "x x",
+ "x x x",
+ ],
+ Preset::TenCellRow => vec![
+ "xxxxxxxxxx",
+ ],
+ Preset::LightweightSpaceship => vec![
+ " xxxxx",
+ "x x",
+ " x",
+ "x x ",
+ ],
+ Preset::Tumbler => vec![
+ " xx xx ",
+ " xx xx ",
+ " x x ",
+ "x x x x",
+ "x x x x",
+ "xx xx",
+ ],
+ Preset::GliderGun => vec![
+ " x ",
+ " x x ",
+ " xx xx xx",
+ " x x xx xx",
+ "xx x x xx ",
+ "xx x x xx x x ",
+ " x x x ",
+ " x x ",
+ " xx ",
+ ],
+ Preset::Acorn => vec![
+ " x ",
+ " x ",
+ "xx xxx",
+ ],
+ };
+
+ let start_row = -(cells.len() as isize / 2);
+
+ cells
+ .into_iter()
+ .enumerate()
+ .flat_map(|(i, cells)| {
+ let start_column = -(cells.len() as isize / 2);
+
+ cells
+ .chars()
+ .enumerate()
+ .filter(|(_, c)| !c.is_whitespace())
+ .map(move |(j, _)| {
+ (start_row + i as isize, start_column + j as isize)
+ })
+ })
+ .collect()
+ }
+}
+
+impl Default for Preset {
+ fn default() -> Preset {
+ Preset::XKCD
+ }
+}
+
+impl std::fmt::Display for Preset {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Preset::Custom => "Custom",
+ Preset::XKCD => "xkcd #2293",
+ Preset::Glider => "Glider",
+ Preset::SmallExploder => "Small Exploder",
+ Preset::Exploder => "Exploder",
+ Preset::TenCellRow => "10 Cell Row",
+ Preset::LightweightSpaceship => "Lightweight spaceship",
+ Preset::Tumbler => "Tumbler",
+ Preset::GliderGun => "Gosper Glider Gun",
+ Preset::Acorn => "Acorn",
+ }
+ )
+ }
+}
diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs
index d59569f2..6605826f 100644
--- a/examples/game_of_life/src/style.rs
+++ b/examples/game_of_life/src/style.rs
@@ -1,4 +1,4 @@
-use iced::{button, container, slider, Background, Color};
+use iced::{button, container, pick_list, slider, Background, Color};
const ACTIVE: Color = Color::from_rgb(
0x72 as f32 / 255.0,
@@ -18,6 +18,12 @@ const HOVERED: Color = Color::from_rgb(
0xC4 as f32 / 255.0,
);
+const BACKGROUND: Color = Color::from_rgb(
+ 0x2F as f32 / 255.0,
+ 0x31 as f32 / 255.0,
+ 0x36 as f32 / 255.0,
+);
+
pub struct Container;
impl container::StyleSheet for Container {
@@ -38,7 +44,7 @@ impl button::StyleSheet for Button {
fn active(&self) -> button::Style {
button::Style {
background: Some(Background::Color(ACTIVE)),
- border_radius: 3,
+ border_radius: 3.0,
text_color: Color::WHITE,
..button::Style::default()
}
@@ -54,7 +60,7 @@ impl button::StyleSheet for Button {
fn pressed(&self) -> button::Style {
button::Style {
- border_width: 1,
+ border_width: 1.0,
border_color: Color::WHITE,
..self.hovered()
}
@@ -67,7 +73,7 @@ impl button::StyleSheet for Clear {
fn active(&self) -> button::Style {
button::Style {
background: Some(Background::Color(DESTRUCTIVE)),
- border_radius: 3,
+ border_radius: 3.0,
text_color: Color::WHITE,
..button::Style::default()
}
@@ -86,7 +92,7 @@ impl button::StyleSheet for Clear {
fn pressed(&self) -> button::Style {
button::Style {
- border_width: 1,
+ border_width: 1.0,
border_color: Color::WHITE,
..self.hovered()
}
@@ -100,9 +106,9 @@ impl slider::StyleSheet for Slider {
slider::Style {
rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
handle: slider::Handle {
- shape: slider::HandleShape::Circle { radius: 9 },
+ shape: slider::HandleShape::Circle { radius: 9.0 },
color: ACTIVE,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
}
@@ -132,3 +138,51 @@ impl slider::StyleSheet for Slider {
}
}
}
+
+pub struct PickList;
+
+impl pick_list::StyleSheet for PickList {
+ fn menu(&self) -> pick_list::Menu {
+ pick_list::Menu {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.7,
+ ..Color::BLACK
+ },
+ selected_background: Color {
+ a: 0.5,
+ ..Color::BLACK
+ }
+ .into(),
+ selected_text_color: Color::WHITE,
+ }
+ }
+
+ fn active(&self) -> pick_list::Style {
+ pick_list::Style {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.6,
+ ..Color::BLACK
+ },
+ border_radius: 2.0,
+ icon_size: 0.5,
+ }
+ }
+
+ fn hovered(&self) -> pick_list::Style {
+ let active = self.active();
+
+ pick_list::Style {
+ border_color: Color {
+ a: 0.9,
+ ..Color::BLACK
+ },
+ ..active
+ }
+ }
+}