From 8d26890cb1c2c45c37b015db5cbf08aef72f6565 Mon Sep 17 00:00:00 2001 From: Brandon Kase Date: Fri, 29 Mar 2019 00:20:15 -0700 Subject: [PATCH] GUI Components -> Rust 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` embedding. See https://github.com/chinedufn/percy/issues/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. --- src/app.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++ src/debug_gui.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ src/game.rs | 19 ++++++++++++ src/hack_vdom.rs | 26 +++++++++++++++++ src/lib.rs | 33 ++++++++++++++------- www/index.html | 11 ------- 6 files changed, 213 insertions(+), 21 deletions(-) create mode 100644 src/app.rs create mode 100644 src/game.rs create mode 100644 src/hack_vdom.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..5f76a40 --- /dev/null +++ b/src/app.rs @@ -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, +} + +/* + * 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> + * * Non-leaves combine the children's Signal>s together + * * At the end we have a single Signal 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 { + 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! { +
+
+ { inner_dom } +
+ } + } + } +} + +pub fn run(app_state: &AppState) -> impl Future { + let start_view = html! {
}; + + 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(|_| ()) +} diff --git a/src/debug_gui.rs b/src/debug_gui.rs index ff6242a..8771428 100644 --- a/src/debug_gui.rs +++ b/src/debug_gui.rs @@ -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, // 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>( + model: In, +) -> impl Signal { + model.map(|m| { + let top_labels: Vec = (0..16) + .map(|i| html! { { format!("{:02x}", i) } }) + .collect(); + + let data_per_row = m.data.chunks(16); + + let draw_row = |(i, row): (usize, &[u8])| { + let cols: Vec = row + .iter() + .map(|byte| html! { { format!("{:02x}", byte) } }) + .collect(); + + let ascii: String = row + .iter() + .map(|byte| { + if *byte >= 32 && *byte < 128 { + *byte as char + } else { + '.' + } + }) + .collect(); + + html! { + + { format!("${:04x}", m.focus + ((i*16) as u16) - (8*16)) } + { cols } + { ascii } + + } + }; + + let all_data: Vec = data_per_row.enumerate().map(draw_row).collect(); + + html! { + + + + + { top_labels } + + + + { all_data } + +
+ } + }) +} + fn sample_view>(model: In) -> impl Signal { model.map(|m| { let greetings = format!("Hello, World! {}", m); @@ -17,8 +81,19 @@ pub fn run>(rx: In) -> impl Future { 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(|_| ()) + */ } diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..9ead355 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,19 @@ +use futures_signals::signal::{Signal, SignalExt}; +use std::rc::Rc; +use virtual_dom_rs::prelude::*; + +pub struct State +where + S: Signal, +{ + // let's just test this thing + pub unit: S, +} + +pub fn component>(state: State) -> impl Signal> { + state.unit.map(|_| { + Rc::new(html! { + + }) + }) +} diff --git a/src/hack_vdom.rs b/src/hack_vdom.rs new file mode 100644 index 0000000..78b9cb6 --- /dev/null +++ b/src/hack_vdom.rs @@ -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); + +impl From 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() + } +} diff --git a/src/lib.rs b/src/lib.rs index e0ede12..d57a7a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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; @@ -60,6 +64,21 @@ fn draw_frame(data: &mut Vec, 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::() @@ -79,22 +98,14 @@ pub fn run() -> Result<(), JsValue> { .dyn_into::() .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 = 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; } @@ -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 @@ -139,5 +151,6 @@ pub fn run() -> Result<(), JsValue> { *g.borrow_mut() = Some(Closure::wrap(Box::new(closure) as Box)); request_animation_frame(g.borrow().as_ref().unwrap()); + Ok(()) } diff --git a/www/index.html b/www/index.html index 829b278..0e442b9 100644 --- a/www/index.html +++ b/www/index.html @@ -6,17 +6,6 @@ -
-
- -
-
-

Tiles

-
- -
-
-