diff options
| author | 2020-04-29 08:25:42 +0200 | |
|---|---|---|
| committer | 2020-04-29 08:25:42 +0200 | |
| commit | 70f86f998b6db102d5b77f756750414efd53aa9e (patch) | |
| tree | f3771264767d2c3f87c098ac41b35b113116ce6b | |
| parent | afa0bca4fd1a5499fd24549eb49a44f9837597c6 (diff) | |
| download | iced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.gz iced-70f86f998b6db102d5b77f756750414efd53aa9e.tar.bz2 iced-70f86f998b6db102d5b77f756750414efd53aa9e.zip  | |
Add `game_of_life` example
RIP John Conway
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/game_of_life/Cargo.toml | 12 | ||||
| -rw-r--r-- | examples/game_of_life/README.md | 18 | ||||
| -rw-r--r-- | examples/game_of_life/src/main.rs | 408 | ||||
| -rw-r--r-- | examples/game_of_life/src/style.rs | 96 | ||||
| -rw-r--r-- | examples/game_of_life/src/time.rs | 34 | 
6 files changed, 569 insertions, 0 deletions
@@ -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() +    } +}  | 
