Skip to content

Commit

Permalink
GUI Components -> Rust
Browse files Browse the repository at this point in the history
index.html just holds the script tag in the body now. We do a nasty hack
in Percy to workaround the lack of an `Rc<VirtualNode>` embedding. See
chinedufn/percy#108

We suffer with an O(n^2) virtual node embedding for now. It seems like
with the current WASM work, we only spend 0.85ms in our Rust code! We
have plenty of time to spare for actual GameBoy logic.

Included here is an unused (for now) memory table debug interface that
will eventually be hooked into the data and GUI.
  • Loading branch information
bkase committed Apr 2, 2019
1 parent f114b12 commit 8d26890
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 21 deletions.
70 changes: 70 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use futures::{Future, FutureExt};
use futures_signals::map_ref;
use futures_signals::signal::{Mutable, Signal, SignalExt};
use game;
use hack_vdom::InjectNode;
use virtual_dom_rs::prelude::*;
use web_utils;

// the idea is that "global" shared state would go here
// Memory or Key-presses would go here
pub struct Globals {
// let's just make sure lifetime stuff is solid
pub unit: Mutable<()>,
// a simple counter per frame
pub frames: Mutable<u32>,
}

/*
* The plan:
*
* * AppState has all atomic pieces of mutable state in `Mutable`s
* * Different components grab different mutables as needed
* * Mutables that wrap mutable references are updated every frame
* * They need cheap PartialEq instances so we can dedupe efficiently
* * Each component (at the leaves!), combines the mutables together and
* creates a single signal
* * Leaf components return Signal<Rc<VirtualNode>>
* * Non-leaves combine the children's Signal<Rc<VirtualNode>>s together
* * At the end we have a single Signal<VirtualNode> that we patch into the DOM
* * AppState owns all the Mutables, but all other components mixin the events they want by
* reference. They mixin readonly signals by value (as these are cheap and are created from
* mutables)
*/
pub struct AppState {
pub globals: Globals,
// we could have one field per component that emits events
}

fn component(state: &AppState) -> impl Signal<Item = VirtualNode> {
let unit = &(state.globals).unit;
let game = game::component(game::State {
unit: Box::new(unit.signal()),
});

map_ref! {
let _ = unit.signal(),
let game_dom = game => {
let inner_dom : InjectNode = InjectNode(game_dom.clone());
html! {
<div class="flex">
<div class="mw7 ph4 mt2 w-60">
{ inner_dom }
</div>
}
}
}
}

pub fn run(app_state: &AppState) -> impl Future<Output = ()> {
let start_view = html! { <div /> };

let body = web_utils::document().body().unwrap();

let mut dom_updater = DomUpdater::new_append_to_mount(start_view, &body);

component(app_state)
.map(move |vdom| dom_updater.update(vdom))
.to_future()
.map(|_| ())
}
75 changes: 75 additions & 0 deletions src/debug_gui.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,72 @@
#![allow(dead_code)]

use futures::{Future, FutureExt};
use futures_signals::signal::{Signal, SignalExt};
use virtual_dom_rs::prelude::*;
use web_utils;

// supports only 16 cols for now
#[derive(Debug, Clone)]
struct MemTableViewModel {
data: Vec<u8>, // assumption: aligned to +0 of the first row
focus: u16, // the row which is centered, invariant 0xXXX0 and >= 0x80
cursor: u16, // invariant in the vec
}

fn mem_table_view<In: Signal<Item = MemTableViewModel>>(
model: In,
) -> impl Signal<Item = VirtualNode> {
model.map(|m| {
let top_labels: Vec<VirtualNode> = (0..16)
.map(|i| html! { <th> { format!("{:02x}", i) } </th> })
.collect();

let data_per_row = m.data.chunks(16);

let draw_row = |(i, row): (usize, &[u8])| {
let cols: Vec<VirtualNode> = row
.iter()
.map(|byte| html! { <td> { format!("{:02x}", byte) } </td> })
.collect();

let ascii: String = row
.iter()
.map(|byte| {
if *byte >= 32 && *byte < 128 {
*byte as char
} else {
'.'
}
})
.collect();

html! {
<tr>
<th> { format!("${:04x}", m.focus + ((i*16) as u16) - (8*16)) } </th>
{ cols }
<td> { ascii } </td>
</tr>
}
};

let all_data: Vec<VirtualNode> = data_per_row.enumerate().map(draw_row).collect();

html! {
<table>
<thead>
<tr>
<th> </th>
{ top_labels }
</tr>
</thead>
<tbody>
{ all_data }
</tbody>
</table>
}
})
}

