painter

package module
v0.12.0 Latest Latest
Warning

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

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

README

go-widgets/painter

CI pages pkg.go.dev coverage go status license

▶ Live demo: https://go-widgets.github.io/painter/

Prototype of a Painter abstraction that lets a single widget render into three deployment families with the same code:

  • WUI — browser wasm + <canvas> + putImageData (via PixelPainter)
  • GUI — native window (SDL / Ebitengine / image files) (via PixelPainter)
  • TUI — terminal cell grid + 24-bit ANSI (via CellPainter)

Both PixelPainter and CellPainter implement the same 5-primitive Painter interface — widgets never see the back-end.

Why this exists

Today the go-widgets/toolkit widget's Draw takes a ([]byte, w int, h int) — a hard binding to a pixel back-end. Great for the browser and native canvases; unworkable for a terminal grid, where the atom is a cell (rune + fg + bg), not a pixel.

This repo prototypes the redesign so we can answer:

Can we write the same widget once and render it to WUI, GUI, and TUI without conditional-compilation gymnastics?

The 5-primitive interface says yes:

type Painter interface {
    FillRect(r Rect, c RGBA)
    StrokeRect(r Rect, c RGBA, lineW int)
    PutPixel(x, y int, c RGBA)
    Text(x, y int, s string, ink RGBA)
    Size() (w, h int)
}

type Widget interface {
    Draw(p Painter, theme *Theme)
}

A widget's Draw composes only those five calls. The back-end decides whether they land as pixels or cells.

Try it

# WUI/GUI proxy — renders to a PNG the same way a browser canvas
# would consume the RGBA buffer.
go run ./cmd/wui-demo --out demo.png            # light theme
go run ./cmd/wui-demo --out demo.png --theme dark

# TUI — writes 24-bit-ANSI to stdout; point your terminal at it.
go run ./cmd/tui-demo
go run ./cmd/tui-demo --theme dark

# WUI live in a browser — Chromium / Firefox / Safari:
task serve                                      # http://localhost:8091/

All three render the exact same three widgets (Label, two Buttons, ProgressBar) through the exact same widget code in widget.go. The only thing that changes between them is which Painter implementation the widget's Draw sees.

What's in the box

File Contents
painter.go Painter interface, Rect, RGBA, RGB helper
pixel.go PixelPainter — writes into an RGBA []byte buffer
cell.go CellPainter — writes into a []Cell grid + 24-bit-ANSI serializer
font.go Minimal 5×7 bitmap font (uppercase + digits + punct.)
theme.go Theme struct + LightTheme / DarkTheme palettes
widget.go Sample widgets: Button, Label, ProgressBar
cmd/wui-demo Renders widgets to a PNG (WUI / GUI back-end proxy)
cmd/wui-wasm Renders widgets to a browser <canvas> (WUI live demo)
cmd/tui-demo Renders widgets to stdout as 24-bit ANSI (TUI)

Status

Prototype — design validation. ~5-primitive API, 3 widgets, 100 % coverage on library packages, CGO_ENABLED=0, builds on all 6 supported 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) + GOOS=js GOARCH=wasm.

The API is deliberately small. The next step, if the design proves out, is a wholesale migration of go-widgets/toolkit's widget set to this interface + folding this repo's implementation into the toolkit as its v1.0 rendering path.

Non-goals for the prototype

  • Not a full toolkit — 3 widgets only.
  • No event dispatch — HitTest / OnEvent are out of scope. They come back once the render side is validated.
  • No full font — 5×7 bitmap covers uppercase + digits + a handful of punctuation. A production merge reuses go-widgets/toolkit's full font table.
  • No terminal host loop — cmd/tui-demo just writes ANSI to stdout; it doesn't put the terminal into raw mode or dispatch input.

License

BSD 3-Clause. See LICENSE.

Documentation

Overview

