diff options
-rw-r--r-- | Cargo.toml | 1 | ||||
-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 | 899 | ||||
-rw-r--r-- | examples/pure/game_of_life/src/preset.rs | 142 | ||||
-rw-r--r-- | examples/pure/game_of_life/src/style.rs | 186 | ||||
-rw-r--r-- | src/pure.rs | 6 |
7 files changed, 1266 insertions, 3 deletions
@@ -88,6 +88,7 @@ members = [ "examples/url_handler", "examples/pure/component", "examples/pure/counter", + "examples/pure/game_of_life", "examples/pure/pick_list", "examples/pure/todos", "examples/pure/tour", diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml new file mode 100644 index 00000000..22e38f00 --- /dev/null +++ b/examples/pure/game_of_life/Cargo.toml @@ -0,0 +1,13 @@ +[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 new file mode 100644 index 00000000..aa39201c --- /dev/null +++ b/examples/pure/game_of_life/README.md @@ -0,0 +1,22 @@ +## 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 new file mode 100644 index 00000000..75a0deef --- /dev/null +++ b/examples/pure/game_of_life/src/main.rs @@ -0,0 +1,899 @@ +//! 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::executor; +use iced::pure::widget::{ + button, checkbox, column, container, pick_list, row, slider, text, +}; +use iced::pure::{Application, Element}; +use iced::time; +use iced::window; +use iced::{Alignment, Color, Command, Length, Settings, Subscription}; +use preset::Preset; +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 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 background_color(&self) -> Color { + style::BACKGROUND + } + + 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) + .style(style::Container) + .into() + } +} + +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) + .style(style::Button), + ) + .push(button("Next").on_press(Message::Next).style(style::Button)); + + let speed_controls = row() + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10) + .push( + slider(1.0..=1000.0, speed as f32, Message::SpeedChanged) + .style(style::Slider), + ) + .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) + .style(style::PickList), + ) + .push(button("Clear").on_press(Message::Clear).style(style::Clear)) + .into() +} + +mod grid { + use crate::Preset; + use iced::pure::canvas::event::{self, Event}; + use iced::pure::canvas::{ + self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text, + }; + use iced::pure::Element; + use iced::{ + alignment, mouse, Color, Length, Point, Rectangle, Size, 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<'a>(&'a self) -> Element<'a, 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 for Grid { + type Message = Message; + 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, + 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 new file mode 100644 index 00000000..05157b6a --- /dev/null +++ b/examples/pure/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/pure/game_of_life/src/style.rs b/examples/pure/game_of_life/src/style.rs new file mode 100644 index 00000000..1a64cf4a --- /dev/null +++ b/examples/pure/game_of_life/src/style.rs @@ -0,0 +1,186 @@ +use iced::{button, container, pick_list, 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 DESTRUCTIVE: Color = Color::from_rgb( + 0xC0 as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 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 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 { + fn style(&self) -> container::Style { + container::Style { + 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.0, + 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.0, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Clear; + +impl button::StyleSheet for Clear { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(DESTRUCTIVE)), + border_radius: 3.0, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color { + a: 0.5, + ..DESTRUCTIVE + })), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1.0, + 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.0 }, + color: ACTIVE, + border_width: 0.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 + } + } +} + +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, + ..pick_list::Style::default() + } + } + + fn hovered(&self) -> pick_list::Style { + let active = self.active(); + + pick_list::Style { + border_color: Color { + a: 0.9, + ..Color::BLACK + }, + ..active + } + } +} diff --git a/src/pure.rs b/src/pure.rs index 0fc02528..f3f73bba 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -48,11 +48,11 @@ pub type Image = iced_pure::Image<crate::widget::image::Handle>; mod application; mod sandbox; +pub use application::Application; +pub use sandbox::Sandbox; + #[cfg(feature = "canvas")] pub use iced_graphics::widget::pure::canvas; #[cfg(feature = "canvas")] pub use canvas::Canvas; - -pub use application::Application; -pub use sandbox::Sandbox; |