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

-
- -
-
-