Package painter is a prototype of the "Painter" abstraction that lets a single widget's Draw method target three deployment families:

  • WUI (browser wasm + <canvas> + putImageData) → PixelPainter
  • GUI (native window; SDL2, Ebitengine, image/png export…) → PixelPainter
  • TUI (terminal cell grid + ANSI escape codes) → CellPainter

The current go-widgets/toolkit widgets are hard-bound to a []byte + surfaceW pair — great for pixel back-ends, incompatible with a cell grid. This repo prototypes a redesign around a primitive-set interface:

type Painter interface {
    FillRect(r Rect, c RGBA)
    StrokeRect(r Rect, c RGBA, lineW int)
    Text(x, y int, s string, ink RGBA)
    PutPixel(x, y int, c RGBA)
}

A widget's Draw becomes:

func (b *Button) Draw(p Painter, theme *Theme) {
    r := b.Bounds
    p.FillRect(r, theme.Surface)
    p.StrokeRect(r, theme.Border, 1)
    p.Text(r.X+8, r.Y+8, b.Label, theme.OnSurface)
}

The same widget code renders identically in a browser canvas (PixelPainter), a native window (PixelPainter again — the host consumes the buffer differently), a terminal (CellPainter maps RGBA to ANSI 16-colour, snaps rects to cells + uses box-draw glyphs for strokes), or an SVG snapshot (not shipped in this prototype — see go-widgets/svg).

Status: PROTOTYPE. The API surface is deliberately small (5 primitives). Once validated the full toolkit widget set migrates + the prototype folds into go-widgets/toolkit as its v1.0 rendering path.

Index

Constants

