diff options
Diffstat (limited to 'examples/pure/game_of_life')
-rw-r--r-- | examples/pure/game_of_life/Cargo.toml | 13 | ||||
-rw-r--r-- | examples/pure/game_of_life/README.md | 22 | ||||
-rw-r--r-- | examples/pure/game_of_life/src/main.rs | 903 | ||||
-rw-r--r-- | examples/pure/game_of_life/src/preset.rs | 142 |
4 files changed, 0 insertions, 1080 deletions
diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml deleted file mode 100644 index 22e38f00..00000000 --- a/examples/pure/game_of_life/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pure_game_of_life" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure", "canvas", "tokio", "debug"] } -tokio = { version = "1.0", features = ["sync"] } -itertools = "0.9" -rustc-hash = "1.1" -env_logger = "0.9" diff --git a/examples/pure/game_of_life/README.md b/examples/pure/game_of_life/README.md deleted file mode 100644 index aa39201c..00000000 --- a/examples/pure/game_of_life/README.md +++ /dev/null @@ -1,22 +0,0 @@ -## Game of Life - -An interactive version of the [Game of Life], invented by [John Horton Conway]. - -It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. - -The __[`main`]__ file contains the relevant code of the example. - -<div align="center"> - <a href="https://gfycat.com/WhichPaltryChick"> - <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif"> - </a> -</div> - -You can run it with `cargo run`: -``` -cargo run --package game_of_life -``` - -[`main`]: src/main.rs -[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life -[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/pure/game_of_life/src/main.rs b/examples/pure/game_of_life/src/main.rs deleted file mode 100644 index cf6560d6..00000000 --- a/examples/pure/game_of_life/src/main.rs +++ /dev/null @@ -1,903 +0,0 @@ -//! 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; - -use grid::Grid; -use preset::Preset; - -use iced::executor; -use iced::pure::{ - button, checkbox, column, container, pick_list, row, slider, text, -}; -use iced::pure::{Application, Element}; -use iced::theme::{self, Theme}; -use iced::time; -use iced::window; -use iced::{Alignment, Command, Length, Settings, Subscription}; -use std::time::{Duration, Instant}; - -pub fn main() -> iced::Result { - env_logger::builder().format_timestamp(None).init(); - - GameOfLife::run(Settings { - antialiasing: true, - window: window::Settings { - position: window::Position::Centered, - ..window::Settings::default() - }, - ..Settings::default() - }) -} - -#[derive(Default)] -struct GameOfLife { - grid: Grid, - is_playing: bool, - queued_ticks: usize, - speed: usize, - next_speed: Option<usize>, - version: usize, -} - -#[derive(Debug, Clone)] -enum Message { - Grid(grid::Message, usize), - Tick(Instant), - TogglePlayback, - ToggleGrid(bool), - Next, - Clear, - SpeedChanged(f32), - PresetPicked(Preset), -} - -impl Application for GameOfLife { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command<Message>) { - ( - Self { - speed: 5, - ..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, version) => { - if version == self.version { - self.grid.update(message); - } - } - Message::Tick(_) | Message::Next => { - self.queued_ticks = (self.queued_ticks + 1).min(self.speed); - - if let Some(task) = self.grid.tick(self.queued_ticks) { - if let Some(speed) = self.next_speed.take() { - self.speed = speed; - } - - self.queued_ticks = 0; - - let version = self.version; - - return Command::perform(task, move |message| { - Message::Grid(message, version) - }); - } - } - Message::TogglePlayback => { - self.is_playing = !self.is_playing; - } - Message::ToggleGrid(show_grid_lines) => { - self.grid.toggle_lines(show_grid_lines); - } - Message::Clear => { - self.grid.clear(); - self.version += 1; - } - Message::SpeedChanged(speed) => { - if self.is_playing { - self.next_speed = Some(speed.round() as usize); - } else { - self.speed = speed.round() as usize; - } - } - Message::PresetPicked(new_preset) => { - self.grid = Grid::from_preset(new_preset); - self.version += 1; - } - } - - Command::none() - } - - fn subscription(&self) -> Subscription<Message> { - if self.is_playing { - time::every(Duration::from_millis(1000 / self.speed as u64)) - .map(Message::Tick) - } else { - Subscription::none() - } - } - - fn view(&self) -> Element<Message> { - let version = self.version; - let selected_speed = self.next_speed.unwrap_or(self.speed); - let controls = view_controls( - self.is_playing, - self.grid.are_lines_visible(), - selected_speed, - self.grid.preset(), - ); - - let content = column() - .push( - self.grid - .view() - .map(move |message| Message::Grid(message, version)), - ) - .push(controls); - - container(content) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - - fn theme(&self) -> Theme { - Theme::Dark - } -} - -fn view_controls<'a>( - is_playing: bool, - is_grid_enabled: bool, - speed: usize, - preset: Preset, -) -> Element<'a, Message> { - let playback_controls = row() - .spacing(10) - .push( - button(if is_playing { "Pause" } else { "Play" }) - .on_press(Message::TogglePlayback), - ) - .push( - button("Next") - .on_press(Message::Next) - .style(theme::Button::Secondary), - ); - - let speed_controls = row() - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(slider(1.0..=1000.0, speed as f32, Message::SpeedChanged)) - .push(text(format!("x{}", speed)).size(16)); - - row() - .padding(10) - .spacing(20) - .align_items(Alignment::Center) - .push(playback_controls) - .push(speed_controls) - .push( - checkbox("Grid", is_grid_enabled, Message::ToggleGrid) - .size(16) - .spacing(5) - .text_size(16), - ) - .push( - pick_list(preset::ALL, Some(preset), Message::PresetPicked) - .padding(8) - .text_size(16), - ) - .push( - button("Clear") - .on_press(Message::Clear) - .style(theme::Button::Destructive), - ) - .into() -} - -mod grid { - use crate::Preset; - use iced::pure::widget::canvas::event::{self, Event}; - use iced::pure::widget::canvas::{ - self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text, - }; - use iced::pure::Element; - use iced::{ - alignment, mouse, Color, Length, Point, Rectangle, Size, Theme, Vector, - }; - use rustc_hash::{FxHashMap, FxHashSet}; - use std::future::Future; - use std::ops::RangeInclusive; - use std::time::{Duration, Instant}; - - pub struct Grid { - state: State, - preset: Preset, - life_cache: Cache, - grid_cache: Cache, - translation: Vector, - scaling: f32, - show_lines: bool, - last_tick_duration: Duration, - last_queued_ticks: usize, - } - - #[derive(Debug, Clone)] - pub enum Message { - Populate(Cell), - Unpopulate(Cell), - Translated(Vector), - Scaled(f32, Option<Vector>), - Ticked { - result: Result<Life, TickError>, - tick_duration: Duration, - }, - } - - #[derive(Debug, Clone)] - pub enum TickError { - JoinFailed, - } - - 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::with_life( - preset - .life() - .into_iter() - .map(|(i, j)| Cell { i, j }) - .collect(), - ), - preset, - life_cache: Cache::default(), - grid_cache: Cache::default(), - translation: Vector::default(), - scaling: 1.0, - show_lines: true, - last_tick_duration: Duration::default(), - last_queued_ticks: 0, - } - } - - pub fn tick( - &mut self, - amount: usize, - ) -> Option<impl Future<Output = Message>> { - let tick = self.state.tick(amount)?; - - self.last_queued_ticks = amount; - - Some(async move { - let start = Instant::now(); - let result = tick.await; - let tick_duration = start.elapsed() / amount as u32; - - Message::Ticked { - result, - tick_duration, - } - }) - } - - pub fn update(&mut self, message: Message) { - match message { - 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::Translated(translation) => { - self.translation = translation; - - self.life_cache.clear(); - self.grid_cache.clear(); - } - Message::Scaled(scaling, translation) => { - self.scaling = scaling; - - if let Some(translation) = translation { - self.translation = translation; - } - - self.life_cache.clear(); - self.grid_cache.clear(); - } - Message::Ticked { - result: Ok(life), - tick_duration, - } => { - self.state.update(life); - self.life_cache.clear(); - - self.last_tick_duration = tick_duration; - } - Message::Ticked { - result: Err(error), .. - } => { - dbg!(error); - } - } - } - - pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - - pub fn clear(&mut self) { - self.state = State::default(); - 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; - } - - pub fn are_lines_visible(&self) -> bool { - self.show_lines - } - - fn visible_region(&self, size: Size) -> Region { - let width = size.width / self.scaling; - let height = size.height / self.scaling; - - Region { - x: -self.translation.x - width / 2.0, - y: -self.translation.y - height / 2.0, - width, - height, - } - } - - fn project(&self, position: Point, size: Size) -> Point { - let region = self.visible_region(size); - - Point::new( - position.x / self.scaling + region.x, - position.y / self.scaling + region.y, - ) - } - } - - impl canvas::Program<Message> for Grid { - type State = Interaction; - - fn update( - &self, - interaction: &mut Interaction, - event: Event, - bounds: Rectangle, - cursor: Cursor, - ) -> (event::Status, Option<Message>) { - if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - *interaction = Interaction::None; - } - - 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); - - let (populate, unpopulate) = if is_populated { - (None, Some(Message::Unpopulate(cell))) - } else { - (Some(Message::Populate(cell)), None) - }; - - match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(button) => { - let message = match button { - mouse::Button::Left => { - *interaction = if is_populated { - Interaction::Erasing - } else { - Interaction::Drawing - }; - - populate.or(unpopulate) - } - mouse::Button::Right => { - *interaction = Interaction::Panning { - translation: self.translation, - start: cursor_position, - }; - - None - } - _ => None, - }; - - (event::Status::Captured, message) - } - mouse::Event::CursorMoved { .. } => { - let message = match *interaction { - Interaction::Drawing => populate, - Interaction::Erasing => unpopulate, - Interaction::Panning { translation, start } => { - Some(Message::Translated( - translation - + (cursor_position - start) - * (1.0 / self.scaling), - )) - } - _ => None, - }; - - let event_status = match interaction { - Interaction::None => event::Status::Ignored, - _ => event::Status::Captured, - }; - - (event_status, message) - } - mouse::Event::WheelScrolled { delta } => match delta { - mouse::ScrollDelta::Lines { y, .. } - | mouse::ScrollDelta::Pixels { y, .. } => { - if y < 0.0 && self.scaling > Self::MIN_SCALING - || y > 0.0 && self.scaling < Self::MAX_SCALING - { - let old_scaling = self.scaling; - - let scaling = (self.scaling * (1.0 + y / 30.0)) - .max(Self::MIN_SCALING) - .min(Self::MAX_SCALING); - - let translation = - if let Some(cursor_to_center) = - cursor.position_from(bounds.center()) - { - let factor = scaling - old_scaling; - - Some( - self.translation - - Vector::new( - cursor_to_center.x * factor - / (old_scaling - * old_scaling), - cursor_to_center.y * factor - / (old_scaling - * old_scaling), - ), - ) - } else { - None - }; - - ( - event::Status::Captured, - Some(Message::Scaled(scaling, translation)), - ) - } else { - (event::Status::Captured, None) - } - } - }, - _ => (event::Status::Ignored, None), - }, - _ => (event::Status::Ignored, None), - } - } - - fn draw( - &self, - _interaction: &Interaction, - _theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec<Geometry> { - let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - - let life = self.life_cache.draw(bounds.size(), |frame| { - let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); - - frame.with_save(|frame| { - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - let region = self.visible_region(frame.size()); - - for cell in region.cull(self.state.cells()) { - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color::WHITE, - ); - } - }); - }); - - let overlay = { - let mut frame = Frame::new(bounds.size()); - - let hovered_cell = - cursor.position_in(&bounds).map(|position| { - Cell::at(self.project(position, frame.size())) - }); - - if let Some(cell) = hovered_cell { - frame.with_save(|frame| { - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color { - a: 0.5, - ..Color::BLACK - }, - ); - }); - } - - let text = Text { - color: Color::WHITE, - size: 14.0, - position: Point::new(frame.width(), frame.height()), - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Bottom, - ..Text::default() - }; - - if let Some(cell) = hovered_cell { - frame.fill_text(Text { - content: format!("({}, {})", cell.j, cell.i), - position: text.position - Vector::new(0.0, 16.0), - ..text - }); - } - - let cell_count = self.state.cell_count(); - - frame.fill_text(Text { - content: format!( - "{} cell{} @ {:?} ({})", - cell_count, - if cell_count == 1 { "" } else { "s" }, - self.last_tick_duration, - self.last_queued_ticks - ), - ..text - }); - - frame.into_geometry() - }; - - if self.scaling < 0.2 || !self.show_lines { - vec![life, overlay] - } else { - let grid = self.grid_cache.draw(bounds.size(), |frame| { - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - let region = self.visible_region(frame.size()); - let rows = region.rows(); - let columns = region.columns(); - let (total_rows, total_columns) = - (rows.clone().count(), columns.clone().count()); - let width = 2.0 / Cell::SIZE as f32; - let color = Color::from_rgb8(70, 74, 83); - - frame.translate(Vector::new(-width / 2.0, -width / 2.0)); - - for row in region.rows() { - frame.fill_rectangle( - Point::new(*columns.start() as f32, row as f32), - Size::new(total_columns as f32, width), - color, - ); - } - - for column in region.columns() { - frame.fill_rectangle( - Point::new(column as f32, *rows.start() as f32), - Size::new(width, total_rows as f32), - color, - ); - } - }); - - vec![life, grid, overlay] - } - } - - fn mouse_interaction( - &self, - interaction: &Interaction, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - match interaction { - Interaction::Drawing => mouse::Interaction::Crosshair, - Interaction::Erasing => mouse::Interaction::Crosshair, - Interaction::Panning { .. } => mouse::Interaction::Grabbing, - Interaction::None if cursor.is_over(&bounds) => { - mouse::Interaction::Crosshair - } - _ => mouse::Interaction::default(), - } - } - } - - #[derive(Default)] - struct State { - life: Life, - births: FxHashSet<Cell>, - is_ticking: bool, - } - - impl State { - pub fn with_life(life: Life) -> Self { - Self { - life, - ..Self::default() - } - } - - fn cell_count(&self) -> usize { - self.life.len() + self.births.len() - } - - fn contains(&self, cell: &Cell) -> bool { - self.life.contains(cell) || self.births.contains(cell) - } - - fn cells(&self) -> impl Iterator<Item = &Cell> { - self.life.iter().chain(self.births.iter()) - } - - fn populate(&mut self, cell: Cell) { - if self.is_ticking { - self.births.insert(cell); - } else { - self.life.populate(cell); - } - } - - fn unpopulate(&mut self, cell: &Cell) { - if self.is_ticking { - let _ = self.births.remove(cell); - } else { - self.life.unpopulate(cell); - } - } - - fn update(&mut self, mut life: Life) { - self.births.drain().for_each(|cell| life.populate(cell)); - - self.life = life; - self.is_ticking = false; - } - - fn tick( - &mut self, - amount: usize, - ) -> Option<impl Future<Output = Result<Life, TickError>>> { - if self.is_ticking { - return None; - } - - self.is_ticking = true; - - let mut life = self.life.clone(); - - Some(async move { - tokio::task::spawn_blocking(move || { - for _ in 0..amount { - life.tick(); - } - - life - }) - .await - .map_err(|_| TickError::JoinFailed) - }) - } - } - - #[derive(Clone, Default)] - pub struct Life { - cells: FxHashSet<Cell>, - } - - impl Life { - fn len(&self) -> usize { - self.cells.len() - } - - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - - fn unpopulate(&mut self, cell: &Cell) { - let _ = self.cells.remove(cell); - } - - fn tick(&mut self) { - let mut adjacent_life = FxHashMap::default(); - - for cell in &self.cells { - let _ = adjacent_life.entry(*cell).or_insert(0); - - for neighbor in Cell::neighbors(*cell) { - let amount = adjacent_life.entry(neighbor).or_insert(0); - - *amount += 1; - } - } - - for (cell, amount) in adjacent_life.iter() { - match amount { - 2 => {} - 3 => { - let _ = self.cells.insert(*cell); - } - _ => { - let _ = self.cells.remove(cell); - } - } - } - } - - pub fn iter(&self) -> impl Iterator<Item = &Cell> { - self.cells.iter() - } - } - - 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") - .field("cells", &self.cells.len()) - .finish() - } - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct Cell { - i: isize, - j: isize, - } - - impl Cell { - const SIZE: usize = 20; - - fn at(position: Point) -> Cell { - let i = (position.y / Cell::SIZE as f32).ceil() as isize; - let j = (position.x / Cell::SIZE as f32).ceil() as isize; - - Cell { - i: i.saturating_sub(1), - j: j.saturating_sub(1), - } - } - - fn cluster(cell: Cell) -> impl Iterator<Item = Cell> { - use itertools::Itertools; - - let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); - let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); - - rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) - } - - fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> { - Cell::cluster(cell).filter(move |candidate| *candidate != cell) - } - } - - pub struct Region { - x: f32, - y: f32, - width: f32, - height: f32, - } - - impl Region { - fn rows(&self) -> RangeInclusive<isize> { - let first_row = (self.y / Cell::SIZE as f32).floor() as isize; - - let visible_rows = - (self.height / Cell::SIZE as f32).ceil() as isize; - - first_row..=first_row + visible_rows - } - - fn columns(&self) -> RangeInclusive<isize> { - let first_column = (self.x / Cell::SIZE as f32).floor() as isize; - - let visible_columns = - (self.width / Cell::SIZE as f32).ceil() as isize; - - first_column..=first_column + visible_columns - } - - fn cull<'a>( - &self, - cells: impl Iterator<Item = &'a Cell>, - ) -> impl Iterator<Item = &'a Cell> { - let rows = self.rows(); - let columns = self.columns(); - - cells.filter(move |cell| { - rows.contains(&cell.i) && columns.contains(&cell.j) - }) - } - } - - pub enum Interaction { - None, - Drawing, - Erasing, - Panning { translation: Vector, start: Point }, - } - - impl Default for Interaction { - fn default() -> Self { - Self::None - } - } -} diff --git a/examples/pure/game_of_life/src/preset.rs b/examples/pure/game_of_life/src/preset.rs deleted file mode 100644 index 964b9120..00000000 --- a/examples/pure/game_of_life/src/preset.rs +++ /dev/null @@ -1,142 +0,0 @@ -#[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", - } - ) - } -} |