use crate::dom::{document, now, Measurements};
use crate::vdom;
use crate::vdom::diff;
use crate::vdom::Patch;
use crate::dom::{Application, Cmd, util::body, DomPatch, IdleCallbackHandle, AnimationFrameHandle};
use crate::dom::dom_node::find_all_nodes;
use crate::html::{self, text,attributes::class};
use mt_dom::TreePath;
use std::collections::VecDeque;
use std::{any::TypeId, cell::RefCell, rc::Rc};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{self, Element, IdleDeadline, Node};
use std::collections::BTreeMap;
use wasm_bindgen::closure::Closure;
#[cfg(feature = "with-ric")]
use crate::dom::request_idle_callback;
#[cfg(feature = "with-raf")]
use crate::dom::request_animation_frame;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash,Hasher};
pub struct Program<APP, MSG>
where
MSG: 'static,
{
pub app: Rc<RefCell<APP>>,
pending_msgs: Rc<RefCell<VecDeque<MSG>>>,
pending_cmds: Rc<RefCell<VecDeque<Cmd<APP, MSG>>>>,
current_vdom: Rc<RefCell<vdom::Node<MSG>>>,
pub(crate) root_node: Rc<RefCell<Option<Node>>>,
mount_node: Rc<RefCell<Node>>,
pub node_closures: Rc<RefCell<ActiveClosure>>,
mount_procedure: MountProcedure,
pending_patches: Rc<RefCell<VecDeque<DomPatch<MSG>>>>,
idle_callback_handles: Rc<RefCell<Vec<IdleCallbackHandle>>>,
animation_frame_handles: Rc<RefCell<Vec<AnimationFrameHandle>>>,
#[allow(clippy::type_complexity)]
pub(crate) event_closures: Rc<RefCell<Vec<Closure<dyn FnMut(web_sys::Event)>>>>,
}
pub type ActiveClosure = BTreeMap<usize, Vec<(&'static str, Closure<dyn FnMut(web_sys::Event)>)>>;
#[derive(Clone, Copy)]
pub enum MountAction {
Append,
ClearAppend,
Replace,
}
#[derive(Clone, Copy)]
pub enum MountTarget {
MountNode,
ShadowRoot,
}
#[derive(Clone, Copy)]
struct MountProcedure {
action: MountAction,
target: MountTarget,
}
impl<APP, MSG> Clone for Program<APP, MSG>
where
MSG: 'static,
{
fn clone(&self) -> Self {
Program {
app: Rc::clone(&self.app),
pending_msgs: Rc::clone(&self.pending_msgs),
pending_cmds: Rc::clone(&self.pending_cmds),
current_vdom: Rc::clone(&self.current_vdom),
root_node: Rc::clone(&self.root_node),
mount_node: Rc::clone(&self.mount_node),
node_closures: Rc::clone(&self.node_closures),
mount_procedure: self.mount_procedure,
pending_patches: Rc::clone(&self.pending_patches),
idle_callback_handles: Rc::clone(&self.idle_callback_handles),
animation_frame_handles: Rc::clone(&self.animation_frame_handles),
event_closures: Rc::clone(&self.event_closures),
}
}
}
impl<APP, MSG> Program<APP, MSG>
where
MSG: 'static,
APP: Application<MSG> + 'static,
{
pub fn new(
app: APP,
mount_node: &web_sys::Node,
action: MountAction,
target: MountTarget,
) -> Self {
let view = app.view();
Program {
app: Rc::new(RefCell::new(app)),
pending_msgs: Rc::new(RefCell::new(VecDeque::new())),
pending_cmds: Rc::new(RefCell::new(VecDeque::new())),
current_vdom: Rc::new(RefCell::new(view)),
root_node: Rc::new(RefCell::new(None)),
mount_node: Rc::new(RefCell::new(mount_node.clone())),
node_closures: Rc::new(RefCell::new(ActiveClosure::new())),
mount_procedure: MountProcedure { action, target },
pending_patches: Rc::new(RefCell::new(VecDeque::new())),
idle_callback_handles: Rc::new(RefCell::new(vec![])),
animation_frame_handles: Rc::new(RefCell::new(vec![])),
event_closures: Rc::new(RefCell::new(vec![])),
}
}
fn after_mounted(&self) {
let cmds = self.app.borrow_mut().init();
cmds.into_iter().for_each(|cmd|cmd.emit(self));
self.inject_dynamic_style();
}
fn app_hash() -> u64{
let type_id = TypeId::of::<APP>();
let mut hasher = DefaultHasher::new();
type_id.hash(&mut hasher);
hasher.finish()
}
fn inject_stylesheet(&self){
let static_styles = APP::stylesheet();
if !static_styles.is_empty() {
let static_css = static_styles.join("");
let class_names = format!("static {}", Self::app_hash());
self.inject_style(class_names, &static_css);
}
}
fn inject_dynamic_style(&self){
let dynamic_styles = self.app.borrow().style();
if !dynamic_styles.is_empty() {
let dynamic_css = dynamic_styles.join("");
let class_names = format!("dynamic {}", Self::app_hash());
self.inject_style(class_names, &dynamic_css);
}
}
pub fn mount_node(&self) -> web_sys::Node {
self.mount_node.borrow().clone()
}
pub fn append_to_mount(app: APP, mount_node: &web_sys::Node) -> Self {
let program = Self::new(app, mount_node, MountAction::Append, MountTarget::MountNode);
program.mount();
program
}
pub fn replace_mount(app: APP, mount_node: &web_sys::Node) -> Self {
let program = Self::new(
app,
mount_node,
MountAction::Replace,
MountTarget::MountNode,
);
program.mount();
program
}
pub fn clear_append_to_mount(app: APP, mount_node: &web_sys::Node) -> Self {
let program = Self::new(
app,
mount_node,
MountAction::ClearAppend,
MountTarget::MountNode,
);
program.mount();
program
}
pub fn mount_to_body(app: APP) -> Self {
Self::append_to_mount(app, &body())
}
pub fn mount(&self) {
self.inject_stylesheet();
let created_node = self.create_dom_node(
&self.current_vdom.borrow(),
);
let mount_node: web_sys::Node = match self.mount_procedure.target {
MountTarget::MountNode => self.mount_node.borrow().clone(),
MountTarget::ShadowRoot => {
let mount_element: web_sys::Element =
self.mount_node.borrow().clone().unchecked_into();
mount_element
.attach_shadow(&web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open))
.expect("unable to attached shadow");
let mount_shadow = mount_element.shadow_root().expect("must have a shadow");
*self.mount_node.borrow_mut() = mount_shadow.unchecked_into();
self.mount_node.borrow().clone()
}
};
match self.mount_procedure.action {
MountAction::Append => {
Self::append_child_and_dispatch_mount_event(&mount_node, &created_node);
}
MountAction::ClearAppend => {
let mount_element: &Element = mount_node.unchecked_ref();
log::debug!("mount_node: {:?}", mount_element.outer_html());
let children = mount_node.child_nodes();
log::debug!("There are {}", children.length());
let child_nodes: Vec<Node> = (0..children.length())
.map(|i| children.item(i).expect("must have a child"))
.collect();
child_nodes.into_iter().for_each(|child| {
mount_node.remove_child(&child).expect("must remove child");
});
Self::append_child_and_dispatch_mount_event(&mount_node, &created_node);
}
MountAction::Replace => {
let mount_element: &Element = mount_node.unchecked_ref();
mount_element
.replace_with_with_node_1(&created_node)
.expect("Could not append child to mount");
Self::dispatch_mount_event(&created_node);
*self.mount_node.borrow_mut() = created_node.clone()
}
}
log::debug!("Root node is now set..");
*self.root_node.borrow_mut() = Some(created_node);
self.after_mounted();
}
#[cfg(feature = "with-ric")]
fn dispatch_pending_msgs_with_ric(&self) -> Result<(), JsValue> {
let program = self.clone();
let handle = request_idle_callback(move |deadline| {
program
.dispatch_pending_msgs(Some(deadline))
.expect("must execute")
})
.expect("must execute");
self.idle_callback_handles.borrow_mut().push(handle);
Ok(())
}
fn dispatch_pending_msgs(&self, deadline: Option<IdleDeadline>) -> Result<(), JsValue> {
if self.pending_msgs.borrow().is_empty() {
return Ok(());
}
let mut i = 0;
let t1 = now();
let mut did_complete = true;
while let Some(pending_msg) = self.pending_msgs.borrow_mut().pop_front() {
let cmd = self.app.borrow_mut().update(pending_msg);
self.pending_cmds.borrow_mut().push_back(cmd);
let t2 = now();
let elapsed = t2 - t1;
if let Some(deadline) = &deadline {
if deadline.did_timeout() {
log::warn!("elapsed time: {}ms", elapsed);
log::warn!("we should be breaking at {}..", i);
did_complete = false;
break;
}
}
i += 1;
}
if !did_complete {
#[cfg(feature = "with-ric")]
self.dispatch_pending_msgs_with_ric()
.expect("must complete");
}
Ok(())
}
pub fn update_dom(&self) -> Result<Measurements, JsValue> {
let t1 = now();
let view = self.app.borrow().view();
let t2 = now();
let node_count = view.node_count();
let total_patches = self.update_dom_with_vdom(view).expect("must not error");
let t3 = now();
let measurements = Measurements {
name: None,
node_count,
build_view_took: t2 - t1,
total_patches,
dom_update_took: t3 - t2,
total_time: t3 - t1,
};
if measurements.total_time > 16.0 {
#[cfg(all(feature = "with-measure", feature = "with-debug"))]
log::warn!("dispatch took {}ms", measurements.total_time.round());
}
Ok(measurements)
}
pub fn update_dom_with_vdom(&self, new_vdom: vdom::Node<MSG>) -> Result<usize, JsValue> {
let total_patches = {
let current_vdom = self.current_vdom.borrow();
let patches = diff(¤t_vdom, &new_vdom);
#[cfg(feature = "with-debug")]
log::debug!("There are {} patches", patches.len());
#[cfg(feature = "with-debug")]
log::debug!("patches: {patches:#?}");
let dom_patches = self
.convert_patches(&patches)
.expect("must convert patches");
self.pending_patches.borrow_mut().extend(dom_patches);
self.apply_pending_patches_with_priority_raf();
patches.len()
};
*self.current_vdom.borrow_mut() = new_vdom;
Ok(total_patches)
}
pub fn set_current_dom(&self, new_vdom: vdom::Node<MSG>) {
let created_node =
self.create_dom_node(&new_vdom);
self.mount_node
.borrow_mut()
.append_child(&created_node)
.expect("Could not append child to mount");
*self.root_node.borrow_mut() = Some(created_node);
*self.current_vdom.borrow_mut() = new_vdom;
}
fn convert_patches(&self, patches: &[Patch<MSG>]) -> Result<Vec<DomPatch<MSG>>, JsValue> {
let nodes_to_find: Vec<(&TreePath, Option<&&'static str>)> = patches
.iter()
.map(|patch| (patch.path(), patch.tag()))
.collect();
let nodes_to_patch = find_all_nodes(
self.root_node
.borrow()
.as_ref()
.expect("must have a root node"),
&nodes_to_find,
);
let dom_patches:Vec<DomPatch<MSG>> = patches.iter().map(|patch|{
let patch_path = patch.path();
let patch_tag = patch.tag();
if let Some(target_node) = nodes_to_patch.get(patch_path) {
let target_element: &Element = target_node.unchecked_ref();
if let Some(tag) = patch_tag {
let target_tag = target_element.tag_name().to_lowercase();
if target_tag != **tag {
panic!(
"expecting a tag: {tag:?}, but found: {target_tag:?}"
);
}
}
self.convert_patch(target_element, patch)
} else {
unreachable!("Getting here means we didn't find the element of next node that we are supposed to patch, patch_path: {:?}, with tag: {:?}", patch_path, patch_tag);
}
}).collect();
Ok(dom_patches)
}
fn apply_pending_patches_with_priority_raf(&self) {
#[cfg(feature = "with-raf")]
self.apply_pending_patches_with_raf()
.expect("must complete");
#[cfg(not(feature = "with-raf"))]
{
#[cfg(feature = "with-ric")]
self.apply_pending_patches_with_ric()
.expect("must complete");
#[cfg(not(feature = "with-ric"))]
self.apply_pending_patches(None).expect("must complete");
#[cfg(not(feature = "with-ric"))]
{
let program = self.clone();
wasm_bindgen_futures::spawn_local(async move {
program.apply_pending_patches(None).expect("must complete");
})
}
}
}
fn apply_pending_patches_with_priority_ric(&self) {
#[cfg(feature = "with-ric")]
self.apply_pending_patches_with_ric()
.expect("must complete");
#[cfg(not(feature = "with-ric"))]
{
#[cfg(feature = "with-raf")]
self.apply_pending_patches_with_raf()
.expect("must complete");
#[cfg(not(feature = "with-raf"))]
{
let program = self.clone();
wasm_bindgen_futures::spawn_local(async move {
program.apply_pending_patches(None).expect("must complete");
})
}
}
}
#[cfg(feature = "with-ric")]
fn apply_pending_patches_with_ric(&self) -> Result<(), JsValue> {
let program = self.clone();
let handle = request_idle_callback(move |deadline| {
program
.apply_pending_patches(Some(deadline))
.expect("must not error");
})
.expect("must complete the remaining pending patches..");
self.idle_callback_handles.borrow_mut().push(handle);
Ok(())
}
#[cfg(feature = "with-raf")]
fn apply_pending_patches_with_raf(&self) -> Result<(), JsValue> {
let program = self.clone();
let handle = request_animation_frame(move || {
program.apply_pending_patches(None).expect("must not error");
})
.expect("must execute");
self.animation_frame_handles.borrow_mut().push(handle);
Ok(())
}
fn apply_pending_patches(&self, deadline: Option<IdleDeadline>) -> Result<(), JsValue> {
if self.pending_patches.borrow().is_empty() {
return Ok(());
}
let mut did_complete = true;
while let Some(dom_patch) = self.pending_patches.borrow_mut().pop_front() {
self.apply_dom_patch(dom_patch)
.expect("must apply dom patch");
if let Some(deadline) = &deadline {
if deadline.did_timeout() {
did_complete = false;
break;
}
}
}
if !did_complete {
self.apply_pending_patches_with_priority_ric();
}
Ok(())
}
#[allow(unused_variables)]
fn dispatch_dom_changes(&self, log_measurements: bool) {
let measurements = self.update_dom().expect("must update dom");
#[cfg(feature = "with-measure")]
if log_measurements && measurements.total_patches > 0 {
let cmd_measurement = self.app.borrow().measurements(measurements).no_render();
cmd_measurement.emit(self);
}
}
#[cfg(feature = "with-ric")]
fn dispatch_inner_with_ric(&self) {
let program = self.clone();
let handle = request_idle_callback(move |deadline| {
program.dispatch_inner(Some(deadline));
})
.expect("must execute");
self.idle_callback_handles.borrow_mut().push(handle);
}
#[allow(unused)]
#[cfg(feature = "with-raf")]
fn dispatch_inner_with_raf(&self) {
let program = self.clone();
let handle = request_animation_frame(move || {
program.dispatch_inner(None);
})
.expect("must execute");
self.animation_frame_handles.borrow_mut().push(handle);
}
fn dispatch_inner_with_priority_ric(&self) {
#[cfg(feature = "with-ric")]
self.dispatch_inner_with_ric();
#[cfg(not(feature = "with-ric"))]
{
#[cfg(feature = "with-raf")]
self.dispatch_inner_with_raf();
#[cfg(not(feature = "with-raf"))]
{
let program = self.clone();
wasm_bindgen_futures::spawn_local(async move {
program.dispatch_inner(None);
})
}
}
}
fn dispatch_inner(&self, deadline: Option<IdleDeadline>) {
self.dispatch_pending_msgs(deadline)
.expect("must dispatch msgs");
if !self.pending_msgs.borrow().is_empty() {
self.dispatch_pending_msgs(None)
.expect("must dispatch all pending msgs");
}
if !self.pending_msgs.borrow().is_empty() {
panic!("Can not proceed until previous pending msgs are dispatched..");
}
let mut all_cmd = vec![];
let mut pending_cmds = self.pending_cmds.borrow_mut();
while let Some(cmd) = pending_cmds.pop_front() {
all_cmd.push(cmd);
}
let cmd = Cmd::batch(all_cmd);
if !self.pending_patches.borrow().is_empty() {
log::error!(
"BEFORE DOM updates there are still Remaining pending patches: {}",
self.pending_patches.borrow().len()
);
}
if cmd.modifier.should_update_view {
let log_measurements = cmd.modifier.log_measurements;
self.dispatch_dom_changes(log_measurements);
}
if !self.pending_patches.borrow().is_empty() {
self.apply_pending_patches(None)
.expect("applying pending patches..");
}
if !self.pending_patches.borrow().is_empty() {
log::error!(
"Remaining pending patches: {}",
self.pending_patches.borrow().len()
);
panic!(
"There are still pending patches.. can not emit cmd, if all pending patches
has not been applied yet!"
);
}
cmd.emit(self);
}
fn inject_style(&self, class_names: String, style: &str) {
let style_node = html::tags::style(
[class(class_names)],
[text(style)],
);
let created_node = self.create_dom_node(&style_node);
let head = document().head().expect("must have a head");
head.append_child(&created_node)
.expect("must append style");
}
pub fn inject_style_to_mount(&self, style: &str) {
let style_node = html::tags::style([], [text(style)]);
let created_node = self.create_dom_node(&style_node);
self.mount_node
.borrow_mut()
.append_child(&created_node)
.expect("could not append child to mount shadow");
}
}
impl<APP, MSG> Program<APP, MSG>
where
MSG: 'static,
APP: Application<MSG> + 'static,
{
pub fn dispatch_multiple(&self, msgs: impl IntoIterator<Item = MSG>) {
self.pending_msgs.borrow_mut().extend(msgs);
self.dispatch_inner_with_priority_ric();
}
pub fn dispatch(&self, msg: MSG) {
self.dispatch_multiple([msg])
}
}