View Source
const (
	// NonZero fills a point when the signed edge-crossing count around it is
	// non-zero — the intuitive rule for most icon shapes.
	NonZero = vector.NonZero
	// EvenOdd fills a point when the crossing count is odd, so a shape drawn over
	// itself punches a hole.
	EvenOdd = vector.EvenOdd
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Button

type Button struct {
	Bounds  Rect
	Label   string
	Pressed bool
}

Button is the prototype's canonical widget — solid fill + border + centred label. Every real widget in the toolkit follows the same four-line pattern.

func (*Button) Draw

func (b *Button) Draw(p Painter, theme *Theme)

Draw paints the button. A pressed button swaps Surface + Accent to visualise the state.

type Cell

type Cell struct {
	Rune rune
	Fg   RGBA
	Bg   RGBA
}

Cell is one terminal cell — a rune plus a foreground and background colour. Painters serialize cell grids into ANSI escape sequences at render time.

type CellPainter

type CellPainter struct {
	W     int
	H     int
	Cells []Cell
	// contains filtered or unexported fields
}

CellPainter maps the primitive set onto a fixed-size cell grid. Coordinates are in cells; a rectangle of size (10, 3) is 10 cells wide and 3 cells tall regardless of the terminal's font.

Colour: written to a 24-bit-ANSI-truecolor terminal at flush time. The primitive set carries full RGBA; the terminal handles quantiz- ation. This keeps the widget code identical between pixel + cell back-ends.

func NewCellPainter

func NewCellPainter(w, h int) *CellPainter

NewCellPainter builds a fresh painter over an allocated grid. The grid is initialized to space + black on black — the widget draws its own background.

func (*CellPainter) DrawImage added in v0.5.0

func (p *CellPainter) DrawImage(dst Rect, src []byte, srcW, srcH int)

DrawImage maps the image onto the cell grid: each cell takes the colour of the source pixel it lands on, as a full-block glyph — which is what CellPainter.PutPixel already means by a pixel. Implements ImagePainter.

A terminal cannot show an image, so this is the same honest degradation PutPixel already makes — a coloured cell rather than nothing at all.

func (*CellPainter) DrawMask added in v0.9.0

func (p *CellPainter) DrawMask(dst Rect, mask []byte, stride int, ink RGBA)

DrawMask puts a coloured cell wherever the mask is more than half covered: a cell is either the glyph's colour or it is not, so the coverage has to resolve to a decision. Implements MaskPainter.

func (*CellPainter) FillRect

func (p *CellPainter) FillRect(r Rect, c RGBA)

FillRect paints a solid block of cells. The rune stays ' '; only the background colour is set. StrokeRect overlays box characters on top.

func (*CellPainter) FillRoundRect added in v0.1.1

func (p *CellPainter) FillRoundRect(r Rect, radius int, c RGBA)

FillRoundRect can't round on a cell grid (a cell is atomic), so it falls back to a square FillRect -- the rounding is a no-op here.

func (*CellPainter) PopClip added in v0.1.3

func (p *CellPainter) PopClip()

PopClip removes the most recent PushClip. Implements Clipper.

func (*CellPainter) PopTranslate added in v0.4.0

func (p *CellPainter) PopTranslate()

PopTranslate removes the most recent PushTranslate. Implements Translator.

func (*CellPainter) PushClip added in v0.1.3

func (p *CellPainter) PushClip(r Rect)

PushClip confines subsequent writes to r (intersected with any enclosing clip). Implements Clipper.

func (*CellPainter) PushTranslate added in v0.4.0

func (p *CellPainter) PushTranslate(dx, dy int)

PushTranslate shifts subsequent drawing by dx,dy, on top of any enclosing translation. Implements Translator.

func (*CellPainter) PutPixel

func (p *CellPainter) PutPixel(x, y int, c RGBA)

PutPixel paints a single cell as a filled block character. Useful for pixel-precise widgets that want to render "dots" on a terminal.

func (*CellPainter) Size

func (p *CellPainter) Size() (int, int)

Size returns width × height in cells.

func (*CellPainter) StrokeRect

func (p *CellPainter) StrokeRect(r Rect, c RGBA, lineW int)

StrokeRect draws a 1-cell-wide box using Unicode box-draw runes. lineW is ignored — a terminal cell is atomic.

func (*CellPainter) StrokeRoundRect added in v0.1.1

func (p *CellPainter) StrokeRoundRect(r Rect, radius int, c RGBA, lineW int)

StrokeRoundRect falls back to the square box-draw StrokeRect on a cell grid.

func (*CellPainter) Text

func (p *CellPainter) Text(x, y int, s string, ink RGBA)

Text writes s as-is starting at (x, y) — one rune per cell. UTF-8 wide characters are not policed at this prototype stage; a produc- tion CellPainter would use golang.org/x/text/width.

func (*CellPainter) WriteANSI

func (p *CellPainter) WriteANSI(w io.Writer) (int, error)

WriteANSI serializes the grid as a single ANSI-encoded string (24-bit truecolor). Each row is prefixed with `\x1b[<y+1>;1H` so it starts at column 1 of its own row regardless of terminal wrap / raw-mode CR handling — the previous `\n`-only terminator broke in raw-mode Terminal.app (ONLCR off): LF alone doesn't reset column, so rows 1..N would be written to wrapped positions and the frame would end up blank except for the last row.

type Clipper added in v0.1.3

type Clipper interface {
	PushClip(r Rect)
	PopClip()
}

Clipper is an optional Painter capability: while a clip rect is pushed, every drawing primitive is confined to it (intersected with any enclosing clip). It is the seam a scrollable/overflowing widget uses to keep a child inside its own bounds — the base Painter interface deliberately cannot draw-clip, so widgets that need it type-assert:

if c, ok := p.(painter.Clipper); ok {
	c.PushClip(bounds)
	defer c.PopClip()
}
child.Draw(p, theme)

Both PixelPainter and CellPainter implement Clipper; a back-end that cannot clip simply does not, and the assertion is skipped.

type Face added in v0.2.0

type Face interface {
	// FontData returns the original TrueType/OpenType sfnt bytes of the face.
	// A consumer embeds or subsets these to render real, selectable text. The
	// slice is read-only; callers must not mutate it.
	FontData() []byte

	// SizePx is the face's em size in painter units (pixels) — the size the
	// widget laid its text out at, which the consumer reproduces so the run
	// occupies the same box.
	SizePx() int

	// Ascent is the baseline offset from the text's top edge, in painter units.
	// The Text/TextFace convention places (x, y) at the run's TOP-LEFT corner,
	// so a baseline-origin back-end (PDF, PostScript) drops the pen by Ascent.
	Ascent() int
}

Face is a resolved font face a run of text is drawn with. The base Text primitive carries only a string + ink, so a non-pixel painter (a vector or recording back-end) renders it in the painter's OWN fallback font. When the text is actually set in a TrueType/OpenType face — a widget running under go-widgets/toolkit's NewTrueTypeFont, say — that fallback loses both the real glyph shapes and the real advances, so the run mis-sizes.

Face is the bridge: it exposes just enough of the face for a vector back-end to embed the true font and place the text at the true size, WITHOUT the painter package taking on a font-engine dependency (it stays stdlib-only). The font's own layout (per-glyph advances, kerning) is reproduced by the consumer re-reading these same sfnt bytes, so the vector text lines up with the on-screen raster the widget laid itself out against.

A Face is handed to a FacePainter (see below); a painter that cannot use one never sees it.

type FacePainter added in v0.2.0

type FacePainter interface {
	// TextFace draws s in face with (x, y) the run's top-left corner and ink the
	// fill colour. Coordinates are painter units (pixels). Text laid out in
	// visual order is passed as-is; the back-end maps each rune through the
	// face's own cmap.
	TextFace(x, y int, s string, face Face, ink RGBA)
}

FacePainter is an optional Painter capability: a back-end that can render real text in a specific font face implements it. It is the shaped-text seam a proportional/TrueType font uses so a vector or recording painter emits genuine selectable text (a PDF text-show operator, an SVG <text>, …) in the true face rather than the painter's built-in fallback font.

A font that owns a Face type-asserts, exactly like Clipper:

if fp, ok := p.(painter.FacePainter); ok {
	fp.TextFace(x, y, s, face, ink)
	return
}
p.Text(x, y, s, ink) // fallback: painter's own font

PixelPainter deliberately does NOT implement FacePainter: a raster font scan-converts its own glyph coverage and blits through PutPixel, so it never needs the seam. Only non-pixel back-ends (which cannot rasterise glyph masks) consume it.

type FillRule added in v0.3.0

type FillRule = vector.FillRule

FillRule selects how a path's winding count decides which side of the outline is filled: NonZero (non-zero winding) or EvenOdd. It is an alias of vector.FillRule.

type ImagePainter added in v0.5.0

type ImagePainter interface {
	DrawImage(dst Rect, src []byte, srcW, srcH int)
}

ImagePainter is an optional Painter capability: it puts a block of RGBA pixels on the surface in one call.

The base interface can draw rectangles, rounded rectangles, text and single pixels — and nothing that carries pixels of its own. Every widget showing an image therefore had to spell it out one pixel at a time: the toolkit's Image, Thumbnail, Wallpaper, Browser, ColorPicker and both font paths all loop over the destination calling PutPixel, which is an interface call per pixel — about 700,000 of them for a full 1000x700 window. Applications with their own framebuffer went further and bypassed the painter completely, reaching for the raw buffer, which is exactly what stops them being hosted by a back-end that hands out a Painter and nothing else.

DrawImage scales src (srcW x srcH, 4 bytes per pixel, RGBA) into dst by nearest-neighbour sampling — the same mapping the hand-written loops used, so output is unchanged — and honours the active clip and translation like every other primitive.

A src that is too short for srcW*srcH*4 is ignored rather than read past its end: the caller has a bug, and a painter is the wrong place to panic.

type Label

type Label struct {
	Bounds Rect
	Text   string
}

Label is a static text widget. No fill, no border, no state.

func (*Label) Draw

func (l *Label) Draw(p Painter, theme *Theme)

Draw paints the label ink on the surface's own background — the host is expected to have filled the parent Bounds first.

type MaskPainter added in v0.9.0

type MaskPainter interface {
	DrawMask(dst Rect, mask []byte, stride int, ink RGBA)
}

MaskPainter is an optional Painter capability: it paints ONE colour through an 8-bit coverage mask in a single call.

This is what a glyph is. A rasterised glyph is not an image — it carries no colour of its own, only how much of each pixel the outline covers — and text is the one thing every widget draws. Before this, a font back-end had to walk the mask calling PutPixel, which shifts, bounds-tests, clip-tests and blends one pixel at a time, and had to scale the coverage by the ink's alpha itself on every pixel.

mask holds one coverage byte per pixel, row-major with the given stride, and mask[0] is the pixel at dst's top-left. 0 leaves the surface untouched, 255 lays down ink at its own alpha, and everything between composites — which is exactly what makes a glyph edge smooth rather than jagged.

ImagePainter carries pixels that bring their own colour; this carries coverage for a colour the caller names. A back-end may implement either, both, or neither.

type Painter

type Painter interface {
	// FillRect paints a solid rectangle.
	FillRect(r Rect, c RGBA)

	// StrokeRect paints a 1-line-wide border around r (no fill).
	// lineW is a hint; back-ends that can't do variable strokes
	// (a cell grid, for instance) ignore it.
	StrokeRect(r Rect, c RGBA, lineW int)

	// FillRoundRect fills r with the corners rounded to the given
	// radius (in painter units). radius is clamped to half the
	// smaller side. Pixel back-ends anti-alias the corners; back-ends
	// that can't round (a cell grid) fall back to a square FillRect.
	FillRoundRect(r Rect, radius int, c RGBA)

	// StrokeRoundRect paints a 1-unit rounded border around r. Like
	// StrokeRect, lineW is a hint; non-rounding back-ends fall back to
	// a square StrokeRect.
	StrokeRoundRect(r Rect, radius int, c RGBA, lineW int)

	// PutPixel paints a single pixel at (x, y). On a CellPainter
	// this promotes to a filled cell.
	PutPixel(x, y int, c RGBA)

	// Text paints ink text starting at (x, y). Font metrics come
	// from the painter's own bitmap (PixelPainter's 5×7 font) or
	// the terminal's own font (CellPainter — 1 cell per rune).
	Text(x, y int, s string, ink RGBA)

	// Size returns the painter's canvas dimensions in painter units
	// (pixels for PixelPainter, cells for CellPainter). A widget
	// that wants to fill the whole surface reads this instead of
	// hard-coding a size.
	Size() (w, h int)
}

Painter is the primitive-set every back-end implements. A widget composes only these calls; the back-end decides how they land on the actual output.

Coordinates are in the painter's OWN unit — pixel for PixelPainter, cell for CellPainter. The widget doesn't need to know which; the host sets the widget's Bounds in the right units before Draw is called.

type Path added in v0.3.0

type Path = vector.Path

Path is a mutable 2-D outline built from move / line / quadratic / cubic / close commands. It is an alias of vector.Path, so the builder methods and any path a consumer holds are unchanged by the extraction.

func NewPath added in v0.3.0

func NewPath() *Path

NewPath returns an empty Path ready to build.

type PathPainter added in v0.3.0

type PathPainter interface {
	// FillPath fills pth with colour c under the given winding rule. Curves are
	// flattened; corner and edge pixels get fractional coverage (anti-aliased),
	// composited through the painter's own pixel write so the active clip is
	// honoured. An empty path, or one enclosing no area, paints nothing.
	FillPath(pth *Path, c RGBA, rule FillRule)

	// StrokePath paints pth's outline with colour c, centred on the path and
	// width units wide, with round joins and caps. A closed sub-path strokes its
	// closing segment too. width <= 0, a nil/empty path, or an isolated point
	// paint nothing.
	StrokePath(pth *Path, c RGBA, width float64)
}

PathPainter is an optional Painter capability: a back-end that can rasterise arbitrary 2-D outlines implements it. Like Clipper and FacePainter it is type-asserted, so the base Painter interface stays a fixed-primitive set and a back-end that cannot rasterise paths (a cell grid) simply does not implement it:

if pp, ok := p.(painter.PathPainter); ok {
	pp.FillPath(icon, ink, painter.NonZero)
} else {
	// coarse fallback in the base primitives, or skip the vector part
}

PixelPainter implements PathPainter with the go-gfx/gfx/vector anti-aliased scanline coverage rasterizer, compositing the coverage through its own pixel write so the active clip is honoured. CellPainter and any FacePainter-only back-end deliberately do NOT: a terminal cell is atomic and cannot carry sub-cell vector coverage, so the capability is reported absent and the consumer falls back.

type PixelPainter

type PixelPainter struct {
	// Buf is the destination RGBA byte slice (4 bytes per pixel).
	// The buffer is written in place; callers own its lifecycle.
	Buf []byte

	// Width is the stride in pixels — number of pixels per row.
	// The buffer's actual byte-stride is Width*4.
	Width int

	// Height is the number of rows.
	Height int
	// contains filtered or unexported fields
}

PixelPainter writes the primitive set into an RGBA byte buffer — the deployment target for the WUI (browser canvas + putImageData) and GUI (native window that consumes a []byte) families. The buffer + stride mirror the toolkit's current Draw signature; a widget migrated to Painter renders identically to today's toolkit output.

func NewPixelPainter

func NewPixelPainter(buf []byte, width, height int) *PixelPainter

NewPixelPainter builds a fresh painter over an already-allocated buffer. The buffer must be exactly `4*width*height` bytes; a mismatch is not policed here (the primitive calls just no-op on out-of-bounds writes).

func NewPixelPainterBGRA added in v0.12.0

func NewPixelPainterBGRA(buf []byte, width, height int) *PixelPainter

NewPixelPainterBGRA is NewPixelPainter for a buffer whose pixels are BLUE, green, red, alpha -- the order a screen capture, a video frame and several native surfaces arrive in.

It exists because the alternative is worse in every direction. A consumer drawing widgets over captured pixels can swap the CAPTURE, which is the largest thing in the frame and the one part that must not be copied twice; or it can hand every colour in the theme over pre-swapped, which is dozens of values and one forgotten one away from a wrong colour nobody traces back. Swapping here costs two byte stores per pixel WRITTEN, and a widget writes a small part of a frame.

Measured in go-xrkit/desk: its canvas holds BGRA because ScreenCaptureKit hands over BGRA and the frame is swapped once on the way to the window. Every overlay the toolkit drew into it -- the screen number, the gallery marks, the application tiles -- came out with red and blue exchanged, so the orange selection ring was blue on the glasses.

func (*PixelPainter) DrawImage added in v0.5.0

func (p *PixelPainter) DrawImage(dst Rect, src []byte, srcW, srcH int)

DrawImage blits src into dst. Implements ImagePainter.

The fast path is one row at a time. When the destination is the same width as the source and the row is fully opaque and unclipped, the row is copied wholesale; otherwise each pixel is composited through the same blend the rest of the painter uses, so translucent images look identical to the per-PutPixel version that came before.

func (*PixelPainter) DrawMask added in v0.9.0

func (p *PixelPainter) DrawMask(dst Rect, mask []byte, stride int, ink RGBA)

DrawMask paints ink through mask. Implements MaskPainter.

Where it may write is decided once, like every other primitive here, so the per-pixel work is a coverage lookup and a blend and nothing else.

func (*PixelPainter) FillPath added in v0.3.0

func (p *PixelPainter) FillPath(pth *Path, c RGBA, rule FillRule)

FillPath fills pth with c under rule. See PathPainter.FillPath.

func (*PixelPainter) FillRect

func (p *PixelPainter) FillRect(r Rect, c RGBA)

FillRect fills r with c. Out-of-bounds bytes are dropped so a widget that ranges past the edge doesn't panic.

This is the primitive the toolkit leans on hardest -- every background, every button, every table row -- and it used to be a PutPixel per pixel, which is a shift, two bounds tests, a clip test and a blend each: 700,000 of them for a window-sized fill. Where a fill may write is a rectangle, decided once; and an opaque fill writes the SAME four bytes everywhere, so one row is built and the rest of the rectangle is that row copied. A translucent fill still composites pixel by pixel, because its result depends on what was underneath.

func (*PixelPainter) FillRoundRect added in v0.1.1

func (p *PixelPainter) FillRoundRect(r Rect, radius int, c RGBA)

FillRoundRect fills r with corners rounded to radius (in pixels), anti- aliasing the corner edge. radius is clamped to half the smaller side; a radius <= 0 degrades to a plain FillRect. Corner-edge pixels are plotted with fractional alpha, which PutPixel composites onto the destination -- so a rounded button/pill reads as a smooth macOS-style shape rather than a jagged one.

func (*PixelPainter) PopClip added in v0.1.3

func (p *PixelPainter) PopClip()

PopClip removes the most recent PushClip. Implements Clipper.

func (*PixelPainter) PopTranslate added in v0.4.0

func (p *PixelPainter) PopTranslate()

PopTranslate removes the most recent PushTranslate. Implements Translator.

func (*PixelPainter) PushClip added in v0.1.3

func (p *PixelPainter) PushClip(r Rect)

PushClip confines subsequent drawing to r (intersected with any enclosing clip). Implements Clipper.

func (*PixelPainter) PushTranslate added in v0.4.0

func (p *PixelPainter) PushTranslate(dx, dy int)

PushTranslate shifts subsequent drawing by dx,dy, on top of any enclosing translation. Implements Translator.

func (*PixelPainter) PutPixel

func (p *PixelPainter) PutPixel(x, y int, c RGBA)

PutPixel writes one RGBA at (x, y). Out-of-bounds writes are silently dropped.

Semi-transparent colours are src-over composited onto the existing pixel, so a theme colour like WhiteSur's borders rgba(0,0,0,0.12) paints as a subtle 12%-black hairline instead of a harsh opaque line. The two common cases stay exact and allocation-free:

  • A == 0xFF (the vast majority of widget paint) overwrites verbatim, so opaque rendering is byte-identical to before.
  • A == 0 (fully transparent) is a no-op.

Compositing over an opaque destination yields an opaque result, so a surface stays fully opaque for the host compositor.

func (*PixelPainter) Size

func (p *PixelPainter) Size() (int, int)

Size returns Width × Height in pixels.

func (*PixelPainter) StrokePath added in v0.3.0

func (p *PixelPainter) StrokePath(pth *Path, c RGBA, width float64)

StrokePath paints pth's outline with c, width units wide. See PathPainter.StrokePath. The stroke coverage (the union of a rectangle per segment and a round join/cap disk at every vertex) is computed by the vector rasterizer and composited once.

func (*PixelPainter) StrokeRect

func (p *PixelPainter) StrokeRect(r Rect, c RGBA, lineW int)

StrokeRect draws a 1-line-wide border around r. lineW is currently ignored — the pixel back-end can't easily draw thick strokes without antialiasing, which is out of scope for this prototype.

func (*PixelPainter) StrokeRoundRect added in v0.1.1

func (p *PixelPainter) StrokeRoundRect(r Rect, radius int, c RGBA, lineW int)

StrokeRoundRect paints a 1-pixel rounded border around r. The straight runs are crisp 1-px lines; the four corners are an anti-aliased quarter-ring.

func (*PixelPainter) Text

func (p *PixelPainter) Text(x, y int, s string, ink RGBA)

Text paints s at (x, y) using the built-in 5×7 bitmap font (see font.go). Each glyph is 5 columns × 7 rows + 1 pixel of inter- glyph spacing (advance = 6).

type ProgressBar

type ProgressBar struct {
	Bounds Rect
	Value  float64
}

ProgressBar visualises a 0.0..1.0 value as a filled ratio of its bounds. Values outside the range clamp.

func (*ProgressBar) Draw

func (b *ProgressBar) Draw(p Painter, theme *Theme)

Draw paints the empty track + a filled portion sized to Value.

type RGBA

type RGBA struct{ R, G, B, A uint8 }

RGBA is a 32-bit colour value. Painters that can't represent a given RGBA (a cell grid limited to 16 colours, for instance) pick the closest supported value.

func RGB

func RGB(r, g, b uint8) RGBA

RGB constructs an opaque colour with A=0xFF.

type Rect

type Rect struct{ X, Y, W, H int }

Rect is a rectangle in the painter's coordinate system. toolkit aliases its own Rect to this one, so the method set is shared — keep any helpers a consumer might want on it (Contains, etc.) defined here.

func (Rect) Contains

func (r Rect) Contains(px, py int) bool

Contains reports whether (px, py) falls inside r. The right + bottom edges are EXCLUSIVE so a w*h rectangle covers exactly w*h units (pixels for PixelPainter, cells for CellPainter).

type Theme

type Theme struct {
	Background RGBA
	Surface    RGBA
	OnSurface  RGBA
	Border     RGBA
	Accent     RGBA
}

Theme is the palette every widget consults. The prototype ships a minimum viable set — a production merge into toolkit reuses the full go-widgets/toolkit theme struct.

func DarkTheme

func DarkTheme() *Theme

DarkTheme mirrors the go-widgets/toolkit default dark palette.

func LightTheme

func LightTheme() *Theme

LightTheme mirrors the go-widgets/toolkit default light palette.

type Translator added in v0.4.0

type Translator interface {
	PushTranslate(dx, dy int)
	PopTranslate()
}

Translator is an optional Painter capability: while a translation is pushed, every coordinate handed to the painter is shifted by it before anything is clipped or written.

Together with Clipper it is a VIEWPORT — the pair a scrolling or panning widget needs. Clipping alone was not enough, and the gap had a real cost: with no way to say "draw my child 250 pixels higher", Clipper's only customer, ScrollView, moved the child's BOUNDS instead, drew, and put them back. Geometry that changes for the duration of a paint is invisible to anything reading it from outside — a screen reader was told a control sat a quarter of a window below where it was painted.

A translation shifts the PAINT, not the widget: a child still lays out and reports its bounds wherever it genuinely is, and the viewport decides where those pixels land. Nothing has to be moved and restored.

if t, ok := p.(painter.Translator); ok {
	t.PushTranslate(-offsetX, -offsetY)
	defer t.PopTranslate()
}
child.Draw(p, theme)

Translations nest: each push adds to the enclosing one, so a scrolled list inside a scrolled panel behaves as a reader would expect. A clip pushed while a translation is active is translated too, since the caller expresses it in the same coordinates as everything else it draws.

Both PixelPainter and CellPainter implement Translator; a back-end that cannot translate simply does not, and the assertion is skipped — the same contract Clipper uses.

type Widget

type Widget interface {
	Draw(p Painter, theme *Theme)
}

Widget is what every UI element implements. A widget's Draw only touches the Painter primitives; it never allocates a buffer, never knows whether it's rendering pixels or cells.

Directories

Path Synopsis
cmd
tui-demo command
tui-demo renders the same three widgets as wui-demo, but into a CellPainter, and writes the resulting 24-bit-ANSI stream to stdout.
tui-demo renders the same three widgets as wui-demo, but into a CellPainter, and writes the resulting 24-bit-ANSI stream to stdout.
wui-demo command
wui-demo renders the prototype's three widgets into a PixelPainter and writes the resulting RGBA buffer as a PNG.
wui-demo renders the prototype's three widgets into a PixelPainter and writes the resulting RGBA buffer as a PNG.
wui-wasm command

Jump to

Keyboard shortcuts

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