diff --git a/Cargo.lock b/Cargo.lock index c0b0718..3afc727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,9 +16,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cirru_parser" -version = "0.1.20" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ede7ca2f6902049f7a9db96da195f70ea0bb29a2b9ac4257c252354d4c155c" +checksum = "39236d6a490271b719e0c04687a913360a829b9bd83ba787e4e53eeaea8819e5" [[package]] name = "console_error_panic_hook" @@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "respo" -version = "0.0.12-a1" +version = "0.0.12-a2" dependencies = [ "cirru_parser", "console_error_panic_hook", @@ -175,9 +175,9 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uuid" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" dependencies = [ "getrandom", ] diff --git a/Cargo.toml b/Cargo.toml index da65f5a..6a3786b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "respo" -version = "0.0.12-a1" +version = "0.0.12-a2" edition = "2021" description = "a tiny virtual DOM library migrated from ClojureScript" license = "Apache-2.0" @@ -18,8 +18,8 @@ console_error_panic_hook = "0.1.7" lazy_static = "1.4.0" serde = { version = "1.0.137", features = [ "derive" ] } serde_json = "1.0.81" -uuid = { version = "1.0.0", features = [ "v4", "js" ] } -cirru_parser = "0.1.20" +uuid = { version = "1.1.1", features = [ "v4", "js" ] } +cirru_parser = "0.1.22" rust-hsluv = "0.1.4" [lib] diff --git a/src/app/panel.rs b/src/app/panel.rs index a71ca4c..f75f7f5 100644 --- a/src/app/panel.rs +++ b/src/app/panel.rs @@ -1,7 +1,6 @@ use std::fmt::Debug; use serde::{Deserialize, Serialize}; -use serde_json::Value; use uuid::Uuid; use web_sys::console::log_1; @@ -58,7 +57,7 @@ pub fn comp_panel(states: &StatesTree) -> Result, String> { ]) .to_owned(), ) - .effect(&vec![] as &Vec, move |_, _dispatch, _el| { + .stable_effect(move |_, _dispatch, _el| { log_1(&format!("panel effect {:?}", cursor).into()); Ok(()) }) diff --git a/src/respo.rs b/src/respo.rs index 459f181..6911b2d 100644 --- a/src/respo.rs +++ b/src/respo.rs @@ -96,12 +96,24 @@ where let to_prev_tree = prev_tree.clone(); util::raf_loop_slow(Box::new(move || -> Result<(), String> { + // let to_prev_tree2 = to_prev_tree.clone(); if drain_rerender_status() { let new_tree = renderer()?; let mut changes: Vec> = vec![]; diff_tree(&new_tree, &to_prev_tree.borrow(), &Vec::new(), &Vec::new(), &mut changes)?; - // util::log!("changes: {:?}", changes); + // util::log!( + // "prev tree: {}", + // cirru_parser::format( + // &[to_prev_tree2.borrow().to_owned().into()], + // cirru_parser::CirruWriterOptions { use_inline: true } + // ) + // .unwrap() + // ); + // util::log!( + // "changes: {}", + // cirru_parser::format(&[changes_to_cirru(&changes)], cirru_parser::CirruWriterOptions { use_inline: true }).unwrap() + // ); let handler = handle_event.clone(); patch_tree(&new_tree, &prev_tree.borrow(), &mount_target, &changes, handler)?; diff --git a/src/respo/patch.rs b/src/respo/patch.rs index 46af73f..fff663a 100644 --- a/src/respo/patch.rs +++ b/src/respo/patch.rs @@ -34,7 +34,7 @@ where for op in changes { // crate::util::log!("op: {:?}", op); let coord = op.get_coord(); - let target = find_coord_dom_target(&mount_target.first_child().ok_or("to get first child")?, &op.get_dom_path())?; + let target = find_coord_dom_target(&mount_target.first_child().ok_or("mount position")?, &op.get_dom_path())?; match op { DomChange::ModifyAttrs { set, unset, .. } => { let el = target.dyn_ref::().expect("load as element"); @@ -128,12 +128,20 @@ where let mut next_coord = coord.to_owned(); next_coord.push(RespoCoord::Key(k.to_owned())); let new_element = build_dom_tree(node, &next_coord, handler).expect("new element"); - let base = target.dyn_ref::().expect("to node").first_child().expect("to first child"); - target - .dyn_ref::() - .expect("to node") - .insert_before(&new_element, Some(&base)) - .expect("element appended"); + if target.child_nodes().length() == 0 { + target + .dyn_ref::() + .expect("to node") + .append_child(&new_element) + .expect("element appended"); + } else { + let base = target.dyn_ref::().expect("to node").first_child().ok_or("to first child")?; + target + .dyn_ref::() + .expect("to node") + .insert_before(&new_element, Some(&base)) + .expect("element appended"); + } } ChildDomOp::RemoveAt(idx) => { let child = target diff --git a/src/respo/primes.rs b/src/respo/primes.rs index 28982ab..ef59ea2 100644 --- a/src/respo/primes.rs +++ b/src/respo/primes.rs @@ -1,10 +1,11 @@ +mod dom_change; + use std::boxed::Box; -use std::collections::HashSet; use std::fmt::Display; use std::rc::Rc; use std::{collections::HashMap, fmt::Debug}; -use cirru_parser::{Cirru, CirruWriterOptions}; +use cirru_parser::Cirru; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -14,6 +15,8 @@ use crate::{MaybeState, StatesTree}; use super::css::RespoStyle; +pub use dom_change::{changes_to_cirru, ChildDomOp, DomChange, RespoCoord}; + /// an `Element` or a `Component` #[derive(Debug, Clone, PartialEq, Eq)] pub enum RespoNode @@ -43,16 +46,16 @@ where { fn from(value: RespoNode) -> Self { match value { - RespoNode::Component(name, _eff, tree) => Cirru::List(vec![Cirru::Leaf(name.into()), (*tree).into()]), - RespoNode::Element { name, children, .. } => Cirru::List(vec![ - Cirru::Leaf(name.into()), - Cirru::List( - children - .iter() - .map(|(k, child)| Cirru::List(vec![Cirru::Leaf(k.0.to_owned().into()), (*child).to_owned().into()])) - .collect(), - ), - ]), + RespoNode::Component(name, _eff, tree) => { + Cirru::List(vec![Cirru::Leaf("::Component".into()), Cirru::Leaf(name.into()), (*tree).into()]) + } + RespoNode::Element { name, children, .. } => { + let mut xs = vec![Cirru::Leaf(name.into())]; + for (k, child) in children { + xs.push(Cirru::List(vec![Cirru::Leaf(k.to_string().into()), child.to_owned().into()])); + } + Cirru::List(xs) + } RespoNode::Referenced(cell) => (*cell).to_owned().into(), } } @@ -63,10 +66,7 @@ where T: Debug + Clone, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match cirru_parser::format(&[self.to_owned().into()], CirruWriterOptions { use_inline: true }) { - Ok(s) => write!(f, "{}", s), - Err(e) => write!(f, "{}", e), - } + write!(f, "{}", self) } } @@ -74,20 +74,35 @@ where #[derive(PartialEq, Eq, Debug, Clone)] pub struct RespoIndexKey(String); -impl From for RespoIndexKey -where - T: Display + Clone + Debug, -{ - fn from(data: T) -> Self { +impl From for RespoIndexKey { + fn from(data: usize) -> Self { Self(data.to_string()) } } -// impl Display for RespoIndexKey { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// write!(f, "{}", self.0) -// } -// } +impl From for RespoIndexKey { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for RespoIndexKey { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl Display for RespoIndexKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Cirru { + fn from(k: RespoIndexKey) -> Cirru { + k.to_string().into() + } +} impl RespoNode where @@ -306,6 +321,22 @@ where } } } + /// add an empty args effect on component, which does not update + pub fn stable_effect(&mut self, handler: U) -> &mut Self + where + U: Fn(Vec, RespoEffectType, &Node) -> Result<(), String> + 'static, + { + match self { + RespoNode::Component(_, ref mut effects, _) => { + effects.push(RespoEffect::new(vec![] as Vec<()>, handler)); + self + } + RespoNode::Element { .. } => unreachable!("effects are on components"), + RespoNode::Referenced(_) => { + unreachable!("should not be called on a referenced node"); + } + } + } /// add a list of effects on component pub fn effects(&mut self, more: U) -> &mut Self where @@ -373,6 +404,14 @@ where pub(crate) type StrDict = HashMap; +fn str_dict_to_cirrus_dict(dict: &StrDict) -> Cirru { + let mut xs = vec![]; + for (k, v) in dict { + xs.push(vec![k.to_owned(), v.to_owned()].into()); + } + Cirru::List(xs) +} + /// (internal) struct to store event handler function on the tree #[derive(Clone)] pub struct RespoListenerFn(Rc) -> Result<(), String>>) @@ -415,14 +454,6 @@ where } } -/// coordinate system on RespoNode, to lookup among elements and components -#[derive(Debug, Clone)] -pub enum RespoCoord { - Key(RespoIndexKey), - /// for indexing by component name, even though there's only one of that - Comp(String), -} - /// marks on virtual DOM to declare that there's an event /// event handler is HIDDEN from this mark. #[derive(Debug, Clone)] @@ -542,99 +573,17 @@ pub enum RespoEffectType { BeforeUnmount, } -/// DOM operations used for diff/patching -/// performance is not optimial since looking up the DOM via dom_path has repetitive operations, -/// might need to fix in future is overhead observed. -#[derive(Debug, Clone)] -pub enum DomChange -where - T: Debug + Clone, -{ - ReplaceElement { - coord: Vec, - dom_path: Vec, - node: RespoNode, - }, - ModifyChildren { - coord: Vec, - dom_path: Vec, - operations: Vec>, - }, - ModifyAttrs { - coord: Vec, - dom_path: Vec, - set: StrDict, - unset: HashSet, - }, - ModifyStyle { - coord: Vec, - dom_path: Vec, - set: StrDict, - unset: HashSet, - }, - ModifyEvent { - coord: Vec, - dom_path: Vec, - add: HashSet, - remove: HashSet, - }, - /// this is only part of effects. - /// effects that collected while diffing children are nested inside - Effect { - coord: Vec, - dom_path: Vec, - effect_type: RespoEffectType, - // when args not changed in update, that effects are not re-run - skip_indexes: HashSet, - }, -} - -impl DomChange -where - T: Debug + Clone, -{ - pub fn get_coord(&self) -> Vec { - match self { - DomChange::ReplaceElement { coord, .. } => coord.clone(), - DomChange::ModifyChildren { coord, .. } => coord.clone(), - DomChange::ModifyAttrs { coord, .. } => coord.clone(), - DomChange::ModifyStyle { coord, .. } => coord.clone(), - DomChange::ModifyEvent { coord, .. } => coord.clone(), - DomChange::Effect { coord, .. } => coord.clone(), - } - } - pub fn get_dom_path(&self) -> Vec { - match self { - DomChange::ReplaceElement { dom_path, .. } => dom_path.clone(), - DomChange::ModifyChildren { dom_path, .. } => dom_path.clone(), - DomChange::ModifyAttrs { dom_path, .. } => dom_path.clone(), - DomChange::ModifyStyle { dom_path, .. } => dom_path.clone(), - DomChange::ModifyEvent { dom_path, .. } => dom_path.clone(), - DomChange::Effect { dom_path, .. } => dom_path.clone(), +impl From for Cirru { + fn from(effect_type: RespoEffectType) -> Self { + match effect_type { + RespoEffectType::Mounted => "::mounted".into(), + RespoEffectType::BeforeUpdate => "::before-update".into(), + RespoEffectType::Updated => "::updated".into(), + RespoEffectType::BeforeUnmount => "::before-unmount".into(), } } } -/// used in list diffing, this is still part of `DomChange` -#[derive(Debug, Clone)] -pub enum ChildDomOp -where - T: Debug + Clone, -{ - InsertAfter(u32, RespoIndexKey, RespoNode), - RemoveAt(u32), - Append(RespoIndexKey, RespoNode), - Prepend(RespoIndexKey, RespoNode), - /// order is required in operating children elements, so put effect inside - NestedEffect { - nested_coord: Vec, - nested_dom_path: Vec, - effect_type: RespoEffectType, - // when args not changed in update, that effects are not re-run - skip_indexes: HashSet, - }, -} - /// dispatch function passed from root of renderer, /// call it like `dispatch.run(op)` #[derive(Clone)] diff --git a/src/respo/primes/dom_change.rs b/src/respo/primes/dom_change.rs new file mode 100644 index 0000000..3473453 --- /dev/null +++ b/src/respo/primes/dom_change.rs @@ -0,0 +1,279 @@ +use std::collections::HashSet; + +use std::fmt::Debug; + +use cirru_parser::Cirru; + +use crate::{RespoEffectType, RespoIndexKey, RespoNode, StrDict}; + +use super::str_dict_to_cirrus_dict; + +/// DOM operations used for diff/patching +/// performance is not optimial since looking up the DOM via dom_path has repetitive operations, +/// might need to fix in future is overhead observed. +#[derive(Debug, Clone)] +pub enum DomChange +where + T: Debug + Clone, +{ + ReplaceElement { + coord: Vec, + dom_path: Vec, + node: RespoNode, + }, + ModifyChildren { + coord: Vec, + dom_path: Vec, + operations: Vec>, + }, + ModifyAttrs { + coord: Vec, + dom_path: Vec, + set: StrDict, + unset: HashSet, + }, + ModifyStyle { + coord: Vec, + dom_path: Vec, + set: StrDict, + unset: HashSet, + }, + ModifyEvent { + coord: Vec, + dom_path: Vec, + add: HashSet, + remove: HashSet, + }, + /// this is only part of effects. + /// effects that collected while diffing children are nested inside + Effect { + coord: Vec, + dom_path: Vec, + effect_type: RespoEffectType, + // when args not changed in update, that effects are not re-run + skip_indexes: HashSet, + }, +} + +impl DomChange +where + T: Debug + Clone, +{ + pub fn get_coord(&self) -> Vec { + match self { + DomChange::ReplaceElement { coord, .. } => coord.clone(), + DomChange::ModifyChildren { coord, .. } => coord.clone(), + DomChange::ModifyAttrs { coord, .. } => coord.clone(), + DomChange::ModifyStyle { coord, .. } => coord.clone(), + DomChange::ModifyEvent { coord, .. } => coord.clone(), + DomChange::Effect { coord, .. } => coord.clone(), + } + } + pub fn get_dom_path(&self) -> Vec { + match self { + DomChange::ReplaceElement { dom_path, .. } => dom_path.clone(), + DomChange::ModifyChildren { dom_path, .. } => dom_path.clone(), + DomChange::ModifyAttrs { dom_path, .. } => dom_path.clone(), + DomChange::ModifyStyle { dom_path, .. } => dom_path.clone(), + DomChange::ModifyEvent { dom_path, .. } => dom_path.clone(), + DomChange::Effect { dom_path, .. } => dom_path.clone(), + } + } +} + +impl From> for Cirru +where + T: Debug + Clone, +{ + fn from(change: DomChange) -> Self { + match change { + DomChange::Effect { + coord, + dom_path, + effect_type, + skip_indexes, + } => { + let xs = vec![ + "::effect".into(), + effect_type.into(), + coord_path_to_cirru(coord), + dom_path_to_cirru(&dom_path), + skip_indexes.iter().map(|x| x.to_string()).collect::>().into(), + ]; + Cirru::List(xs) + } + DomChange::ReplaceElement { coord, dom_path, node } => { + let xs = vec![ + "::replace-element".into(), + coord_path_to_cirru(coord), + dom_path_to_cirru(&dom_path), + node.into(), + ]; + Cirru::List(xs) + } + DomChange::ModifyChildren { + coord, + dom_path, + operations, + } => { + let mut xs = vec!["::modify-children".into(), coord_path_to_cirru(coord), dom_path_to_cirru(&dom_path)]; + let mut ys = vec!["::operations".into()]; + for op in operations { + ys.push(op.into()); + } + xs.push(Cirru::List(ys)); + Cirru::List(xs) + } + DomChange::ModifyAttrs { + coord, + dom_path, + set, + unset, + } => { + let xs = vec![ + "::modify-attrs".into(), + coord_path_to_cirru(coord), + dom_path_to_cirru(&dom_path), + str_dict_to_cirrus_dict(&set), + unset.iter().map(|x| x.to_owned()).collect::>().into(), + ]; + Cirru::List(xs) + } + DomChange::ModifyStyle { + coord, + dom_path, + set, + unset, + } => { + let xs = vec![ + "::modify-style".into(), + coord_path_to_cirru(coord), + dom_path_to_cirru(&dom_path), + str_dict_to_cirrus_dict(&set), + unset.iter().map(|x| x.to_owned()).collect::>().into(), + ]; + Cirru::List(xs) + } + DomChange::ModifyEvent { + coord, + dom_path, + add, + remove, + } => { + let xs = vec![ + "::modify-event".into(), + coord_path_to_cirru(coord), + dom_path_to_cirru(&dom_path), + add.iter().map(|x| x.to_owned()).collect::>().into(), + remove.iter().map(|x| x.to_owned()).collect::>().into(), + ]; + Cirru::List(xs) + } + } + } +} + +pub fn changes_to_cirru(change: &[DomChange]) -> Cirru +where + T: Debug + Clone, +{ + let mut xs = vec!["::changes".into()]; + for c in change { + xs.push(c.to_owned().into()); + } + Cirru::List(xs) +} + +/// used in list diffing, this is still part of `DomChange` +#[derive(Debug, Clone)] +pub enum ChildDomOp +where + T: Debug + Clone, +{ + InsertAfter(u32, RespoIndexKey, RespoNode), + RemoveAt(u32), + Append(RespoIndexKey, RespoNode), + Prepend(RespoIndexKey, RespoNode), + /// order is required in operating children elements, so put effect inside + NestedEffect { + nested_coord: Vec, + nested_dom_path: Vec, + effect_type: RespoEffectType, + // when args not changed in update, that effects are not re-run + skip_indexes: HashSet, + }, +} + +impl From> for Cirru +where + T: Debug + Clone, +{ + fn from(op: ChildDomOp) -> Self { + match op { + ChildDomOp::InsertAfter(index, key, node) => { + let xs = vec!["::insert-after".into(), index.to_string().into(), key.into(), node.into()]; + Cirru::List(xs) + } + ChildDomOp::RemoveAt(index) => { + let xs = vec!["::remove-at".into(), index.to_string().into()]; + Cirru::List(xs) + } + ChildDomOp::Append(key, node) => { + let xs = vec!["::append".into(), key.into(), node.into()]; + Cirru::List(xs) + } + ChildDomOp::Prepend(key, node) => { + let xs = vec!["::prepend".into(), key.into(), node.into()]; + Cirru::List(xs) + } + ChildDomOp::NestedEffect { + nested_coord, + nested_dom_path, + effect_type, + skip_indexes, + } => { + let xs = vec![ + "::effect".into(), + effect_type.into(), + coord_path_to_cirru(nested_coord), + nested_dom_path.iter().map(|x| x.to_string()).collect::>().into(), + skip_indexes.iter().map(|x| x.to_string()).collect::>().into(), + ]; + Cirru::List(xs) + } + } + } +} + +/// coordinate system on RespoNode, to lookup among elements and components +#[derive(Debug, Clone)] +pub enum RespoCoord { + Key(RespoIndexKey), + /// for indexing by component name, even though there's only one of that + Comp(String), +} + +impl From for Cirru { + fn from(coord: RespoCoord) -> Self { + match coord { + RespoCoord::Key(key) => key.into(), + RespoCoord::Comp(name) => vec!["::Comp".to_owned(), name].into(), + } + } +} + +fn coord_path_to_cirru(coord: Vec) -> Cirru { + let mut xs = vec!["::coord".into()]; + for c in coord { + xs.push(c.into()); + } + Cirru::List(xs) +} + +fn dom_path_to_cirru(dom_path: &[u32]) -> Cirru { + let mut xs = vec!["::dom-path".into()]; + for c in dom_path { + xs.push(c.to_string().into()); + } + Cirru::List(xs) +} diff --git a/src/respo/util.rs b/src/respo/util.rs index 7b53d05..bf6ac78 100644 --- a/src/respo/util.rs +++ b/src/respo/util.rs @@ -12,7 +12,9 @@ pub fn raf_loop(mut cb: Box Result<(), String>>) { let g = f_.clone(); *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { - cb().expect("called in raq loop"); + if let Err(e) = cb() { + crate::log!("failed in raq loop: {}", e); + } // Schedule ourself for another requestAnimationFrame callback. request_animation_frame(f_.borrow().as_ref().expect("call raq")); @@ -38,7 +40,9 @@ pub fn raf_loop_slow(mut cb: Box Result<(), String>>) { let g = f.clone(); *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { - cb().expect("called in raq loop"); + if let Err(e) = cb() { + crate::log!("failed in slow loop: {}", e); + } let f2 = f.clone(); let h = Closure::wrap(Box::new(move || {