///|
/// The rendering pipeline will do the following step:
///
/// 1. view(model) -> newView
/// compute new vdom from model
///
/// 2. reconcile(oldView, newView)
/// update the mounted render state
struct BrowserHost {
after_render_queue : Queue[() -> Cmd]
task_queue : Queue[(@cmd.Op, @cmd.Extension)]
stores : @slotmap.SlotMap[Store]
scope : @duplix.ScopeId
mut graph_output : @duplix.Node[VNode]?
mut drain_scheduled : Bool
mut paint_scheduled : Bool
mut document : @vdom.VDom?
mut url_request : @cmd.Injector[String]
mut url_changed : @cmd.Injector[String]
}
///|
pub fn BrowserHost::BrowserHost(
element_id? : String,
builder : GraphBuilder,
) -> Self {
let (host, output) = @duplix.with_scope(scope => {
let host = BrowserHost::{
stores: SlotMap(),
scope,
task_queue: Queue([]),
graph_output: None,
document: None,
url_request: _ => @cmd.none,
url_changed: _ => @cmd.none,
paint_scheduled: false,
drain_scheduled: true,
after_render_queue: Queue([]),
}
(host, ambient_host.protect(host, builder))
})
host.graph_output = Some(output)
let url = @dom.window().current_url()
let cmd = Context::inject_url_changed(host, url)
Scheduler::queue_command(host, cmd)
let root = ambient_host.protect(host, () => output.read())
host.document = Some(
@vdom.VDom::initialize(root, host, target_element_id?=element_id),
)
host.drain_scheduled = false
host
}
///|
pub impl Host for BrowserHost with fn flush(self) {
self.request_task()
}
///|
pub impl Host for BrowserHost with fn get_stores(self) {
self.stores
}
///|
/// Schedules a drawing frame to be rendered on the next animation frame.
fn BrowserHost::request_frame(self : Self) -> Unit {
guard !self.paint_scheduled else { return }
self.paint_scheduled = true
@dom.window().request_animation_frame(fn(_) {
if self.document is Some(document) {
let output = self.graph_output.unwrap()
let vnode = ambient_host.protect(self, () => output.read())
document.update(vnode, self)
}
self.paint_scheduled = false
while self.after_render_queue.pop() is Some(f) {
let cmd = f()
Scheduler::queue_command(self, cmd)
}
})
|> ignore
}
///|
fn BrowserHost::request_task(self : Self) -> Unit {
guard !self.drain_scheduled else { return }
self.drain_scheduled = true
@dom.window().queue_microtask(() => {
while self.task_queue.pop() is Some((op, extension)) {
if handle_message(self, extension) {
continue
}
try @cmd.invoke(@key.key, op, extension, self) catch {
Unhandled => ()
} noraise {
None => ()
Ready(cmd) => self.queue_command(cmd)
Async(f) =>
@js_async.Promise::from_async(() => {
Scheduler::queue_command(self, f())
})
|> ignore
AfterLayout(f) => self.after_render_queue.push(f)
LegacyEffect(Immediately, f) => f(self)
LegacyEffect(AfterRender, f) =>
self.after_render_queue.push(() => {
f(self)
@cmd.none
})
}
}
self.drain_scheduled = false
self.request_frame()
})
}
///|
pub impl Host for BrowserHost with fn cleanup(self) {
self.scope.cleanup()
}
///|
pub impl Scheduler for BrowserHost with fn queue_command(self, cmd) {
let tasks = @cmd.flatten(@key.key, cmd)
for task in tasks {
self.task_queue.push(task)
}
self.request_task()
}
///|
pub impl Scheduler for BrowserHost with fn set_url_changed_injector(
self,
injector,
) {
self.url_changed = injector
}
///|
pub impl Scheduler for BrowserHost with fn set_url_request_injector(
self,
injector,
) {
self.url_request = injector
}
///|
pub impl Context for BrowserHost with fn get_origin(_self) {
@dom.window().origin()
}
///|
pub impl Context for BrowserHost with fn inject_url_changed(self, url) {
(self.url_changed)(url)
}
///|
pub impl Context for BrowserHost with fn inject_url_request(self, url) {
(self.url_request)(url)
}