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