]> git.huck.website - cellseq.git/commitdiff
removed: scaling and gridlines
authorHuck Boles <huck@huck.website>
Fri, 16 Jun 2023 22:11:00 +0000 (17:11 -0500)
committerHuck Boles <huck@huck.website>
Fri, 16 Jun 2023 22:11:00 +0000 (17:11 -0500)
src/grid.rs [new file with mode: 0644]
src/main.rs

diff --git a/src/grid.rs b/src/grid.rs
new file mode 100644 (file)
index 0000000..52b48cc
--- /dev/null
@@ -0,0 +1,537 @@
+use iced::alignment;
+use iced::mouse;
+use iced::touch;
+use iced::widget::canvas;
+use iced::widget::canvas::event::{self, Event};
+use iced::widget::canvas::{Cache, Canvas, Cursor, Frame, Geometry, Path, Text};
+use iced::{Color, Element, 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 Map {
+    state: State,
+    life_cache: Cache,
+    translation: Vector,
+    show_lines: bool,
+    last_tick_duration: Duration,
+    last_queued_ticks: usize,
+}
+
+#[derive(Debug, Clone)]
+pub enum Message {
+    Populate(Cell),
+    Unpopulate(Cell),
+    Ticked {
+        result: Result<Life, TickError>,
+        tick_duration: Duration,
+    },
+}
+
+#[derive(Debug, Clone)]
+pub enum TickError {
+    JoinFailed,
+}
+
+impl Default for Map {
+    fn default() -> Self {
+        Self {
+            state: State::with_life(Life::default()),
+            life_cache: Cache::default(),
+            translation: Vector::default(),
+            show_lines: true,
+            last_tick_duration: Duration::default(),
+            last_queued_ticks: 0,
+        }
+    }
+}
+
+impl Map {
+    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();
+            }
+            Message::Unpopulate(cell) => {
+                self.state.unpopulate(&cell);
+                self.life_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.life_cache.clear();
+    }
+
+    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;
+        let height = size.height;
+
+        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 / 1.0 + region.x, position.y / 1.0 + region.y)
+    }
+}
+
+impl canvas::Program<Message> for Map {
+    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::Touch(touch::Event::FingerMoved { .. }) => {
+                let message = {
+                    *interaction = if is_populated {
+                        Interaction::Erasing
+                    } else {
+                        Interaction::Drawing
+                    };
+
+                    populate.or(unpopulate)
+                };
+
+                (event::Status::Captured, message)
+            }
+            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)
+                        }
+                        _ => None,
+                    };
+
+                    (event::Status::Captured, message)
+                }
+                mouse::Event::CursorMoved { .. } => {
+                    let message = match *interaction {
+                        Interaction::Drawing => populate,
+                        Interaction::Erasing => unpopulate,
+                        _ => None,
+                    };
+
+                    let event_status = match interaction {
+                        Interaction::None => event::Status::Ignored,
+                        _ => event::Status::Captured,
+                    };
+
+                    (event_status, message)
+                }
+                _ => (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(1.0);
+                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(1.0);
+                    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()
+        };
+
+        vec![life, 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::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 = 16;
+
+    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,
+}
+
+impl Default for Interaction {
+    fn default() -> Self {
+        Self::None
+    }
+}
index ade73190920b61391c294567c68cf14b5d555fb4..1e28cb034b66cac9e97c61372c0f003b464ed5da 100644 (file)
@@ -1,20 +1,19 @@
 //! 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 grid;
 
-use grid::Grid;
-use preset::Preset;
+use grid::Map;
 
 use iced::executor;
 use iced::theme::{self, Theme};
 use iced::time;
-use iced::widget::{button, checkbox, column, container, pick_list, row, slider, text};
+use iced::widget::{button, checkbox, column, container, row, slider, text};
 use iced::window;
 use iced::{Alignment, Application, Command, Element, Length, Settings, Subscription};
 use std::time::{Duration, Instant};
 
 pub fn main() -> iced::Result {
-    GameOfLife::run(Settings {
+    CellSeq::run(Settings {
         antialiasing: true,
         window: window::Settings {
             position: window::Position::Centered,
@@ -25,8 +24,8 @@ pub fn main() -> iced::Result {
 }
 
 #[derive(Default)]
-struct GameOfLife {
-    grid: Grid,
+struct CellSeq {
+    grid: Map,
     is_playing: bool,
     queued_ticks: usize,
     speed: usize,
@@ -43,10 +42,9 @@ enum Message {
     Next,
     Clear,
     SpeedChanged(f32),
-    PresetPicked(Preset),
 }
 
-impl Application for GameOfLife {
+impl Application for CellSeq {
     type Message = Message;
     type Theme = Theme;
     type Executor = executor::Default;
@@ -105,10 +103,6 @@ impl Application for GameOfLife {
                     self.speed = speed.round() as usize;
                 }
             }
-            Message::PresetPicked(new_preset) => {
-                self.grid = Grid::from_preset(new_preset);
-                self.version += 1;
-            }
         }
 
         Command::none()
@@ -129,7 +123,6 @@ impl Application for GameOfLife {
             self.is_playing,
             self.grid.are_lines_visible(),
             selected_speed,
-            self.grid.preset(),
         );
 
         let content = column![
@@ -154,7 +147,6 @@ fn view_controls<'a>(
     is_playing: bool,
     is_grid_enabled: bool,
     speed: usize,
-    preset: Preset,
 ) -> Element<'a, Message> {
     let playback_controls = row![
         button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback),
@@ -179,9 +171,6 @@ fn view_controls<'a>(
             .size(16)
             .spacing(5)
             .text_size(16),
-        pick_list(preset::ALL, Some(preset), Message::PresetPicked)
-            .padding(8)
-            .text_size(16),
         button("Clear")
             .on_press(Message::Clear)
             .style(theme::Button::Destructive),
@@ -191,687 +180,3 @@ fn view_controls<'a>(
     .align_items(Alignment::Center)
     .into()
 }
-
-mod grid {
-    use crate::Preset;
-    use iced::alignment;
-    use iced::mouse;
-    use iced::touch;
-    use iced::widget::canvas;
-    use iced::widget::canvas::event::{self, Event};
-    use iced::widget::canvas::{Cache, Canvas, Cursor, Frame, Geometry, Path, Text};
-    use iced::{Color, Element, 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::Touch(touch::Event::FingerMoved { .. }) => {
-                    let message = {
-                        *interaction = if is_populated {
-                            Interaction::Erasing
-                        } else {
-                            Interaction::Drawing
-                        };
-
-                        populate.or(unpopulate)
-                    };
-
-                    (event::Status::Captured, message)
-                }
-                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))
-                                    .clamp(Self::MIN_SCALING, 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
-        }
-    }
-}