webcanvas

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

webcanvas

CI Go Reference Go Report Card

Runs a go-widgets scene in a browser tab, on a plain <canvas>: no compositor, no SharedArrayBuffer, no cross-origin isolation, nothing on the far end. It blits an RGBA framebuffer into the canvas and routes DOM pointer and keyboard events back into the scene.

It carries no widget logic of its own. An application implements a small interface and hands it to Run:

//go:build js && wasm

package main

import "github.com/go-widgets/webcanvas"

func main() { webcanvas.Run("screen", myScene()) }

App is Size, Draw(buf []byte), and one method per kind of event, each reporting whether the scene changed so a repaint can be skipped when nothing did. A scene may also implement Ticker, Animator, Resizer or Scroller to be told about time, animation, a resized canvas or the wheel.

Where this came from

It was internal/webcanvas inside go-widgets/gallery, where nothing outside that one module could import it. It is the same code, in a place a second application can reach — which is the point: the DOM plumbing exists once.

Testing

go test -covermode=set ./...

CI gates on exact 100% statement coverage, go vet, a cross-compile across the fleet's targets, and a js/wasm build.

What that coverage does and does not say. The interface, the scene contract and the event dispatch are covered. run_js.go — the DOM loop itself — is behind a js && wasm build tag and so is not part of the measured build at all: it compiles for the browser and is exercised by the applications that use it, not by a test here. A gate that reports 100% while a file is invisible to it is worth saying out loud.

License

BSD-3-Clause — see LICENSE. Copyright the go-widgets/webcanvas authors.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var PanicReporter = func(r any, stack []byte) {
	fmt.Fprintf(os.Stderr, "webcanvas: recovered panic in a handler: %v\n%s\n", r, stack)
}

PanicReporter logs a handler panic the [guard] net recovered. The wasm host swaps in a console.error reporter carrying the JS stack; the default writes to standard error so a native run — and the recover test — still surfaces it. It is a package var, not a const, precisely so run_js.go can replace it.

Functions

This section is empty.

Types

type Animator

type Animator interface {
	// AnimationStep advances the scene's animation by dt seconds of real elapsed
	// time and reports whether the scene now needs a repaint.
	AnimationStep(dt float64) (repaint bool)
}

Animator is an optional companion to App: a scene with time-varying content driven by a REAL wall clock (procedurally animated icons, say) implements it, and [Run] installs a requestAnimationFrame loop that hands it the elapsed dt between frames — in seconds — through AnimationStep. Unlike Ticker (a fixed cadence that always repaints), an Animator advances by the true frame delta and reports whether the frame changed anything, so Run repaints only when a pixel actually moved. A scene that implements neither installs no clock and repaints on input alone. The phase-advance logic lives in the scene (natively testable); only the rAF wiring is browser-side.

type App

type App interface {
	// Size returns the fixed pixel dimensions of the scene's surface. The host
	// sizes the canvas and allocates the framebuffer from it; it is read once,
	// at startup, so it must not change over the App's life.
	Size() (w, h int)

	// Draw paints the whole scene into buf, a width*height*4 RGBA byte slice
	// laid out exactly like an image.RGBA's Pix (row-major, 4 bytes/pixel).
	Draw(buf []byte)

	// Click delivers a primary (left) button press at (x, y). It begins a
	// gesture — a selection, a drag, a placement — that later Move/Release
	// calls advance and commit.
	Click(x, y int) bool

	// Move delivers a pointer move at (x, y). While a Click gesture is in
	// flight it is a drag tick; otherwise it is a hover.
	Move(x, y int) bool

	// Release delivers the primary button release at (x, y), committing any
	// in-flight gesture Click began.
	Release(x, y int) bool

	// Context delivers a secondary (right) button press at (x, y), typically
	// opening a context menu. The host suppresses the browser's own menu.
	Context(x, y int) bool

	// Char delivers a single printable character typed with no Ctrl/Meta/Alt
	// modifier — text input for a focused field.
	Char(s string) bool

	// KeyDown delivers a named key (Enter, Backspace, Delete, Arrow*, …) or a
	// modified key press, routed to the focused widget.
	KeyDown(s string) bool
}

App is a self-contained canvas scene. A host (the wasm [Run] loop, or a native test) owns the pixel buffer and the event source; the App owns the widgets. Every coordinate is in canvas-local pixels (top-left origin), the same space [Run] derives from the pointer event's position within the canvas' bounding rectangle.

Each event method reports whether the scene changed and therefore needs a repaint, so the host can skip a redraw when nothing moved. Draw is expected to fully paint the buffer (it is never given a dirty region).

type Resizer

type Resizer interface {
	// Resize relays out the scene to fit w×h device pixels and returns the pixel
	// size (rw, rh) it will render at — the size the host sizes the canvas and
	// framebuffer to.
	Resize(w, h int) (rw, rh int)
}

Resizer is an optional companion to App: a scene that can adapt its layout to a NEW surface size implements it, and [Run] installs a window "resize" listener (and fits the canvas to the viewport once at startup) that re-sizes the canvas and framebuffer, calls Resize, and repaints. A scene that omits it keeps the fixed App.Size forever — the pre-resize behaviour every existing demo relies on — so Run never installs the listener and the surface never changes.

Resize is handed the target pixel size (the canvas' laid-out client box) and returns the size it will actually render at: a scene may clamp to a sane minimum, and the host allocates the framebuffer from the RETURNED size, so the scene and the buffer can never disagree. The relayout logic lives in the scene (natively testable); only the DOM resize wiring is browser-side.

type Scroller

type Scroller interface {
	// Scroll delivers a wheel / trackpad scroll of dy vertical and dx horizontal
	// ROWS at canvas-local (x, y), and reports whether the scene needs a repaint.
	Scroll(x, y, dx, dy int) (repaint bool)
}

Scroller is an optional companion to App: a scene with a scrollable region (a docked list, an icon palette, an overflowing panel) implements it, and [Run] installs a "wheel" listener that translates the browser's WheelEvent into toolkit scroll ROWS and routes them — with the canvas-local pointer position, so the scene can hit-test which region the wheel is over — through Scroll. A scene that omits it installs no wheel listener, so the page keeps its default wheel behaviour and every existing demo (the widget gallery) is unchanged.

dx / dy are the horizontal / vertical scroll amounts in toolkit ROWS (already normalised from the event's deltaMode by [scrollRows]): positive dy scrolls down / forward, positive dx scrolls right. A scene typically forwards them to the widget under (x, y) as a [toolkit.Event] of kind EventScroll (Delta = dy, DeltaX = dx), which scrollable widgets clamp at both ends. Scroll reports whether the scene changed and therefore needs a repaint.

type Ticker

type Ticker interface {
	// Tick advances one animation frame. Run repaints after every Tick.
	Tick()
}

Ticker is an optional companion to App: a scene that needs a steady animation clock (a toast countdown, a blinking caret) implements it, and [Run] installs a 60-Hz timer that calls Tick and repaints. A scene with no time-varying state omits it, and Run installs no timer, so it never repaints except in response to input.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL