use crate::{
dom_types,
dom_types::{El, Namespace},
routing, util, websys_bridge,
};
use std::{cell::RefCell, collections::HashMap, panic, rc::Rc};
use wasm_bindgen::closure::Closure;
use web_sys::{Document, Element, Event, EventTarget, Window};
pub enum Update<Mdl: Clone> {
Render(Mdl),
Skip(Mdl),
}
type UpdateFn<Ms, Mdl> = fn(Ms, Mdl) -> Update<Mdl>;
type ViewFn<Ms, Mdl> = fn(App<Ms, Mdl>, &Mdl) -> El<Ms>;
type Routes<Ms> = HashMap<String, Ms>;
type WindowEvents<Ms, Mdl> = fn(Mdl) -> Vec<dom_types::Listener<Ms>>;
pub struct Mailbox<Message: 'static> {
func: Rc<Fn(Message)>,
}
impl<Ms> 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(),
}
}
}
type StoredPopstate = RefCell<Option<Closure<FnMut(Event)>>>;
pub struct AppData<Ms: Clone + 'static, Mdl: Clone> {
pub model: RefCell<Mdl>,
main_el_vdom: RefCell<El<Ms>>,
pub popstate_closure: StoredPopstate,
pub routes: RefCell<Routes<Ms>>,
window_listeners: RefCell<Vec<dom_types::Listener<Ms>>>,
}
pub struct AppCfg<Ms: Clone + 'static, Mdl: Clone + 'static> {
document: web_sys::Document,
mount_point: web_sys::Element,
pub update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
window_events: Option<WindowEvents<Ms, Mdl>>,
}
pub struct App<Ms: Clone + 'static, Mdl: 'static + Clone> {
pub cfg: Rc<AppCfg<Ms, Mdl>>,
pub data: Rc<AppData<Ms, Mdl>>,
}
#[derive(Clone)]
pub struct AppBuilder<Ms: Clone + 'static, Mdl: 'static + Clone> {
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
parent_div_id: Option<&'static str>,
routes: Routes<Ms>,
window_events: Option<WindowEvents<Ms, Mdl>>,
}
impl<Ms: Clone, Mdl: Clone> AppBuilder<Ms, Mdl> {
pub fn mount(mut self, id: &'static str) -> Self {
self.parent_div_id = Some(id);
self
}
pub fn routes(mut self, routes: Routes<Ms>) -> Self {
self.routes = routes;
self
}
pub fn window_events(mut self, evts: WindowEvents<Ms, Mdl>) -> Self {
self.window_events = Some(evts);
self
}
pub fn finish(self) -> App<Ms, Mdl> {
let parent_div_id = self.parent_div_id.unwrap_or("app");
App::new(
self.model,
self.update,
self.view,
parent_div_id,
self.routes,
self.window_events,
)
}
}
impl<Ms: Clone, Mdl: Clone> App<Ms, Mdl> {
pub fn build(
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
) -> AppBuilder<Ms, Mdl> {
AppBuilder {
model,
update,
view,
parent_div_id: None,
routes: HashMap::new(),
window_events: None,
}
}
fn new(
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
parent_div_id: &str,
routes: Routes<Ms>,
window_events: Option<WindowEvents<Ms, Mdl>>,
) -> Self {
let window = util::window();
let document = window
.document()
.expect("Can't find the window's document.");
let mount_point = document.get_element_by_id(parent_div_id).unwrap();
Self {
cfg: Rc::new(AppCfg {
document,
mount_point,
update,
view,
window_events,
}),
data: Rc::new(AppData {
model: RefCell::new(model),
main_el_vdom: RefCell::new(El::empty(dom_types::Tag::Div)),
popstate_closure: RefCell::new(None),
routes: RefCell::new(routes),
window_listeners: RefCell::new(Vec::new()),
}),
}
}
pub fn run(self) -> Self {
let window = util::window();
let mut topel_vdom;
match self.cfg.window_events {
Some(_window_events) => {
topel_vdom = (self.cfg.view)(self.clone(), &self.data.model.borrow());
setup_window_listeners(
&util::window(),
&mut Vec::new(),
&mut Vec::new(),
&self.mailbox(),
);
}
None => {
topel_vdom = (self.cfg.view)(self.clone(), &self.data.model.borrow());
}
}
let document = window.document().expect("Problem getting document");
setup_els(&document, &mut topel_vdom, 0, 0);
attach_listeners(&mut topel_vdom, &self.mailbox());
websys_bridge::attach_els(&mut topel_vdom, &self.cfg.mount_point);
self.data.main_el_vdom.replace(topel_vdom);
let routes_inner = self.data.routes.borrow().clone();
let app2 = routing::initial(self.clone(), routes_inner.clone());
routing::update_popstate_listener(&app2, routes_inner);
panic::set_hook(Box::new(console_error_panic_hook::hook));
self
}
pub fn update(&self, message: Ms) {
let model_to_update = self.data.model.borrow().clone();
let updated_model = (self.cfg.update)(message, model_to_update);
let mut should_render = true;
let updated_model = match updated_model {
Update::Render(m) => m,
Update::Skip(m) => {
should_render = false;
m
}
};
if let Some(window_events) = self.cfg.window_events {
let mut new_listeners = (window_events)(updated_model.clone());
setup_window_listeners(
&util::window(),
&mut self.data.window_listeners.borrow_mut(),
&mut new_listeners,
&self.mailbox(),
);
self.data.window_listeners.replace(new_listeners);
}
if should_render {
let mut topel_new_vdom = (self.cfg.view)(self.clone(), &updated_model);
setup_els(&self.cfg.document, &mut topel_new_vdom, 0, 0);
detach_listeners(&mut self.data.main_el_vdom.borrow_mut());
patch(
&self.cfg.document,
&mut self.data.main_el_vdom.borrow_mut(),
&mut topel_new_vdom,
&self.cfg.mount_point,
&self.mailbox(),
);
self.data.main_el_vdom.replace(topel_new_vdom);
}
self.data.model.replace(updated_model);
}
pub fn add_route(&self, path: &str, message: Ms) {
let mut r = self.data.routes.borrow_mut().clone();
r.insert(path.to_string(), message.clone());
routing::update_popstate_listener(&self, r.clone());
self.data.routes.replace(r);
}
fn mailbox(&self) -> Mailbox<Ms> {
let cloned = self.clone();
Mailbox::new(move |message| {
cloned.update(message);
})
}
}
pub fn setup_els<Ms>(document: &Document, el_vdom: &mut El<Ms>, active_level: u32, active_id: u32)
where
Ms: Clone + 'static,
{
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, document);
el_vdom.el_ws = Some(el_ws);
for child in &mut el_vdom.children {
setup_els(document, child, active_level + 1, id);
id += 1;
}
}
impl<Ms: Clone, Mdl: Clone> Clone for App<Ms, Mdl> {
fn clone(&self) -> Self {
App {
cfg: Rc::clone(&self.cfg),
data: Rc::clone(&self.data),
}
}
}
fn attach_listeners<Ms: Clone>(el: &mut dom_types::El<Ms>, mailbox: &Mailbox<Ms>) {
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)
}
el.el_ws.replace(el_ws);
}
fn detach_listeners<Ms: Clone>(el: &mut dom_types::El<Ms>) {
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);
}
fn setup_window_listeners<Ms: Clone>(
window: &Window,
old: &mut Vec<dom_types::Listener<Ms>>,
new: &mut Vec<dom_types::Listener<Ms>>,
mailbox: &Mailbox<Ms>,
) {
for listener in old {
listener.detach(window);
}
for listener in new {
listener.attach(window, mailbox.clone());
}
}
fn patch<Ms: Clone>(
document: &Document,
old: &mut El<Ms>,
new: &mut El<Ms>,
parent: &web_sys::Node,
mailbox: &Mailbox<Ms>,
) {
let old_el_ws = old.el_ws.take().expect("No old elws");
if old != new {
if old.tag != new.tag {
if let Some(unmount_actions) = &mut old.will_unmount {
unmount_actions(&old_el_ws)
}
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);
return;
}
websys_bridge::patch_el_details(old, new, &old_el_ws);
}
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);
} 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); }
}
for child in avail_old_children {
let child_el_ws = child.el_ws.take().expect("Missing child el_ws");
if let Some(unmount_actions) = &mut child.will_unmount {
unmount_actions(&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
}
pub trait Attrs: PartialEq + ToString {
fn vals(self) -> HashMap<String, String>;
}
pub trait Style: PartialEq + ToString {
fn vals(self) -> HashMap<String, String>;
}
pub trait Listener<Ms>: Sized {
fn attach<T: AsRef<EventTarget>>(&mut self, el_ws: &T, mailbox: Mailbox<Ms>);
fn detach<T: AsRef<EventTarget>>(&self, el_ws: &T);
}
pub trait DomEl<Ms>: Sized + PartialEq + DomElLifecycle {
type Tg: PartialEq + ToString; type At: Attrs;
type St: Style;
type Ls: Listener<Ms>;
type Tx: PartialEq + ToString + Clone + Default;
fn tag(self) -> Self::Tg;
fn attrs(self) -> Self::At;
fn style(self) -> Self::St;
fn listeners(self) -> Vec<Self::Ls>;
fn text(self) -> Option<Self::Tx>;
fn children(self) -> Vec<Self>;
fn websys_el(self) -> Option<web_sys::Element>;
fn id(self) -> Option<u32>;
fn raw_html(self) -> bool;
fn namespace(self) -> Option<Namespace>;
fn empty(self) -> Self;
fn set_id(&mut self, id: Option<u32>);
fn set_websys_el(&mut self, el: Option<Element>);
}
pub trait DomElLifecycle {
fn did_mount(self) -> Option<Box<FnMut(&Element)>>;
fn did_update(self) -> Option<Box<FnMut(&Element)>>;
fn will_unmount(self) -> Option<Box<FnMut(&Element)>>;
}
#[cfg(test)]
pub mod tests {
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
use super::*;
use wasm_bindgen_test::*;
use crate as seed; use crate::{div, li, prelude::*};
#[derive(Clone)]
enum Msg {}
#[ignore]
#[test]
fn el_added() {
let mut old_vdom: El<Msg> = div!["text", vec![li!["child1"],]];
let mut new_vdom: El<Msg> = div!["text", vec![li!["child1"], li!["child2"]]];
let doc = util::document();
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).unwrap();
new_ws.append_child(&child1).unwrap();
new_ws.append_child(&child2).unwrap();
let mailbox = Mailbox::new(|msg: Msg| {});
let parent = doc.create_element("div").unwrap();
patch(&doc, &mut old_vdom, &mut new_vdom, &parent, &mailbox);
unimplemented!()
}
#[ignore]
#[test]
fn el_removed() {
unimplemented!()
}
#[ignore]
#[test]
fn el_changed() {
unimplemented!()
}
}