minigui

July 5, 2026 · View on GitHub

A tiny immediate-mode GUI toolkit for Ebitengine, meant to be shared across small Ebiten programs that need basic user input without pulling in a heavy widget framework. Its dependencies are Ebitengine itself and native (cgo-free system clipboard for copy/cut/paste); everything else is the Go standard library.

Status: early but usable — pluggable text faces (see Fonts) and per-app styling (see Theming).

Model

Immediate mode: every frame you build an Input, call Begin, run the widgets, then End; widgets read the input and append draw commands that Render flushes to an *ebiten.Image. Input is a plain struct, so the widget logic is testable without a window.

type game struct {
	gui   minigui.Context
	name  string
	count int
}

func (g *game) Update() error {
	g.gui.Begin(minigui.InputFromEbiten(), 20, 20)
	g.gui.Label("hello")
	if g.gui.Button("inc", fmt.Sprintf("count: %d", g.count)) {
		g.count++
	}
	g.gui.TextField("name", &g.name)
	g.gui.End()
	return nil
}

func (g *game) Draw(screen *ebiten.Image) {
	g.gui.Render(screen)
}

Widgets

  • Label(text) — static text.
  • Button(id, label) bool — reports a click this frame.
  • Toggle(id, label, on) bool — button with an active state.
  • Swatch(id, color, selected) bool — clickable color square.
  • TextField(id, *string) bool — editable single-line field (focus, caret, horizontal scroll); reports whether the text changed.
  • TextArea(id, *string, rows) bool — multi-line editor (Enter inserts a line, Up/Down move between lines, line-relative Home/End, vertical scroll).
  • List(id, items, *selected) bool / ListWithIcons(...) — scrollable, selectable list; the icon variant reserves a square per row for the caller to draw into.

Text editing (TextField and TextArea) comes with selection (Shift+arrows, mouse drag, double-click word select, Ctrl/Cmd+A), system-clipboard copy/cut/paste (Ctrl/Cmd+C/X/V, via native/clipboard), and undo (Ctrl/Cmd+Z, with typing runs coalesced).

Layout helpers: SameLine, SetItemWidth. Focus helpers: HasFocus, ClearFocus (so the host can suspend single-key shortcuts while a field is being typed into), Focus(id) to hand focus to a widget, and Submitted(id) to detect Enter in a field.

Fonts

By default a Context renders labels with Ebitengine's built-in debug font (a fixed 6×16 cell, ASCII only). Call SetFace to use any text/v2 face — then text is measured and drawn through it (so widths, the caret and row heights follow the font), non-Latin scripts such as Japanese render, and label colors are honored.

SystemFace(size) is a convenience that loads a CJK-capable operating-system font (it tries a small per-OS list of well-known paths, no font bundled, no external dependency) and returns a face for SetFace. It returns an error when none is found, so you can fall back to the debug font:

if face, err := minigui.SystemFace(16); err == nil {
	gui.SetFace(face)
}

To bundle your own font instead, build a face from embedded bytes with text.NewGoTextFaceSource and pass it to SetFace.

Theming

Colors and layout metrics live in a Style. A zero-value Context uses DefaultStyle (a dark, cyan-tinted scheme); override what you want and apply it with SetStyle:

s := minigui.DefaultStyle()
s.Button = color.RGBA{0x20, 0x14, 0x28, 0xff}
s.Focus = color.RGBA{0xff, 0x99, 0x33, 0xff}
s.RowH = 28
s.Face = face // optional; same as SetFace
gui.SetStyle(s)

SetFace is a shortcut that sets only Style.Face; Style() returns the current style for reading or tweaking a single field.

VGAPalette is the classic 16-color IBM VGA text palette as hex strings — handy as a ready-made swatch set for Swatch rows in retro-styled tools.

Panels

BeginPanel(title, x, y) / EndPanel() frame the widgets between them in a titled background box (a small simulated window), auto-sized to the content. EndPanel returns the panel's screen rectangle, handy for hit-testing the whole panel — e.g. to keep it interactive in a click-through overlay.

gui.Begin(in, 0, 0)
gui.BeginPanel("Tools", 8, 8)
gui.Toggle("draw", "Draw", drawing)
rect := gui.EndPanel()
gui.End()

Windows

A Window is a panel with a life of its own: draggable by the title bar and closable by a close box. The caller owns the state (X, Y, Open persist across frames); minigui moves X/Y during a drag and clears Open on close (NoClose hides the close box, e.g. for an always-present toolbar). Dragging() reports an in-progress drag, so a click-through host can keep grabbing the mouse until the drag ends.

var win = minigui.Window{Title: "Tools", X: 8, Y: 8, Open: true}

if gui.BeginWindow(&win) {
	gui.Toggle("draw", "Draw", drawing)
	gui.EndWindow()
}

Install

go get github.com/crgimenes/minigui

Demo

go run github.com/crgimenes/minigui/cmd/minigui-demo

License

See LICENSE.


More of my projects

  • native: cgo-free Go bindings for OS APIs: clipboard, mmap, keep-awake, and friends.
  • NeoFrame: draw over your screen; a transparent overlay for demos and classes.
  • kutta: a 2D wind tunnel; watch air misbehave around an airfoil.
  • glaze: WebView desktop apps in Go, cgo-free.

More at github.com/crgimenes and crg.eti.br.