///|
let need_to_rerender : Ref[Bool] = Ref(false)
///|
/// check where need to trigger rerendering, also resets the status to false
fn drain_rerender_status() -> Bool {
let ret = need_to_rerender.val
// @dom_ffi.log("draining: " + ret.to_string())
if ret {
// @dom_ffi.warn_log("rerender")
need_to_rerender.val = false
}
ret
}
///|
/// Signal that the store has changed and a re-render should be scheduled.
///
/// Call this after assigning `app.store.val = new_store` to trigger the next
/// animation-frame render cycle.
pub fn mark_need_rerender() -> Unit {
let ret = need_to_rerender.val
if !ret {
need_to_rerender.val = true
// @dom_ffi.warn_log("mark need rerender")
}
}
///|
/// Mount and start the render loop for a Respo application.
///
/// ## Immutability contract
///
/// `store` **must** hold an immutable value type (no `mut` struct fields).
/// The render loop uses two optimizations to avoid unnecessary work:
///
/// 1. **Physical equality** (`physical_equal`): if `store.val` is the exact
/// same heap object as in the previous frame, the render is skipped entirely.
/// 2. **Logical equality** (`==`): if `store.val != prev_store` but they compare
/// equal, the render is also skipped.
///
/// ⚠️ **`mut` field pitfall**: if your `Store` struct has `mut` fields and
/// `update()` mutates them in place, then `{ ..store.val }` only creates a
/// shallow copy — but `prev_store` already sees the mutated values through the
/// shared struct reference. Both checks then return `true` and the DOM is never
/// updated.
///
/// ✅ **Correct pattern**: `update()` must return a brand-new `Store` value.
///
/// ```moonbit nocheck
/// // WRONG — mut field + in-place mutation
/// struct Store { mut count : Int } derive(Eq)
/// fn Store::update(self : Store, op : Op) -> Op? {
/// self.count += 1 // mutates in place — prev_store sees this too!
/// None
/// }
///
/// // CORRECT — no mut, update returns new store
/// struct Store { count : Int } derive(Eq)
/// fn Store::update(self : Store, op : Op) -> (Store, Op?) {
/// ({ ..self, count: self.count + 1 }, None)
/// }
/// // caller:
/// let (new_store, maybe_op) = app.store.val.update(op)
/// app.store.val = new_store
/// ```
pub fn[T, U : Eq, G] render_node(
mount_target : @dom_ffi.Node,
// TODO it copies the whole store, need to optimize
store : Ref[U],
renderer : () -> @node.RespoNode[T, G] raise @node.RespoCommonError,
dispatch_action : (T) -> Unit raise @node.RespoCommonError,
_interval : Float?,
) -> Unit raise @node.RespoCommonError {
let prev_store = @ref.new(store.val)
let tree0 : @node.RespoNode[T, G] = renderer()
let prev_tree = @ref.new(tree0)
let handle_event = fn(
mark : @node.RespoEventMark,
) -> Unit raise @node.RespoCommonError {
// @dom_ffi.warn_log(
// "start rendering: " + mark.name.to_string() + " " + mark.coord.to_string(),
// )
let handler = request_for_target_handler(
prev_tree.val,
mark.name,
mark.coord,
)
handler(mark.event_info, dispatch_action)
// @dom_ffi.log(
// "finished event: " + mark.name.to_string() + " " + mark.coord.to_string(),
// )
mark_need_rerender()
}
let element = @node.build_dom_tree(tree0, @immut/vector.new(), handle_event)
// collection mounted effects
let mount_changes : Ref[Array[@node.DomChange[T, G]]] = Ref([])
@node.collect_effects_outside_in_as(
tree0,
@immut/vector.new(),
@immut/vector.new(),
Mounted,
mount_changes,
)
mount_target.append_child(element)
// println("mounted changed: \{mount_changes.val}")
@node.patch_tree(
tree0,
prev_tree.val,
mount_target,
mount_changes.val,
handle_event,
)
@node.raf_loop(fn(_t : Float) -> Unit raise @node.RespoCommonError {
if drain_rerender_status() {
// Skip re-render if store hasn't changed (immutable data optimization).
// physical_equal is O(1); == may walk the whole structure.
// Both checks are only reliable when Store has no `mut` fields — see
// render_node doc comment for the full explanation.
if physical_equal(store.val, prev_store.val) {
debug_render("skipped - store reference unchanged")
return
}
// Skip re-render if store changed but logically equal.
// If this fires unexpectedly after a state change, check that your Store
// has no `mut` fields and that update() returns a new value.
if store.val == prev_store.val {
debug_render("skipped - store logically equal")
return
}
prev_store.val = store.val
debug_render("starting render cycle")
let new_tree = renderer()
let changes : Ref[Array[@node.DomChange[T, G]]] = Ref([])
@node.diff_tree(
new_tree,
prev_tree.val,
@immut/vector.new(),
@immut/vector.new(),
changes,
)
debug_render("\{changes.val.length()} DOM changes")
@node.patch_tree(
new_tree,
prev_tree.val,
mount_target,
changes.val,
handle_event,
)
prev_tree.val = new_tree
}
})
}
///|
fn[T, G] request_for_target_handler(
tree : @node.RespoNode[T, G],
event_name : @node.RespoEventType,
coord : @immut/vector.Vector[@node.RespoCoord],
) -> ((@node.RespoEvent, @node.DispatchFn[T]) -> Unit raise @node.RespoCommonError) raise @node.RespoCommonError {
let target_node = @node.load_coord_target_tree(tree, coord.to_array())
match target_node {
Component({ name, .. }) =>
raise @node.RespoCommonError(
"expected element, found target being a component: " + name,
)
Element({ name: tag_name, event, .. }) =>
match event.get(event_name) {
Some(v) => v
None =>
raise @node.RespoCommonError(
"No event handler found for event '\{event_name}' on element '\{tag_name}', available events: TODO{event.keys()}",
)
}
}
}