package wasmruntime import ( "strconv" "strings" ) // The host API: the channel through which the browser tells WebAssembly things Go // cannot work out on its own — how big an element is, where the pointer is, how // tall the viewport is — plus the imperative escape hatches (write a style, focus // an input, download a file) that a pure re-render cannot express. // // Every function here exists in two builds: the real one (host_wasm.go, js+wasm) // and a no-op that returns zero values (host_native.go). That is what lets the // neutral kjol/webui components — which must also compile on the server for SSR — // call them unconditionally. On the server a measurement is simply the zero Rect, // a listener is never installed, and a timer never fires; components render their // pre-measurement state (a panel with `visibility: hidden`), which is exactly what // SSR should ship. // // Two rules for callers: // // 1. Measure only after the DOM exists. A ref is empty until the reconciler has // committed; use AfterRender to run measurement code once the current render // is on screen. // 2. Do NOT position through signals. Signal.Set re-renders the whole tree; doing // that on every scroll frame is pathological. Write positions with SetStyle, // which mutates the DOM node directly and leaves the vdom alone. // Rect is an element's box in viewport coordinates — the result of // getBoundingClientRect. Viewport coordinates compose directly with // `position: fixed`, so no scroll compensation is needed (or wanted) anywhere. type Rect struct{ X, Y, Width, Height float64 } func (r Rect) Top() float64 { return r.Y } func (r Rect) Left() float64 { return r.X } func (r Rect) Right() float64 { return r.X + r.Width } func (r Rect) Bottom() float64 { return r.Y + r.Height } func (r Rect) CenterX() float64 { return r.X + r.Width/2 } func (r Rect) CenterY() float64 { return r.Y + r.Height/2 } // Empty reports whether the rect carries no useful geometry — an unmounted ref, or // an element that has not been laid out yet. Positioning code must treat this as // "cannot measure yet" rather than as a box at the origin, or the panel paints at // 0,0 for a frame. func (r Rect) Empty() bool { return r.Width == 0 && r.Height == 0 } // Size is a width/height pair — the viewport, or an element's size. type Size struct{ Width, Height float64 } // Unsub removes a listener installed by OnWindow / OnDocument / ObserveResize. // Always call it when the thing that installed it goes away; a floating panel that // leaks a window scroll listener per open will crawl. type Unsub func() // ScrollBlock values for ScrollIntoView. const ( ScrollBlockStart = "start" ScrollBlockCenter = "center" ScrollBlockEnd = "end" ScrollBlockNearest = "nearest" ) // parseCSSLength resolves a CSS length to pixels. rootFontSize is the computed // font-size of :root (itself a px string), which is what a rem is measured // against. Unitless and unparseable values yield 0. func parseCSSLength(value, rootFontSize string) float64 { value = strings.TrimSpace(value) if value == "" { return 0 } switch { case strings.HasSuffix(value, "px"): return parseFloat(strings.TrimSuffix(value, "px")) case strings.HasSuffix(value, "rem"): root := parseFloat(strings.TrimSuffix(strings.TrimSpace(rootFontSize), "px")) if root == 0 { root = 16 // the browser default, if :root's font-size is unreadable } return parseFloat(strings.TrimSuffix(value, "rem")) * root default: return parseFloat(value) } } func parseFloat(s string) float64 { f, err := strconv.ParseFloat(strings.TrimSpace(s), 64) if err != nil { return 0 } return f }