use std::{cell::{RefCell}, rc::Rc};
use crate::dom_types;
use crate::dom_types::El;
use crate::websys_bridge;
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(),
}
}
}
pub struct Data<Ms: Clone + Sized + 'static , Mdl: Sized + 'static> {
pub document: web_sys::Document, pub mount_point: web_sys::Element,
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>>
}
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)),
})
}
}
fn update_dom(&self, message: Ms) {
let model_to_update = self.data.model.borrow().clone();
let updated_model = (self.data.update)(message, model_to_update);
let mut topel_new_vdom = (self.data.view)(updated_model.clone());
self.data.model.replace(updated_model);
self.setup_vdom(&mut topel_new_vdom, 0, 0);
detach_listeners(&mut self.data.main_el_vdom.borrow_mut());
patch(
&self.data.document,
&mut self.data.main_el_vdom.borrow_mut(),
&mut topel_new_vdom, &self.data.mount_point,
self.mailbox()
);
self.data.main_el_vdom.replace(topel_new_vdom);
}
pub fn mailbox(&self) -> Mailbox<Ms> {
let cloned = self.clone();
Mailbox::new(move |message| {
cloned.update_dom(message);
})
}
pub fn setup_vdom(&self, el_vdom: &mut El<Ms>, active_level: u32, active_id: u32) {
let mut id = active_id;
el_vdom.id = Some(id);
id += 1; el_vdom.nest_level = Some(active_level);
let el_ws = websys_bridge::make_websys_el(el_vdom, &self.data.document);
el_vdom.el_ws = Some(el_ws);
for child in &mut el_vdom.children {
self.setup_vdom(child, active_level + 1, id);
id += 1;
}
}
}
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),
}
}
}
pub fn attach_listeners<Ms>(el: &mut dom_types::El<Ms>, mailbox: Mailbox<Ms>)
where Ms: Clone + Sized + 'static
{
let el_ws = el.el_ws.take().expect("Missing el_ws on attach_all_listeners");
for listener in &mut el.listeners {
listener.attach(&el_ws, mailbox.clone());
}
for child in &mut el.children {
attach_listeners(child, mailbox.clone())
}
el.el_ws.replace(el_ws);
}
pub fn detach_listeners<Ms>(el: &mut dom_types::El<Ms>)
where Ms: Clone + Sized + 'static
{
let el_ws = el.el_ws.take().expect("Missing el_ws on detach_all_listeners");
for listener in &mut el.listeners {
listener.detach(&el_ws);
}
for child in &mut el.children {
detach_listeners(child)
}
el.el_ws.replace(el_ws);
}
pub fn patch<Ms>(document: &web_sys::Document, old: &mut El<Ms>, new: &mut El<Ms>,
parent: &web_sys::Element, mailbox: Mailbox<Ms>)
where Ms: Clone + Sized + 'static
{
let old_el_ws = old.el_ws.take().expect("No old elws");
if old != new {
if old.tag != new.tag {
parent.remove_child(&old_el_ws).expect("Problem removing an element");
websys_bridge::attach_els(new, parent);
let mut new = new;
attach_listeners(&mut new, mailbox.clone());
return
}
websys_bridge::patch_el_details(old, new, &old_el_ws, document);
}
for listener in &mut new.listeners {
listener.attach(&old_el_ws, mailbox.clone());
}
let avail_old_children = &mut old.children;
for child_new in &mut new.children {
if avail_old_children.is_empty() {
websys_bridge::attach_els(child_new, &old_el_ws);
let mut child_new = child_new;
attach_listeners(&mut child_new, mailbox.clone());
} else {
let mut scores: Vec<(u32, f32)> = avail_old_children.iter()
.map(|c| (c.id.unwrap(), match_score(c, child_new))).collect();
scores.sort_by(|b, a| b.1.partial_cmp(&a.1).unwrap());
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, mailbox.clone()); }
}
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);
}
fn match_score<Ms: Clone>(old: &El<Ms>, new: &El<Ms>) -> f32 {
let mut score = 0.;
if old.tag == new.tag { score += 0.3 } else { score -= 0.3 };
if old.attrs == new.attrs { score += 0.15 } else { score -= 0.15 };
if old.style == new.style { score += 0.05 } else { score -= 0.05 };
if old.text == new.text { score += 0.05 } else { score -= 0.05 };
if old.children.len() == new.children.len() {
score += 0.1
} else { score -= 0.1 }
if old.id.expect("Missing id") == new.id.expect("Missing id") { score += 0.15 } else { score -= 0.15 };
score
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate as seed; 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() {
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();
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);
assert_eq!(2 + 2, 4);
}
}