fn sample_view<In: Signal<Item = u32>>(model: In) -> impl Signal<Item = VirtualNode> {
model.map(|m| {
let greetings = format!("Hello, World! {}", m);
Expand All @@ -17,8 +81,19 @@ pub fn run<In: Signal<Item = u32>>(rx: In) -> impl Future<Output = ()> {

let mut dom_updater = DomUpdater::new_append_to_mount(start_view, &body);

mem_table_view(rx.map(|_i| MemTableViewModel {
data: (0..=255).collect(),
focus: 0xff00,
cursor: 0xff30,
}))
.map(move |vdom| dom_updater.update(vdom))
.to_future()
.map(|_| ())

/*
sample_view(rx)
.map(move |vdom| dom_updater.update(vdom))
.to_future()
.map(|_| ())
*/
}
19 changes: 19 additions & 0 deletions src/game.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use futures_signals::signal::{Signal, SignalExt};
use std::rc::Rc;
use virtual_dom_rs::prelude::*;

pub struct State<S>
where
S: Signal<Item = ()>,
{
// let's just test this thing
pub unit: S,
}

pub fn component<S: Signal<Item = ()>>(state: State<S>) -> impl Signal<Item = Rc<VirtualNode>> {
state.unit.map(|_| {
Rc::new(html! {
<canvas width="160" height="144" id="canvas" style="width: 100%;image-rendering: pixelated;"></canvas>
})
})
}
26 changes: 26 additions & 0 deletions src/hack_vdom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::convert::From;
use std::rc::Rc;
use virtual_dom_rs::prelude::*;
use virtual_dom_rs::{Events, IterableNodes, VElement, VText};

pub struct InjectNode(pub Rc<VirtualNode>);

impl From<InjectNode> for IterableNodes {
fn from(h: InjectNode) -> IterableNodes {
fn clone(n: &VirtualNode) -> VirtualNode {
match n {
VirtualNode::Element(e) => VirtualNode::Element(VElement {
tag: e.tag.clone(),
attrs: e.attrs.clone(),
events: Events(e.events.0.clone()),
children: e.children.iter().map(|v| clone(v)).collect(),
}),
VirtualNode::Text(txt) => VirtualNode::Text(VText {
text: txt.to_string(),
}),
}
}

clone(&*h.0).into()
}
}
33 changes: 23 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
extern crate console_error_panic_hook;
extern crate css_rs_macro;
extern crate futures;
#[macro_use]
extern crate futures_signals;
extern crate futures_util;
extern crate js_sys;
Expand All @@ -23,9 +24,12 @@ pub mod test {
}

mod alu;
mod app;
mod cpu;
mod debug_gui;
mod future_driver;
mod game;
mod hack_vdom;
mod instr;
mod mem;
mod ppu;
Expand Down Expand Up @@ -60,6 +64,21 @@ fn draw_frame(data: &mut Vec<u8>, width: u32, height: u32, i: u32) {
pub fn run() -> Result<(), JsValue> {
utils::set_panic_hook();

// Rc is used to do a closure dance so we can stop the
// request_animation_frame loop
let f = Rc::new(RefCell::new(None));
let g = f.clone();

let app_state: app::AppState = app::AppState {
globals: app::Globals {
unit: Mutable::new(()),
frames: Mutable::new(0),
},
};
let signal_future = Rc::new(RefCell::new(app::run(&app_state)));
// trigger the initial render
let _ = future_driver::tick(signal_future.clone());

let canvas = document().get_element_by_id("canvas").unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
Expand All @@ -79,22 +98,14 @@ pub fn run() -> Result<(), JsValue> {
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();

// Rc is used to do a closure dance so we can stop the
// request_animation_frame loop
let f = Rc::new(RefCell::new(None));
let g = f.clone();

let mut i = 0;
let mut data = vec![0; (height * width * 4) as usize];

let state: Mutable<u32> = Mutable::new(0);
let signal_future = Rc::new(RefCell::new(debug_gui::run(state.signal())));

let mut last = performance_now();
let closure = move || {
{
// Change the state
let mut lock = state.lock_mut();
let mut lock = app_state.globals.frames.lock_mut();
*lock = i;
}

Expand All @@ -120,8 +131,9 @@ pub fn run() -> Result<(), JsValue> {
ctx.put_image_data(&data, 0.0, 0.0).expect("put_image_data");

// Show fps
let fps = (1000.0 / diff).ceil();
ctx.set_font("bold 12px Monaco");
ctx.fill_text(&format!("FPS {}", (1000.0 / diff).ceil()), 10.0, 50.0)
ctx.fill_text(&format!("FPS {}", fps), 10.0, 50.0)
.expect("fill_text");

// Increment once per call
Expand All @@ -139,5 +151,6 @@ pub fn run() -> Result<(), JsValue> {
*g.borrow_mut() = Some(Closure::wrap(Box::new(closure) as Box<FnMut()>));

request_animation_frame(g.borrow().as_ref().unwrap());

Ok(())
}
11 changes: 0 additions & 11 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,6 @@
<link rel="stylesheet" href="https://unpkg.com/tachyons@4/css/tachyons.min.css">
</head>
<body>
<div class="flex">
<div class="mw7 ph4 mt2 w-60">
<canvas width="160" height="144" id="canvas" style="width: 100%;image-rendering: pixelated;"></canvas>
</div>
<div class="mw6 ph4 w-30 ba mt2 pv2">
<h3>Tiles</h3>
<div class="w-100">
<canvas width="32" height="128" id="tiles" style="width: 100%; image-rendering: pixelated;"></canvas>
</div>
</div>
</div>
<script src="./bootstrap.js"></script>
</body>
</html>

0 comments on commit 8d26890

Please sign in to comment.