import "pkg:/source/utils/misc.bs"
' The SKELETON-cell convention: a row that exists before its items have landed.
'
' Both cell components render skeletons — `JRRowItem` for Home's rows and `GridItem` for
' the item grid's genre rows — and several views build and guard against them. The whole
' convention lives here: the placeholder a producer appends, the predicate a consumer
' guards with, and the spinner the cells draw. A hand-typed `"Loading"` literal at any
' site is drift waiting to happen — a misspelled construction site would send a bare
' ContentNode into the media render path, where reading absent fields crashes the
' render thread.
namespace skeleton
' The `type` field value that marks a placeholder. Compare via isPlaceholder(),
' construct via createPlaceholder() — never hand-type this string elsewhere.
const PLACEHOLDER_TYPE = "Loading"
' Builds the placeholder child a skeleton row carries so the RowList renders the row
' (a childless row is not laid out at all) at its correct slot size.
function createPlaceholder() as object
placeholder = CreateObject("roSGNode", "ContentNode")
placeholder.addFields({ type: skeleton.PLACEHOLDER_TYPE })
return placeholder
end function
' True when `node` is a skeleton placeholder. Safe on invalid and on nodes with no
' `type` field (a bare ContentNode reads invalid there).
function isPlaceholder(node as dynamic) as boolean
if not isValid(node) then return false
if not isValid(node.type) then return false
return node.type = skeleton.PLACEHOLDER_TYPE
end function
' Diameter of the spinner graphic, in px. Small enough to read as "this cell is
' waiting" rather than as content in its own right.
const SPINNER_SIZE = 48
' Lazy-creates, centers and starts the spinner over `owner`'s poster area.
'
' The node is passed in and handed back rather than kept in an `m` here: these run
' inside a component's scope, where `m` is that component instance. The caller owns
' the reference (one spinner per recycled cell); this only knows how to size, place
' and start it.
'
' Creation work (sizing the graphic, parenting to `owner`) happens ONCE, in the
' create branch — cells are recycled and re-bound often, an appendChild of an
' already-parented child is a detach+reattach scene-graph mutation, and
' `control = "start"` on a running spinner visibly resets the animation. The
' steady-state path only re-places (slot sizes differ across rows) and re-shows.
'
' @param owner - the cell node the spinner is parented to (constant per caller)
' @param spinner - the caller's existing spinner, or invalid on first use
' @param slotWidth - the cell's width
' @param posterHeight - the poster area's height (the spinner centers within it, NOT
' within the full cell, whose height includes the title area below the poster)
' @param posterTop - the poster area's y offset within the cell (0 when the poster
' sits at the cell origin, as in JRRowItem; GridItem offsets it)
' @return the spinner node, for the caller to hold onto across recycles
function showSpinner(owner as object, spinner as dynamic, slotWidth as float, posterHeight as float, posterTop = 0 as float) as object
justCreated = not isValid(spinner)
if justCreated
spinner = CreateObject("roSGNode", "Spinner")
spinner.poster.width = skeleton.SPINNER_SIZE
spinner.poster.height = skeleton.SPINNER_SIZE
owner.appendChild(spinner)
end if
spinner.translation = [
(slotWidth - skeleton.SPINNER_SIZE) / 2,
posterTop + (posterHeight - skeleton.SPINNER_SIZE) / 2
]
if justCreated or not spinner.visible
spinner.visible = true
spinner.control = "start"
end if
return spinner
end function
' Hides and stops `spinner` if the caller ever created one.
'
' Call this on every non-skeleton render, not just when leaving a skeleton: cells are
' RECYCLED, so the cell now rendering a real item may be the same node that showed a
' spinner a moment ago. A spinner left running would keep animating over real content.
sub hideSpinner(spinner as dynamic)
if not isValid(spinner) then return
spinner.visible = false
spinner.control = "stop"
end sub
end namespace