#[cfg(feature = "with-raf")]
use crate::dom::request_animation_frame;
#[cfg(feature = "with-ric")]
use crate::dom::request_idle_callback;
use crate::dom::{document, now, Measurements, Modifier, IdleDeadline};
use crate::dom::{util::body, AnimationFrameHandle, Application, DomPatch, IdleCallbackHandle};
use crate::html::{self, attributes::class, text};
use crate::vdom;
use crate::vdom::diff;
use app_context::AppContext;
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
use std::{
any::TypeId,
cell::{Ref, RefCell},
rc::Rc,
};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{self, Element, Node};
mod app_context;
pub struct Program<APP, MSG>
where
MSG: 'static,
{
pub(crate) app_context: AppContext<APP, 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, BTreeMap<&'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_context: self.app_context.clone(),
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 {
Program {
app_context: AppContext::new(app),
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![])),
}
}
pub fn app(&self) -> Ref<'_, APP> {
self.app_context.app.borrow()
}
fn after_mounted(&self) {
let cmd = self.app_context.init_app();
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_style = self.app_context.static_style();
if !static_style.is_empty() {
let class_names = format!("static {}", Self::app_hash());
self.inject_style(class_names, &static_style);
}
}
fn inject_dynamic_style(&self) {
let dynamic_style = self.app_context.dynamic_style();
if !dynamic_style.is_empty() {
let class_names = format!("dynamic {}", Self::app_hash());
self.inject_style(class_names, &dynamic_style);
}
}
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 pre_mount(&self) {
self.inject_stylesheet();
}
pub fn mount(&self) {
self.pre_mount();
let created_node = self.create_dom_node(&self.app_context.current_vdom());
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 => {
Self::clear_children(&mount_node);
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()
}
}
*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.app_context.has_pending_msgs() {
return Ok(());
}
let mut did_complete = true;
while self.app_context.dispatch_pending_msg() {
if let Some(deadline) = &deadline {
if deadline.did_timeout() {
did_complete = false;
break;
}
}
}
if !did_complete {
#[cfg(feature = "with-ric")]
self.dispatch_pending_msgs_with_ric()
.expect("must complete");
}
Ok(())
}
pub fn update_dom(&self, modifier: &Modifier) -> Result<Measurements, JsValue> {
let t1 = now();
let view = self.app_context.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: modifier.measurement_name.to_string(),
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.app_context.current_vdom();
let patches = diff(¤t_vdom, &new_vdom);
#[cfg(all(feature = "with-debug", feature = "log-patches"))]
{
log::debug!("There are {} patches", patches.len());
log::debug!("patches: {patches:#?}");
}
let dom_patches = self
.convert_patches(&patches)
.expect("must convert patches");
self.pending_patches.borrow_mut().extend(dom_patches);
#[cfg(feature = "with-raf")]
self.apply_pending_patches_with_raf().expect("raf");
#[cfg(not(feature = "with-raf"))]
self.apply_pending_patches().expect("raf");
patches.len()
};
self.app_context.set_current_dom(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.app_context.set_current_dom(new_vdom);
}
#[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().expect("must not error");
})
.expect("must execute");
self.animation_frame_handles.borrow_mut().push(handle);
Ok(())
}
fn apply_pending_patches(&self) -> Result<(), JsValue> {
if self.pending_patches.borrow().is_empty() {
return Ok(());
}
while let Some(dom_patch) = self.pending_patches.borrow_mut().pop_front() {
self.apply_dom_patch(dom_patch)
.expect("must apply dom patch");
}
Ok(())
}
fn dispatch_dom_changes(&self, modifier: &Modifier) {
#[allow(unused_variables)]
let measurements = self.update_dom(modifier).expect("must update dom");
#[cfg(feature = "with-measure")]
if modifier.log_measurements && measurements.total_patches > 0 {
let cmd_measurement = self.app_context.measurements(measurements);
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.app_context.has_pending_msgs() {
self.dispatch_pending_msgs(None)
.expect("must dispatch all pending msgs");
}
if self.app_context.has_pending_msgs() {
panic!("Can not proceed until previous pending msgs are dispatched..");
}
let cmd = self.app_context.batch_pending_cmds();
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 {
self.dispatch_dom_changes(&cmd.modifier);
}
if !self.pending_patches.borrow().is_empty() {
self.apply_pending_patches()
.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");
}
pub fn dispatch_multiple(&self, msgs: impl IntoIterator<Item = MSG>) {
self.app_context.push_msgs(msgs);
self.dispatch_inner_with_priority_ric();
}
pub fn dispatch(&self, msg: MSG) {
self.dispatch_multiple([msg])
}
}