ui.um offers an immediate GUI library suitable both for simple game menus and more complex applications. See the tutorial for example usage.

struct BoxStyle

type BoxStyle* = struct {
	img: image.Image
	outer, inner: rect.Rect
	scale: th.fu
	color: uint32
}

BoxStyle describes how a box within the GUI is styled. In this case box can be anything, ranging from a button to a container. By default the box is drawn using the image.Image.drawNinepatch method. However if the image is invalid, a rectangle with color color is drawn.

interface Font

type Font* = interface {
	draw(text: str, pos: th.Vf2, color: uint32, scale: th.fu = 1.0)
	measure(test: str): th.Vf2
}

This interface is used by all elements that draw text. A font.Font implements this interface.

struct PixelFont

type PixelFont* = struct { }

This struct implement the Font interface using the canvas.um pixel font.

struct Style

type Style* = struct {
	// current font
	ft: Font
	// font scale
	ftScale: th.fu
	// text color
	ftColor: uint32

	// Positive box - i. e. unpressed button
	posBox: BoxStyle
	// Negative box - i. e. pressed button, text box
	negBox: BoxStyle
	// Used to draw containers
	containerBox: BoxStyle
}

Style is used as a global state for styling the GUI.

interface Container

type Container* = interface {
	// This adds a rectangle to the container, and returns the rectangle
	// which was actually added (the container can modify the rectangle).
	// See individual containers for further documentation.
	pushRect(r: rect.Rect): rect.Rect
	getDims(): rect.Rect
}

Containers are used to layout elements or other containers.

struct Gui

