use std::{cell::{RefCell}, rc::Rc};
use crate::dom_types;
use crate::dom_types::El;
use crate::websys_bridge;
// todo: Get rid of the clone assiated with MS everywhere if you can!
pub struct Mailbox<Message: 'static> {
func: Rc<Fn(Message)>,
}
impl<Ms: 'static> Mailbox<Ms> {
pub fn new(func: impl Fn(Ms) + 'static) -> Self {
Mailbox {
func: Rc::new(func),
}
}
pub fn send(&self, message: Ms) {
(self.func)(message)
}
}
impl<Ms> Clone for Mailbox<Ms> {
fn clone(&self) -> Self {
Mailbox {
func: self.func.clone(),
}
}
}
// todo: Examine what needs to be ref cells, rcs etc
/// Used as part of an interior-mutability pattern, ie Rc<RefCell<>>
pub struct Data<Ms: Clone + Sized + 'static , Mdl: Sized + 'static> {
document: web_sys::Document,
pub mount_point: web_sys::Element,
// Model is in a RefCell here so we can replace it in self.update_dom().
pub model: RefCell<Mdl>,
update: fn(Ms, Mdl) -> Mdl,
pub view: fn(Mdl) -> El<Ms>,
pub main_el_vdom: RefCell<El<Ms>>,
}
pub struct App<Ms: Clone + Sized + 'static , Mdl: Sized + 'static> {
pub data: Rc<Data<Ms, Mdl>>
}
/// We use a struct instead of series of functions, in order to avoid passing
/// repetative sequences of parameters.
impl<Ms: Clone + Sized + 'static, Mdl: Clone + Sized + 'static> App<Ms, Mdl> {
pub fn new(model: Mdl, update: fn(Ms, Mdl) -> Mdl,
view: fn(Mdl) -> El<Ms>, parent_div_id: &str) -> Self {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let mount_point = document.get_element_by_id(parent_div_id).unwrap();
Self {
data: Rc::new(Data {
document,
mount_point: mount_point.clone(),
model: RefCell::new(model),
update,
view,
main_el_vdom: RefCell::new(El::empty(dom_types::Tag::Div)),
})
}
}
/// This runs whenever the state is changed, ie the user-written update function is called.
/// It updates the state, and any DOM elements affected by this change.
/// todo this is where we need to compare against differences and only update nodes affected
/// by the state change.
///
/// We re-create the whole virtual dom each time (Is there a way around this? Probably not without
/// knowing what vars the model holds ahead of time), but only edit the rendered, web_sys dom
/// for things that have been changed.
/// We re-render the virtual DOM on every change, but (attempt to) only change
/// the actual DOM, via web_sys, when we need.
/// The model storred in inner is the old model; updated_model is a newly-calculated one.
fn update_dom(&self, message: Ms) {
// data.model is the old model; pass it to the update function created in the app,
// which outputs an updated model.
// We clone the model before running update, and again before passing it
// to the view func, instead of using refs, to improve API syntax.
// This approach may have performance impacts of unknown magnitude.
let model_to_update = self.data.model.borrow().clone();
let updated_model = (self.data.update)(message, model_to_update);
// Create a new vdom: The top element, and all its children. Does not yet
// have ids, nest levels, or associated web_sys elements.
// We accept cloning here, for the benefit of making data easier to work
// with in the app.
let mut topel_new_vdom = (self.data.view)(updated_model.clone());
// We're now done with this updated model; store it for use as the old
// model for the next update.
// Note: It appears that this step is why we need data.model to be in a RefCell.
self.data.model.replace(updated_model);
// We setup the vdom (which populates web_sys els through it, but don't
// render them with attach_children; we try to do it cleverly via patch().
self.setup_vdom(&mut topel_new_vdom, 0, 0);
// self.data.mount_point.set_inner_html("");
// let el_ws = websys_bridge::make_websys_el(&mut self.data.main_el_vdom.borrow_mut(), &self.data.document, self.mailbox());
// self.data.mount_point.append_child(&el_ws);
// self.data.mount_point.append_child(&self.data.main_el_vdom.borrow().el_ws.unwrap()).unwrap();
// We haven't updated data.main_el_vdom, so we use it as our old (previous) state.
patch(&self.data.document, &mut self.data.main_el_vdom.borrow_mut(), &mut topel_new_vdom, &self.data.mount_point);
// Now that we've re-rendered, replace our stored El with the new one;
// it will be used as the old El next (.
self.data.main_el_vdom.replace(topel_new_vdom);
}
fn mailbox(&self) -> Mailbox<Ms> {
let cloned = self.clone();
Mailbox::new(move |message| {
cloned.update_dom(message);
})
}
/// Populate the attached web_sys elements, ids, and nest-levels. Run this after creating a vdom, but before
/// using it to process the web_sys dom. Does not attach children in the DOM. Run this on the top-level element.
pub fn setup_vdom(&self, el_vdom: &mut El<Ms>, active_level: u32, active_id: u32) {
// id iterates once per item; active-level once per nesting level.
let mut id = active_id;
el_vdom.id = Some(id);
id += 1; // Raise the id after each element we process.
el_vdom.nest_level = Some(active_level);
// Create the web_sys element; add it to the working tree; store it in
// its corresponding vdom El.
let el_ws = websys_bridge::make_websys_el(el_vdom, &self.data.document, self.mailbox());
el_vdom.el_ws = Some(el_ws);
for child in &mut el_vdom.children {
// Raise the active level once per recursion.
self.setup_vdom(child, active_level + 1, id);
id += 1;
}
}
}
// trying this approach leads to lifetime problems.
//fn mailbox<Ms, Mdl>(app: &'static App<Ms, Mdl>) -> Mailbox<Ms>
// where Ms: Clone + Sized + 'static, Mdl: Clone + Sized + 'static {
// Mailbox::new(move |message| {
// app.clone().update_dom(message);
// })
//
//}
impl<Ms: Clone + Sized + 'static , Mdl: Sized + 'static> std::clone::Clone for App<Ms, Mdl> {
fn clone(&self) -> Self {
App {
data: Rc::clone(&self.data),
}
}
}
fn patch<Ms>(document: &web_sys::Document, old: &mut El<Ms>, new: &mut El<Ms>, parent: &web_sys::Element)
where Ms: Clone + Sized + 'static
{
// Todo: Current sceme is that if the parent changes, redraw all children...
// todo fix this later.
// We make an assumption that most of the page is not dramatically changed
// by each event, to optimize.
// todo: There are a lot of ways you could make this more sophisticated.
// Assume setup_vdom has been run on the new el, but only the old vdom's nodes are attached.
// todo only redraw teh whole subtree if children are diff; if it's
// todo just text or attrs etc, patch them.
// take removes the interior value from the Option; otherwise we run into problems
// about not being able to remove from borrowed content.
// We remove it from the old el_vodom now, and at the end... add it to the new one.
// We don't run attach_children() when patching, hence this approach.
// if new.is_dummy() == true { return }
let old_el_ws = old.el_ws.take().expect("No old elws");
if old != new {
// Something about this element itself is different: patch it.
// At this step, we already assume we have the right element - either
// by entering this func directly for the top-level, or recursively after
// analyzing children
if old.tag != new.tag {
parent.remove_child(&old_el_ws).expect("Problem removing this element");
websys_bridge::attach(new, parent);
// We've re-rendered this child and all children; we're done with this recursion.
return
}
// Patch attributes.
websys_bridge::patch_el_details(old, new, &old_el_ws, document);
}
// If there are the same number of children, assume there's a 1-to-1 mapping,
// where we will not add or remove any; but patch as needed.
// A more sophisticated approach would be to find the best match of every
// combination of score of new vs old, then rank them somehow. (Eg even
// if old id=2 is the best match for the first new, if it's only a marginal
// winner, but a strong winner for the second, it makes sense to put it
// in the second, but we are not allowing it this opporunity as-is.
// One approach would be check all combinations, combine scores within each combo, and pick the one
// with the highest total score, but this increases with the factorial of
// child size!
// todo: Look into this improvement sometime after the initial release.
let avail_old_children = &mut old.children;
for child_new in &mut new.children {
if avail_old_children.is_empty() {
// One or more new children has been added, or much content has
// changed, or we've made a mistake: Attach new children.
websys_bridge::attach(child_new, &old_el_ws);
} else {
// We still have old children to pick a match from. If we pick
// incorrectly, or there is no "good" match, we'll have some
// patching and/or attaching (rendering) to do in subsequent recursions.
let mut scores: Vec<(u32, f32)> = avail_old_children.iter()
.map(|c| (c.id.unwrap(), match_score(c, child_new))).collect();
// should put highest score at the end.
scores.sort_by(|b, a| b.1.partial_cmp(&a.1).unwrap());
// Sorting children vice picking the best one makes this easier to handle
// without irking the borrow checker, despite appearing less counter-intuitive,
// due to the convenient pop method.
avail_old_children.sort_by(|b, a| {
scores.iter().find(|s| s.0 == b.id.unwrap()).unwrap().1.partial_cmp(
&scores.iter().find(|s| s.0 == a.id.unwrap()).unwrap().1
).unwrap()
});
let mut best_match = avail_old_children.pop().expect("Probably popping");
patch(document, &mut best_match, child_new, &old_el_ws); // todo old vs new for par
}
}
// Now purge any existing children; they're not part of the new model.
for child in avail_old_children {
let child_el_ws = child.el_ws.take().expect("Missing child el_ws");
old_el_ws.remove_child(&child_el_ws).expect("Problem removing child");
child.el_ws.replace(child_el_ws);
}
new.el_ws = Some(old_el_ws);
}
/// Compare two elements. Rank based on how similar they are, using subjective criteria.
fn match_score<Ms: Clone>(old: &El<Ms>, new: &El<Ms>) -> f32 {
// children_to_eval is the number of children to look at on each nest level.
// let children_to_eval = 3;
// Don't weight children as heavily as the parent. This effect acculates the further down we go.
// let child_score_significance = 0.6;
let mut score = 0.;
// Tags are not likely to change! Good indicator of it being the wrong element.
if old.tag == new.tag { score += 0.3 } else { score -= 0.3 };
// Attrs are not likely to change.
// todo: Compare attrs more directly.
if old.attrs == new.attrs { score += 0.15 } else { score -= 0.15 };
// Style is likely to change.
if old.style == new.style { score += 0.05 } else { score -= 0.05 };
// Text is likely to change, but may still be a good indicator.
if old.text == new.text { score += 0.05 } else { score -= 0.05 };
// For children length, don't do it based on the difference, since children that actually change in
// len may have very large changes. But having identical length is a sanity check.
if old.children.len() == new.children.len() {
score += 0.1
// } else if (old.children.len() as i16 - new.children.len() as i16).abs() == 1 {
// // Perhaps we've added or removed a child.
// score += 0.05 // todo non-even transaction
} else { score -= 0.1 }
// Same id implies it may have been added in the same order.
if old.id.expect("Missing id") == new.id.expect("Missing id") { score += 0.15 } else { score -= 0.15 };
// For now, just look at the first child: Easier to code, and still helps.
// Doing indefinite recursion of first child for now. Weight each child
// subsequently-less. This is effective for some common HTML patterns.
// for posit in 0..children_to_eval {
// if let Some(child_old) = &old.children.get(posit) {
// if let Some(child_new) = &old.children.get(posit) {
// score += child_score_significance * match_score(child_old, child_new);
// }
// }
// }
score
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate as seed; // required for macros to work.
use crate::dom_types::{UpdateListener, UpdateEl};
fn make_doc() -> web_sys::Document {
let window = web_sys::window().unwrap();
window.document().unwrap()
}
#[derive(Clone)]
enum Msg {}
#[test]
fn el_added() {
// todo macros not working here.
let old_vdom: El<Msg> = div![ "text", vec![
li![ "child1" ],
] ];
let new_vdom: El<Msg> = div![ "text", vec![
li![ "child1" ],
li![ "child2" ]
] ];
let doc = make_doc();
let old_ws = doc.create_element("div").unwrap();
let new_ws = doc.create_element("div").unwrap();
let child1 = doc.create_element("li").unwrap();
let child2 = doc.create_element("li").unwrap();
// todo make this match how you're setting text_content, eg could
// todo be adding a text node.
old_ws.set_text_content(Some("text"));
child1.set_text_content(Some("child1"));
child2.set_text_content(Some("child2"));
old_ws.append_child(&child1);
new_ws.append_child(&child1);
new_ws.append_child(&child2);
// let patched = patch();
assert_eq!(2 + 2, 4);
}
}