#![allow(clippy::module_name_repetitions)]
use crate::browser::dom::virtual_dom_bridge;
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
use crate::browser::service::routing;
use crate::browser::{
util::{self, window},
Url, DUMMY_BASE_URL,
};
use crate::virtual_dom::{patch, El, EventHandlerManager, IntoNodes, Mailbox, Node, Tag};
use enclose::enclose;
use std::{
any::Any,
cell::{Cell, RefCell},
collections::VecDeque,
fmt,
rc::Rc,
};
use sub_manager::SubManager;
use wasm_bindgen::closure::Closure;
pub mod cfg;
pub mod cmd_manager;
pub mod cmds;
pub mod data;
mod effect;
pub mod get_element;
pub mod message_mapper;
pub mod orders;
pub mod render_info;
pub mod stream_manager;
pub mod streams;
pub mod sub_manager;
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
pub mod subs;
pub use cfg::AppCfg;
pub use cmd_manager::CmdHandle;
pub(crate) use data::AppData;
use effect::Effect;
pub use get_element::GetElement;
pub use message_mapper::MessageMapper;
pub use orders::{Orders, OrdersContainer, OrdersProxy};
pub use render_info::RenderInfo;
pub use stream_manager::StreamHandle;
pub use sub_manager::{Notification, SubHandle};
pub enum ShouldRender {
Render,
ForceRenderNow,
Skip,
}
pub struct App<Ms, Mdl, INodes>
where
Ms: 'static,
Mdl: 'static,
INodes: IntoNodes<Ms>,
{
cfg: Rc<AppCfg<Ms, Mdl, INodes>>,
data: Rc<AppData<Ms, Mdl>>,
}
impl<Ms, Mdl, INodes> fmt::Debug for App<Ms, Mdl, INodes>
where
Ms: 'static,
Mdl: 'static,
INodes: IntoNodes<Ms>,
{
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> fmt::Result {
write!(f, "App")
}
}
impl<Ms, Mdl, INodes> Clone for App<Ms, Mdl, INodes>
where
INodes: IntoNodes<Ms>,
{
fn clone(&self) -> Self {
Self {
cfg: Rc::clone(&self.cfg),
data: Rc::clone(&self.data),
}
}
}
impl<Ms, Mdl, INodes> App<Ms, Mdl, INodes>
where
INodes: IntoNodes<Ms> + 'static,
{
pub fn start(
root_element: impl GetElement,
init: impl FnOnce(Url, &mut OrdersContainer<Ms, Mdl, INodes>) -> Mdl + 'static,
update: impl FnOnce(Ms, &mut Mdl, &mut OrdersContainer<Ms, Mdl, INodes>) + Clone + 'static,
view: impl FnOnce(&Mdl) -> INodes + Clone + 'static,
) -> Self {
std::mem::drop(util::document().query_selector("html"));
#[cfg(feature = "panic-hook")]
console_error_panic_hook::set_once();
let base_path: Rc<[String]> = Rc::from(
util::document()
.query_selector("base")
.expect("query element with 'base' tag")
.and_then(|element| element.get_attribute("href"))
.and_then(|href| web_sys::Url::new_with_base(&href, DUMMY_BASE_URL).ok())
.map(|url| {
url.pathname()
.trim_matches('/')
.split('/')
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default()
.as_slice(),
);
let app = Self {
cfg: Rc::new(AppCfg {
document: util::window().document().expect("get window's document"),
mount_point: root_element.get_element().expect("get root element"),
update: Box::new(move |msg, model, orders| update.clone()(msg, model, orders)),
view: Box::new(move |model| view.clone()(model)),
base_path,
}),
data: Rc::new(AppData {
model: RefCell::new(None),
root_el: RefCell::new(None),
popstate_closure: RefCell::new(None),
hashchange_closure: RefCell::new(None),
window_event_handler_manager: RefCell::new(EventHandlerManager::new()),
sub_manager: RefCell::new(SubManager::new()),
msg_listeners: RefCell::new(Vec::new()),
scheduled_render_handle: RefCell::new(None),
after_next_render_callbacks: RefCell::new(Vec::new()),
render_info: Cell::new(None),
}),
};
app.data.root_el.replace(Some(app.bootstrap_vdom()));
let mut orders = OrdersContainer::new(app.clone());
let new_model = init(
Url::current().skip_base_path(&Rc::clone(&app.cfg.base_path)),
&mut orders,
);
app.data.model.replace(Some(new_model));
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
app.setup_routing(&mut orders);
app.process_effect_queue(orders.effects);
app.rerender_vdom();
app
}
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
fn setup_routing(&self, orders: &mut impl Orders<Ms>) {
use enclose::enc;
routing::setup_popstate_listener(
enc!((self => s) move |closure| {
s.data.popstate_closure.replace(Some(closure));
}),
enc!((self => s) move |notification| s.notify_with_notification(notification)),
Rc::clone(&self.cfg.base_path),
);
routing::setup_link_listener(
enc!((self => s) move |notification| s.notify_with_notification(notification)),
);
orders.subscribe(enc!((self => s) move |url_requested| {
routing::url_request_handler(
url_requested,
Rc::clone(&s.cfg.base_path),
move |notification| s.notify_with_notification(notification),
);
}));
}
pub fn update(&self, message: Ms) {
self.update_with_option(Some(message));
}
pub fn update_with_option(&self, message: Option<Ms>) {
let mut queue: VecDeque<Effect<Ms>> = VecDeque::new();
queue.push_front(Effect::Msg(message));
self.process_effect_queue(queue);
}
pub fn notify<SubMs: 'static + Any + Clone>(&self, message: SubMs) {
let mut queue: VecDeque<Effect<Ms>> = VecDeque::new();
queue.push_front(Effect::Notification(Notification::new(message)));
self.process_effect_queue(queue);
}
pub fn notify_with_notification(&self, notification: Notification) {
let mut queue: VecDeque<Effect<Ms>> = VecDeque::new();
queue.push_front(Effect::Notification(notification));
self.process_effect_queue(queue);
}
pub(crate) fn process_effect_queue(&self, mut queue: VecDeque<Effect<Ms>>) {
if std::thread::panicking() {
return;
}
while let Some(effect) = queue.pop_front() {
match effect {
Effect::Msg(msg) => {
let mut new_effects = self.process_queue_message(msg);
queue.append(&mut new_effects);
}
Effect::Notification(notification) => {
let mut new_effects = self.process_queue_notification(¬ification);
queue.append(&mut new_effects);
}
Effect::TriggeredHandler(handler) => {
let mut new_effects = self.process_queue_message(handler());
queue.append(&mut new_effects);
}
}
}
}
fn bootstrap_vdom(&self) -> El<Ms> {
let mut new = El::empty(Tag::Placeholder);
let mut dom_nodes: El<Ms> = (&self.cfg.mount_point).into();
#[cfg(debug_assertions)]
dom_nodes.warn_about_script_tags();
dom_nodes.strip_ws_nodes_from_self_and_children();
new.children = dom_nodes.children;
virtual_dom_bridge::assign_ws_nodes_to_el(&util::document(), &mut new);
while let Some(child) = self.cfg.mount_point.first_child() {
self.cfg
.mount_point
.remove_child(&child)
.expect("No problem removing node from parent.");
}
for child in &mut new.children {
match child {
Node::Element(child_el) => {
virtual_dom_bridge::attach_el_and_children(
child_el,
&self.cfg.mount_point,
&self.mailbox(),
);
}
Node::Text(top_child_text) => {
virtual_dom_bridge::attach_text_node(top_child_text, &self.cfg.mount_point);
}
Node::Empty | Node::NoChange => (),
}
}
new
}
fn rerender_vdom(&self) {
if std::thread::panicking() {
return;
}
let new_render_timestamp = window().performance().expect("get `Performance`").now();
let mut new = El::empty(Tag::Placeholder);
new.children = (self.cfg.view)(self.data.model.borrow().as_ref().unwrap()).into_nodes();
let old = self
.data
.root_el
.borrow_mut()
.take()
.expect("missing root element");
patch::patch_els(
&self.cfg.document,
&self.mailbox(),
&self.clone(),
&self.cfg.mount_point,
old.children.into_iter(),
new.children.iter_mut(),
);
self.data.root_el.borrow_mut().replace(new);
let render_info = match self.data.render_info.take() {
Some(old_render_info) => RenderInfo {
timestamp: new_render_timestamp,
timestamp_delta: Some(new_render_timestamp - old_render_info.timestamp),
},
None => RenderInfo {
timestamp: new_render_timestamp,
timestamp_delta: None,
},
};
self.data.render_info.set(Some(render_info));
self.process_effect_queue(
self.data
.after_next_render_callbacks
.replace(Vec::new())
.into_iter()
.map(|callback| Effect::TriggeredHandler(Box::new(move || callback(render_info))))
.collect(),
);
}
fn process_queue_notification(&self, notification: &Notification) -> VecDeque<Effect<Ms>> {
self.data
.sub_manager
.borrow()
.notify(notification)
.into_iter()
.map(Effect::TriggeredHandler)
.collect()
}
fn process_queue_message(&self, message: Option<Ms>) -> VecDeque<Effect<Ms>> {
let mut orders = OrdersContainer::new(self.clone());
if let Some(message) = message {
for l in self.data.msg_listeners.borrow().iter() {
(l)(&message);
}
(self.cfg.update)(
message,
self.data.model.borrow_mut().as_mut().unwrap(),
&mut orders,
);
}
match orders.should_render {
ShouldRender::Render => self.schedule_render(),
ShouldRender::ForceRenderNow => {
self.cancel_scheduled_render();
self.rerender_vdom();
}
ShouldRender::Skip => (),
};
orders.effects
}
fn schedule_render(&self) {
let mut scheduled_render_handle = self.data.scheduled_render_handle.borrow_mut();
if scheduled_render_handle.is_none() {
let cb = Closure::new(enclose!((self => s) move |_| {
s.data.scheduled_render_handle.borrow_mut().take();
s.rerender_vdom();
}));
*scheduled_render_handle = Some(util::request_animation_frame(cb));
}
}
fn cancel_scheduled_render(&self) {
self.data.scheduled_render_handle.borrow_mut().take();
}
pub fn mailbox(&self) -> Mailbox<Ms> {
Mailbox::new(enclose!((self => s) move |option_message| {
s.update_with_option(option_message);
}))
}
}