type Gui* = struct {
	// user context passed to layout functions
	ctx: any

	// the index of the current selection. TODO implement properly
	selection: int
	// true, if the layout is being evaluated, not drawn
	isEval: bool
	// contains more unexported fields

This is the main struct of any UI. Styles and containers are in a stack.

fn mk

fn mk*(r: rect.Rect, s: Style): Gui

Creates a new gui spanning r, with style s.

type LayoutFn

type LayoutFn* = fn(gui: ^Gui)

The layout function calls different element or container methods to create the user interface itself. It is called in the eval and draw.

fn BoxStyle.draw

fn (this: ^BoxStyle) draw*(r: rect.Rect) {

Draws a rectangle using a BoxStyle

fn Gui.pushStyle

fn (this: ^Gui) pushStyle*(s: Style) {

Pushes a style onto the style stack.

fn Gui.popStyle

fn (this: ^Gui) popStyle*() {

Pops a style from the style stack.

fn Gui.getStyle

fn (this: ^Gui) getStyle*(): ^Style {

Returns a pointer to the style atop the style stack.

fn Gui.getContainer

fn (this: ^Gui) getContainer*(): Container {

Returns the container atop the container stack.

fn Gui.pushRect

fn (this: ^Gui) pushRect*(r: rect.Rect): rect.Rect {

Shortcut to this.getContainer().pushRect(r)

fn Gui.getDims

fn (this: ^Gui) getDims*(): rect.Rect {

Shortcut to this.getContainer().getDims(r)

fn Gui.dupStyle

fn (this: ^Gui) dupStyle*() {

Duplicates the current style.

fn Gui.pushContainer

fn (this: ^Gui) pushContainer*(c: Container) {

Pushes a container onto the container stack.

fn Gui.popContainer

fn (this: ^Gui) popContainer*() {

Pops a container from the container stack.

fn Gui.eval

fn (this: ^Gui) eval*(layout: LayoutFn) {

Runs the evaluation phase on layout.

fn Gui.draw

fn (this: ^Gui) draw*(layout: LayoutFn) {

Runs the draw phase on layout.

enum BoxGrow

type BoxGrow* = uint
const (
	// Increments by an amount set by the user.
	BoxGrowDimension*   = BoxGrow(0)
	// Divides the container into `n` equal parts.
	BoxGrowSubdivision* = BoxGrow(1)
)

The different types of "growing" the box can do.

enum BoxDirection

type BoxDirection* = uint
const (
	BoxDirectionDown*  = BoxDirection(0)
	BoxDirectionRight* = BoxDirection(1)
	BoxDirectionUp*    = BoxDirection(2)
	BoxDirectionLeft*  = BoxDirection(3)
)

Direction in which the box will grow.

struct BoxConfig

type BoxConfig* = struct {
	// dimension to grow by if `BoxGrowDimension` is used
	dimension: th.fu
	// number of subdivisions if `BoxGrowSubdivisions` is used
	subdivisions: uint
	// the grow type
	growType: BoxGrow
	// the grow direction
	dir: BoxDirection
	// rect passed to the current container
	rect: rect.Rect
	// padding inserted after each element
	padding: th.fu
}

Configuration of the Box container.

struct Box

type Box* = struct {
	grow: th.fu
	dm: rect.Rect
	cfg: BoxConfig
}

Box is the main layout. It puts the elements next to each other, according to the config.

If the dimensions of the rect passed to pushRect are non zero, they will be kept. Position is always forced.

fn Gui.box

fn (gui: ^Gui) box*(cfg: BoxConfig = {
	dimension: 30,
	growType: BoxGrowDimension,
	dir: BoxDirectionDown }) {

Adds the Box container to the gui.

struct StackConfig

type StackConfig* = struct {
	rect: rect.Rect
	padding: th.fu
}

Configuration for the Stack container.

struct Stack

type Stack* = struct {
	dm: rect.Rect
	cfg: StackConfig
}

The stack container puts elements on top of each other. If a property of the rect passed to pushRect is zero, it will be changed to an equivalent property of the containers' dimensions (minus the padding), else it will stay the same. This means stack can be used either to put multiple elements/containers on top of each other, or for absolutely positioned elements.

fn Gui.stack

fn (gui: ^Gui) stack*(cfg: StackConfig = {}) {

Adds the Stack container to the gui.

struct ScrollAreaConfig

type ScrollAreaConfig* = struct {
	rect: rect.Rect
	// scroll speed. Default is 1
	speed: real32
	// if true, scrolling will be horizontal
	horizontal: bool
}

Configuration for the scroll area.

struct ScrollArea

type ScrollArea* = struct {
	dm: rect.Rect
	cfg: ScrollAreaConfig
	scroll: ^real32
	maxScroll: real32
}

Scroll area is a container which allows the user to scroll. It acts as a stack container, but all the elements are shifted by the scroll.

fn Gui.scrollArea

fn (gui: ^Gui) scrollArea*(scroll: ^real32, maxScroll: real32, cfg: ScrollAreaConfig = {}) {

Pushes a scroll area. scroll is both input and output value. Both scroll and maxScroll are in pixels.

struct ButtonConfig

type ButtonConfig* = struct {
	rect: rect.Rect
}

Configuration for the button.

fn Gui.button

fn (gui: ^Gui) button*(cfg: ButtonConfig = {}): bool {

Adds a button to the gui. The button acts like a Stack container, but it is drawn using the pos/nexBox styles and handles clicks. If the button is pressed and the gui is in the eval phase, the return value will be true.

struct LabelConfig

type LabelConfig* = struct {
	// centers the label along the X axis, enables `stretchX`
	centerX: bool
	// centers the label along the Y axis, enables `stretchY`
	centerY: bool
	// if false, the rect passed to `pushRect` will have the width of
	// the text, else it will be 0
	stretchX: bool
	// if false, the rect passed to `pushRect` will have the height of
	// the text, else it will be 0
	stretchY: bool
	// forces the rectangle the label will use
	rect: rect.Rect
}

fn Gui.label

fn (gui: ^Gui) label*(text: str, cfg: LabelConfig = {

Draws a label using the current font style.

fn Gui.qbutton

fn (gui: ^Gui) qbutton*(text: str, cfg: ButtonConfig = {}): bool {

Adds a button with a label to gui.

struct TextBoxConfig

type TextBoxConfig* = struct {
	// force the rect of the text box
	rect: rect.Rect
}

struct TextBox

type TextBox* = struct {
	// the content of the textbox
	buffer: str
	// index of the cursor
	cursor: int
}

fn TextBox.clear

fn (this: ^TextBox) clear*() {
	this.buffer = ""
	this.cursor = 0
}

Clears the textbox

	gui.idx++

	r := gui.pushRect(cfg.rect)

	hover := gui.hover(r)

	if gui.isEval {
		if input.isJustPressed(input.mouse1) && hover {
			gui.selection = gui.idx
		}

		if input.isJustPressed(input.mouse1) && !hover &&
			gui.selection == gui.idx {
				gui.selection = 0
		}
		
		if input.isJustPressed(input.key_escape) && gui.selection == gui.idx {
			gui.selection = 0
		}

		if gui.selection != gui.idx {
			return
		}

		if input.isJustPressed(input.key_left) ||
			input.isPressedRepeat(input.key_left) {
			if tb.cursor > 0 { tb.cursor-- }
		}

		if input.isJustPressed(input.key_right) ||
			input.isPressedRepeat(input.key_right) {
			if tb.cursor < len(tb.buffer) { tb.cursor++ }
		}

		if input.isJustPressed(input.key_backspace) ||
			input.isPressedRepeat(input.key_backspace) {

			if tb.cursor > 0 {
				tb.buffer = slice(tb.buffer, 0, tb.cursor - 1) +
					slice(tb.buffer, tb.cursor)
				tb.cursor--
			}
		}

		v := true

		if input.isPressed(input.key_ctrl) && input.isJustPressedc('u') {
			tb.clear()
			v = false
		}

		s := input.getStr()
		for i in s {
			if int(s[i]) < int(' ') || int(s[i]) == int('z') + 1 {
				v = false
			}
		}
		if len(s) > 0 && v {
			tb.buffer = slice(tb.buffer, 0, tb.cursor) + s +
				slice(tb.buffer, tb.cursor)
			tb.cursor += len(s)
		}

		return
	}


	style := gui.getStyle()

	style.negBox.draw(r)
	canvas.beginScissorRect(r)

	dm := style.ft.measure(tb.buffer).mulf(style.ftScale)

	p := th.Vf2{}
	p.y = r.y + r.h/2 - dm.y/2
	c := th.Vf2{r.x, p.y}

	cdmx := style.ft.measure(slice(tb.buffer, 0, tb.cursor)).x * style.ftScale
	if cdmx < r.w - 2 {
		p.x = r.x + 1
		c.x = p.x + cdmx
	} else {
		c.x = r.x + r.w - 1
		p.x = c.x - cdmx
	}
	
	aW := style.ft.measure("A").x * style.ftScale

	style.ft.draw(tb.buffer, p, style.ftColor, style.ftScale)
	if gui.selection == gui.idx {
		canvas.drawRect(style.ftColor, rect.mk(c.x, c.y, aW / 4, dm.y))
	}

	canvas.endScissor()
}


	if cfg.centerX { cfg.stretchX = true }
	if cfg.centerY { cfg.stretchY = true }

	r := cfg.rect
	dm := i.getDims().mul(cfg.scale)
	if r.w == 0 { r.w = dm.x }
	if r.h == 0 { r.h = dm.y }

	if cfg.stretchX { r.w = 0 }
	if cfg.stretchY { r.h = 0 }

	r = gui.pushRect(r)

	if gui.isEval { return }

	x := r.x
	y := r.y
	if cfg.centerX {
		x += (r.w - dm.x) / 2
	}

	if cfg.centerY {
		y += (r.h - dm.y) / 2
	}

	i.draw(th.Transform{
		p: { x, y },
		s: cfg.scale }, cfg.color)
}