//go:build js && wasm
package wasmruntime
import (
"syscall/js"
"kjol/vdom"
)
// The browser half of the host API (see host.go for the contract and the two
// rules for callers).
var window = js.Global()
// node resolves a Ref to its live DOM handle, or an invalid js.Value if the ref
// is unattached. Every entry point below guards on this, so calling a host
// function against an unmounted ref is a silent no-op rather than a panic — the
// common case during the first render, before the reconciler has committed.
func node(r *vdom.Ref) (js.Value, bool) {
if r == nil {
return js.Value{}, false
}
n, ok := r.Node().(js.Value)
if !ok || !n.Truthy() {
return js.Value{}, false
}
return n, true
}
// ---- measurement ----
// Measure runs getBoundingClientRect on an element. Returns the zero Rect if the
// ref is not mounted — callers must check Rect.Empty() before positioning against
// it (see host.go), or the panel lands at 0,0.
func Measure(r *vdom.Ref) Rect {
n, ok := node(r)
if !ok {
return Rect{}
}
b := n.Call("getBoundingClientRect")
return Rect{
X: b.Get("left").Float(),
Y: b.Get("top").Float(),
Width: b.Get("width").Float(),
Height: b.Get("height").Float(),
}
}
// Viewport is window.innerWidth/innerHeight — the collision boundary for the
// floating engine.
func Viewport() Size {
return Size{
Width: window.Get("innerWidth").Float(),
Height: window.Get("innerHeight").Float(),
}
}
// CSSVarPx reads a CSS custom property off :root and resolves it to pixels,
// handling rem (against the root font size) and px. Returns 0 if unset or
// unparseable. Used for --radius-default, so the tutorial spotlight's corners
// match the app's theme.
func CSSVarPx(name string) float64 {
root := document.Get("documentElement")
style := window.Call("getComputedStyle", root)
raw := style.Call("getPropertyValue", name).String()
return parseCSSLength(raw, style.Get("fontSize").String())
}
// ---- frame timing + the post-render hook ----
func RAF(fn func()) int {
var cb js.Func
cb = js.FuncOf(func(js.Value, []js.Value) any {
cb.Release()
fn()
return nil
})
return window.Call("requestAnimationFrame", cb).Int()
}
func CancelRAF(id int) { window.Call("cancelAnimationFrame", id) }
// afterRender holds callbacks queued for the next render commit. See mount.go,
// which drains it once the DOM is up to date.
var afterRender []func()
// AfterRender runs fn once, right after the current render has been committed to
// the DOM. This is how a component measures something it just rendered: signal
// writes only *schedule* a render, so a ref read in the same tick is still empty.
//
// open.Set(true)
// wasmruntime.AfterRender(func() { reposition() }) // the panel exists by now
//
// If no render is pending (nothing changed), fn still runs — on the next frame —
// so a caller can never be stranded waiting for a commit that will not come.
func AfterRender(fn func()) {
afterRender = append(afterRender, fn)
if !renderScheduled {
// No re-render coming; run on the next frame so the caller's contract
// ("after the DOM is settled") still holds.
RAF(flushAfterRender)
}
}
func flushAfterRender() {
if len(afterRender) == 0 {
return
}
pending := afterRender
afterRender = nil
for _, fn := range pending {
fn()
}
}
// ---- imperative DOM writes (bypassing the vdom on purpose) ----
// SetStyle writes an inline style property directly on the element. Positioning
// goes through here rather than through a signal: a signal write re-renders the
// entire tree, and repositioning happens on every scroll and resize frame.
func SetStyle(r *vdom.Ref, prop, value string) {
if n, ok := node(r); ok {
n.Get("style").Call("setProperty", prop, value)
}
}
func RemoveStyle(r *vdom.Ref, prop string) {
if n, ok := node(r); ok {
n.Get("style").Call("removeProperty", prop)
}
}
// SetText / SetHTML write content imperatively. SetHTML exists for the formula
// editor's syntax-highlight overlay, which is repainted per keystroke and must not
// go through a full re-render.
func SetText(r *vdom.Ref, text string) {
if n, ok := node(r); ok {
n.Set("textContent", text)
}
}
func SetHTML(r *vdom.Ref, html string) {
if n, ok := node(r); ok {
n.Set("innerHTML", html)
}
}
// ---- focus, selection, scrolling ----
func Focus(r *vdom.Ref) {
if n, ok := node(r); ok {
n.Call("focus")
}
}
func Blur(r *vdom.Ref) {
if n, ok := node(r); ok {
n.Call("blur")
}
}
// SelectionRange / SetSelectionRange expose the caret in an /