Skip to content

Commit

Permalink
Merge pull request #9 from kimonp/general_refactor
Browse files Browse the repository at this point in the history
Improve README
  • Loading branch information
kimonp authored Dec 28, 2023
2 parents 674a138 + 55295cd commit 847f480
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 76 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# Conway's Game Of Life implemented with the Dioxus framework
An implementation of [Conway's game of life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) using the Dioxus framework,
adapted from the [rust wasm tutorial](https://rustwasm.github.io/docs/book/game-of-life/introduction.html).
An example implementation of [Conway's game of life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)
using the Dioxus framework, adapted from the
[rust wasm tutorial](https://rustwasm.github.io/docs/book/game-of-life/introduction.html).

While the code from the original tutorial is about 50% Rust, 50% JavaScript, with the Dioxus framework the code is 100% Rust.
The code from the original tutorial is about 50% Rust, 50% JavaScript.
With the Dioxus framework the code is 100% Rust.

<img src="game_of_life.png" alt="Game of Life" class="center" width="480" height="616">

## Demonstrates
* A Dioxus web app written completely in Rust.
* Frame animation using `request_animation_frame()` and abstracting to a Dioxus `use_state` hook.
* Limited by the frame rate of the monitor.
* A FramesPerSecond component that displays the current frames per second.
* Building a component from a 2d HTML canvas.
* Using using Dioxus' onmount event to get an element (similar to react's use_ref)

## Install and run
* Install the rust development environment: https://www.rust-lang.org/tools/install
Expand All @@ -18,6 +30,6 @@ Defines a `GameOfLifeGrid` component that renders the game of life (with several
and a `FramesPerSecond` component which shows how many frames per seconds are being rendered (which
depends on the monitor frame rate).

This is stitched together using a `use_animation_frame()` hook returns a `frame_id` that can be used
This is stitched together using a `use_animation_frame()` hook that returns a `frame_id` which can be used
to trigger `use_effect` calls to render each frame (for the grid, and frames per second). Also returns
`frames_running`, which can be set false or true to stop or start the frames.
Binary file added game_of_life.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 38 additions & 22 deletions src/game_of_life.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
//! Implementation of the GameOfLifeGrid component and supporting structures and methods.
//!
//! Adapted from the rust wasm tutorial: https://rustwasm.github.io/docs/book/game-of-life/introduction.html

use dioxus::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};

use crate::{game_of_life::universe::{Cell, Universe, GRID_COLUMNS, GRID_ROWS}, websys_utils::{into_2d_context, into_canvas_element}};
use crate::{
game_of_life::universe::{Cell, Universe, GRID_COLUMNS, GRID_ROWS},
websys_utils::{into_2d_context, into_canvas_element},
};

mod universe;

Expand All @@ -17,17 +22,29 @@ const ALIVE_COLOR: &str = "#000000";
const DEAD_COLOR: &str = "#FFFFFF";

/// Draws the game of life grid, cells and buttons that can modify the universe.
///
/// frame_id represents each frame. Each time the frame_id changes, the universe is advanced.
#[component]
pub fn GameOfLifeGrid(cx: Scope<'a>, frame_id: i32) -> Element {
// State of all the cells in the universe.
let universe = use_ref(cx, Universe::new);
// Set by the "onmounted" event to give drawing functions access to the canvas element.
let canvas_element = use_state(cx, || None::<web_sys::HtmlCanvasElement>);
// Set true to redraw the cells.
// Set true to redraw the cells. Start as false as there is no need to draw an empty grid.
let redraw = use_state(cx, || false);

// Advance and redraw the universe when the frame is advanced.
use_effect(cx, (frame_id,), |(_frame_id,)| {
// Draw the grid when the canvas_element is created. Should happen only once.
use_effect(cx, (canvas_element,), |(_,)| {
to_owned![canvas_element];
async move {
if let Some(canvas_ele) = canvas_element.get() {
draw_grid(canvas_ele);
}
}
});

// Advance and redraw the universe when the frame_id is changed.
use_effect(cx, (frame_id,), |(_,)| {
to_owned![universe, redraw];
async move {
universe.with_mut(|universe| {
Expand All @@ -37,7 +54,7 @@ pub fn GameOfLifeGrid(cx: Scope<'a>, frame_id: i32) -> Element {
}
});

// Redraw all cells when redraw is set to true.
// Redraw the universe when redraw is set to true (and set redraw to false).
use_effect(cx, (redraw,), |(redraw,)| {
to_owned![universe, canvas_element];
async move {
Expand All @@ -53,7 +70,9 @@ pub fn GameOfLifeGrid(cx: Scope<'a>, frame_id: i32) -> Element {
render! {
div { display: "flex", justify_content: "center",
canvas {
onmounted: move |create_event| { config_grid(create_event, canvas_element) },
width: GRID_WIDTH as i64,
height: GRID_HEIGHT as i64,
onmounted: move |create_event| { canvas_element.set(get_canvas_element(create_event)) },
onclick: move |mouse_event| { click_grid(mouse_event, universe, canvas_element) }
}
}
Expand All @@ -80,25 +99,17 @@ fn clear_and_redraw(universe: &UseRef<Universe>, redraw: &UseState<bool>) {
});
}

/// Dig out the canvas element from the "onmount" event and configure the canvas as the grid:
/// * Set the height and width based on the universe size.
/// * Draw the grid.
/// * Set the canvas_element to the state so that it can be retrieved later to draw cells.
fn config_grid(
mount_event: dioxus::prelude::Event<dioxus::events::MountedData>,
canvas_element: &UseState<Option<web_sys::HtmlCanvasElement>>,
) {
/// Dig out the canvas element from the "onmount" event (which we get when the canvas element is created).
fn get_canvas_element(mount_event: dioxus::prelude::Event<dioxus::events::MountedData>) -> Option<HtmlCanvasElement> {
if let Ok(Some(element)) = mount_event
.get_raw_element()
.map(|any| any.downcast_ref::<web_sys::Element>())
{
let canvas_ele = into_canvas_element(element);
Some(into_canvas_element(element))
} else {
console_log!("mount_event should return a HtmlCanvasElement but did not");

canvas_ele.set_height(GRID_HEIGHT);
canvas_ele.set_width(GRID_WIDTH);
draw_grid(&canvas_ele);

canvas_element.set(Some(canvas_ele));
None
}
}

Expand Down Expand Up @@ -128,16 +139,21 @@ fn click_grid(
universe.toggle_cell(row, col);
draw_cells(canvas_ele, universe.cells());
});
} else {
console_log!("Clicked on canvas element before it exists")
}
}

// Draw the grid lines which contain the game of life cells.
//
// Only drawn once at startup as the cells are inbetween the grid
// and thus don't effect the grid pixels.
fn draw_grid(canvas_ele: &HtmlCanvasElement) {
let context = into_2d_context(canvas_ele);

let grid_color = JsValue::from_str(GRID_COLOR);
let height = GRID_HEIGHT;
let width = GRID_WIDTH;
let height = canvas_ele.height();
let width = canvas_ele.width();

context.begin_path();
context.set_line_width(0.5);
Expand Down
57 changes: 7 additions & 50 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// use dioxus_elements::canvas;
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
//! Entrypoint.
//!
//! Adapted from the rust wasm tutorial: https://rustwasm.github.io/docs/book/game-of-life/introduction.html

use dioxus::prelude::*;

use game_of_life::animation::use_animation_frame;
use game_of_life::frames_per_second::FramesPerSecond;
use game_of_life::game_of_life::GameOfLifeGrid;

// Entry point
fn main() {
// launch the dioxus app in a webview
// dioxus_desktop::launch(App);
dioxus_web::launch(App);
}

Expand All @@ -21,51 +20,9 @@ fn App(cx: Scope) -> Element {
h2 { display: "flex", justify_content: "center", font_family: "Helvetica", "Game of Life" }
GameOfLifeGrid { frame_id: *frame_id.get() }
div { display: "flex", justify_content: "center",
button {
onclick: move |_| {
frames_running.set(true);
},
"Start"
}
button {
onclick: move |_| {
frames_running.set(false);
},
"Stop"
}
button { onclick: move |_| { frames_running.set(true) }, "Start" }
button { onclick: move |_| { frames_running.set(false) }, "Stop" }
}
FramesPerSecond { frame_id: *frame_id.get() }
}
}

// https://www.w3schools.com/react/react_useref.asp
// How to do the ref tag of an element?
// How do you do this in dioxus? https://react.dev/learn/manipulating-the-dom-with-refs
// use web_sys::HtmlElement;

// #[component]
// fn Focus(cx: Scope) -> Element {
// let test = use_ref(cx, || None::<i32>);
// let input_element = use_ref(cx, || None::<HtmlElement>);

// // input { r#type: "text", r#ref: input_element }
// // input { r#type: "text" },
// // button { onclick: focus_input, "Focus Input" }
// render! {
// input { r#type: "text", ty: move |_| { input_element } }
// button {
// onclick: move |_| {
// input_element
// .with(|input_element| {
// input_element
// .clone()
// .map(|input_element| {
// let _ = input_element.focus();
// input_element
// });
// });
// },
// "Focus Input"
// }
// }
// }
}

0 comments on commit 847f480

Please sign in to comment.