lifers_raylib/
generic.rs

1//! Implementation of the frontend for generic automata.
2
3use crate::timer::{RepeatingTimer, TimerState};
4use lifers::{engine::ExecutionState, frontend::RenderCell, prelude::generic::Automaton};
5use raylib::{
6    color::Color, drawing::RaylibDraw, ffi::KeyboardKey, math::Vector2, RaylibHandle, RaylibThread,
7};
8use std::time::Duration;
9
10// TODO:
11// - Uninfy API with / in favor of `life_like`
12
13/// Maps a function to both coordinates of all given vectors.
14#[macro_export]
15macro_rules! map_vecs {
16    ($( $vec:expr ),+ => $f:expr) => {
17        Vector2::new($f($( $vec.x ),+), $f($( $vec.y ),+))
18    };
19}
20
21/// The main struct that implements the frontend capabilities.
22pub struct RaylibFrontend<S, D> {
23    automaton: Automaton<S, D>,
24    rl: RaylibHandle,
25    thread: RaylibThread,
26    timer: RepeatingTimer,
27    cell_margin: u32,
28    rect_size: f32,
29    center_translation: Vector2,
30}
31
32impl<S, D> RaylibFrontend<S, D> {
33    // NOTE: This function is quite a mess
34    /// Instantiates the frontend.
35    ///
36    /// You may want to use [`FrontendBuilder`] for convenience.
37    #[allow(clippy::as_conversions)]
38    pub fn new(
39        automaton: Automaton<S, D>,
40        update_rate: Duration,
41        cell_margin: u32,
42        window_size: (u32, u32),
43    ) -> Self {
44        let (rl, thread) = raylib::init()
45            .size(window_size.0 as i32, window_size.1 as i32)
46            .title("lifers")
47            .build();
48
49        let cell_margin_f = cell_margin as f32;
50        let window_size = Vector2::new(window_size.0 as f32, window_size.1 as f32);
51
52        let grid_dimensions = {
53            let (x, y) = automaton.grid_size();
54
55            Vector2::new(x as f32, y as f32)
56        };
57        let rect_size = {
58            let Vector2 { x, y } = map_vecs!(
59                window_size,
60                grid_dimensions
61                => |win, cells: f32| (cells + 1.).mul_add(-cell_margin_f, win) / cells
62            );
63            let side = x.min(y);
64
65            Vector2::new(side, side)
66        };
67        let grid_size = map_vecs!(
68            rect_size,
69            grid_dimensions
70            => |size, cells: f32| cells.mul_add(size, (cells + 1.) * cell_margin_f)
71        );
72        let grid_center = grid_size.scale_by(0.5);
73        let window_center = window_size.scale_by(0.5);
74
75        // NOTE: `grid_center` is calculated with respect to the
76        // window dimensions, so it can't be greater than
77        // `window_center`
78        #[allow(clippy::arithmetic_side_effects)]
79        let center_translation = window_center - grid_center;
80
81        Self {
82            automaton,
83            rl,
84            thread,
85            timer: RepeatingTimer::new(update_rate),
86            cell_margin,
87            rect_size: rect_size.x,
88            center_translation,
89        }
90    }
91
92    /// Checks if the window should close (e.g. `esc` pressed).
93    pub fn window_should_close(&self) -> bool {
94        self.automaton.is_finished() || self.rl.window_should_close()
95    }
96
97    /// Updates the inner timer to compute the next generation
98    /// according to the update rate (see
99    /// [`FrontendBuilder::update_rate()`]).
100    pub fn tick(&mut self) -> Option<ExecutionState> {
101        matches!(self.timer.update(), TimerState::Finished).then(|| self.automaton.step())
102    }
103
104    /// Computes the next generation of the automaton immediately.
105    ///
106    /// See [`tick()`](Self::tick()) for properly timed updating.
107    pub fn step(&mut self) -> ExecutionState {
108        self.automaton.step()
109    }
110
111    /// Registers default key actions:
112    /// - Space -> Pause
113    /// - LMB -> Toggle cell under cursor
114    pub fn default_key_actions(&mut self) {
115        match self.rl.get_key_pressed() {
116            None => (),
117            Some(key) => match key {
118                KeyboardKey::KEY_SPACE => self.timer.toggle_pause(), // HACK?
119                // NOTE: Minus reduces the rate (not the time taken), equals
120                // increases the rate.
121                KeyboardKey::KEY_MINUS => {
122                    self.timer = RepeatingTimer::new(self.timer.rate() + Duration::from_millis(10))
123                }
124                KeyboardKey::KEY_EQUAL => {
125                    let duration = self
126                        .timer
127                        .rate()
128                        .checked_sub(Duration::from_millis(10))
129                        .unwrap_or(Duration::from_millis(0));
130
131                    self.timer = RepeatingTimer::new(duration);
132                }
133                _ => (),
134            },
135        }
136    }
137}
138
139impl<S: RenderCell<Color>, D> RaylibFrontend<S, D> {
140    /// Displays the cell grid using Raylib.
141    ///
142    /// Manages the job of clearing the background and drawing all the
143    /// cells with respect to their [`RenderCell`] implementation.
144    pub fn display_grid(&mut self) {
145        let mut drawer = self.rl.begin_drawing(&self.thread);
146
147        drawer.clear_background(Color::GRAY);
148
149        #[allow(clippy::as_conversions)]
150        self.automaton
151            .cells()
152            .iter()
153            .enumerate()
154            .for_each(|(y, xs)| {
155                xs.iter().enumerate().for_each(|(x, cell)| {
156                    let pos = map_vecs!(
157                        Vector2::new(x as f32, y as f32),
158                        self.center_translation
159                        => |pos: f32, center_vec| pos.mul_add(self.rect_size, (pos + 1.) * self.cell_margin as f32) + center_vec
160                    );
161
162                    let rect = Vector2::new(self.rect_size, self.rect_size);
163                    drawer.draw_rectangle_v(pos, rect, cell.render_cell());
164                });
165            });
166    }
167}
168
169/// A helper struct to instantiate a [`RaylibFrontend`].
170pub struct FrontendBuilder {
171    window_size: (u32, u32),
172    cell_margin: u32,
173    update_rate: Duration,
174}
175
176impl FrontendBuilder {
177    /// Creates a new builder with the given window size.
178    #[must_use]
179    pub const fn new(window_size: (u32, u32)) -> Self {
180        Self {
181            window_size,
182            cell_margin: 5,
183            update_rate: Duration::from_millis(100),
184        }
185    }
186
187    /// Sets the cell margin (purely visual).
188    #[must_use]
189    pub const fn cell_margin(self, cell_margin: u32) -> Self {
190        Self {
191            cell_margin,
192            ..self
193        }
194    }
195
196    /// Sets the update rate.
197    ///
198    /// This is the amount of time that passes between each generation
199    /// is computed and displayed.
200    #[must_use]
201    pub const fn update_rate(self, update_rate: Duration) -> Self {
202        Self {
203            update_rate,
204            ..self
205        }
206    }
207
208    /// Convert the builder to an actual [`RaylibFrontend`].
209    pub fn finish<S, D>(self, automaton: Automaton<S, D>) -> RaylibFrontend<S, D> {
210        RaylibFrontend::new(
211            automaton,
212            self.update_rate,
213            self.cell_margin,
214            self.window_size,
215        )
216    }
217}