gift

package module
v0.0.0-...-11dc024 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: BSD-2-Clause Imports: 10 Imported by: 0

README

Gift

Gift is a declarative UI toolkit for Go: SwiftUI-style views, typed state and GPU rendering with Ebitengine, without a browser or WebView. It focuses on desktop and touchscreen kiosk applications, particularly on Raspberry Pi 4 and 5.

Why?

Go applications should be able to describe their interfaces directly in Go, without bringing along a web stack. Gift combines small, composable views with targeted state updates instead of repeatedly rebuilding the entire UI. Virtualized galleries, asynchronous image loading and bounded resource budgets are designed for image-heavy interfaces on modest hardware.

Under development: APIs may still change. 1080p at 60 Hz on Raspberry Pi is a target, not a verified performance guarantee.

What It Looks Like

Actual framebuffer captures of the running examples, taken with giftauto.

Kitchen Sink: every component, each with the live demo of its documentation page, behind a list grouped like the documentation, with search, an on-screen keyboard and switchable light/dark themes.

Image Gallery Effects
Virtualized gallery with generated sample images in a masonry layout Experimental glass panel over colored tiles and cards with shadows
Masonry/justified layouts, selection, file/HTTP sources and a thumbnail cache. The red tile demonstrates a loading error. Shadows, rounded shapes and experimental glass with adjustable quality.

Try It

Requires Go 1.27 and a graphical session with GPU support. The examples include Inter as an embedded font. The intended Raspberry Pi setup is Raspberry Pi OS 64-bit with X11/XWayland and hardware-accelerated Mesa; builds do not require CGO.

From the cloned repository, run one example at a time:

go run ./cmd/example-counter      # Start here: state, buttons, scrolling
go run ./cmd/example-kitchensink  # UI component showcase
go run ./cmd/example-gallery      # Generates its own sample images
go run ./cmd/example-effects -quality=full
go run ./cmd/example-photos       # Gallery and full screen viewer, see Pictures below
go run ./cmd/example-video        # Needs the FFmpeg libraries, see Video below

Use your own JPEG/PNG images: go run ./cmd/example-gallery -dir "$HOME/Pictures". Interact using a mouse, keyboard or touch; the gallery also supports drag scrolling.

Your Own Application

Add the module with go get github.com/worldiety/gift. Views are Go functions; state is created in the context, and changes rebuild the views that depend on it:

func counter(ctx *gift.Context) gift.View {
    count := ctx.State("count", 0)
    n := ctx.Read(count)
    return ui.Window(ui.VStack(
        ui.Text(fmt.Sprintf("%d clicks", n)),
        ui.Button(ui.Text("+1"), func() { count.Set(count.Get() + 1) }),
    ).Gap(16).Padding(24))
}

gift.New(gift.Options{Root: counter}) creates the app; backend.Run from github.com/worldiety/gift/backend/ebiten opens the window. The application explicitly sets its font and window size. See cmd/example-counter for a complete example.

Kiosks And Appliances

A device that shows the same screen for hours does not need sixty frames a second. Two settings in backend.Config take the load off:

backend.Run(app, backend.Config{
    DrawOnDemand: true, // draw only when something changed, keep the last frame otherwise
    IdleTPS:      10,   // and tick slowly while nothing happens
})

With DrawOnDemand a frame is drawn only while the tree changed, an animation runs, or the renderer still has pictures to upload; every other display refresh keeps the previous frame. On an idle screen that is a few frames a minute instead of sixty a second.

Picking Files

There is no native file dialog on a fullscreen kiosk, so gift brings its own, made of its own views. ui/filepicker opens one or several files, chooses a folder or picks a path to save to, through an fs.FS you choose — the application decides what may be browsed. Present it in a ui.Sheet:

ui.Sheet(screen, filepicker.New(os.DirFS("/media/usb"), open, cancel).
    Mode(filepicker.OpenMulti).
    Extensions(".jpg", ".png")).
    Presented(picking).
    OnDismiss(cancel)

Directories are read off the UI goroutine and listed with ui.VirtualList, so a folder of a hundred thousand files scrolls like one of a hundred.

Camera Photos

Galleries of 12–50 megapixel camera JPEGs thumbnail several times faster with libjpeg-turbo, which also decodes straight to 1/2, 1/4 or 1/8 of the size when only a tile is needed. It is loaded at run time, still without CGO; install libturbojpeg0 on Raspberry Pi OS or jpeg-turbo with Homebrew, and register it explicitly:

if turbojpeg.Register() { // github.com/worldiety/gift/asset/turbojpeg
    asset.RegisterDecoder(asset.MIMEJPEG, turbojpeg.Decoder{})
}

Without the library, Register returns false and image/jpeg stays in charge.

Pictures And Gestures

ui/imageviewer shows pictures as large and as interactive as the screen allows, in the manner of iOS's Photos: a full screen viewer that grows out of the gallery tile that was tapped and shrinks back into it, to swipe through, pinch, double tap, drag down to close and swipe up for a sheet of details, with fading arrows, wheel zoom and keys on the desktop. Pictures load in stages — a preview at once, which libjpeg-turbo decodes from the DC coefficients of the JPEG's blocks, the screen size next, more detail while zoomed.

viewer := imageviewer.New(gallery.Collection(), resolve) // github.com/worldiety/gift/ui/imageviewer
viewer.SetOrigin(gallery)
return ui.ZStack(
    ui.ImageGallery(gallery).OnOpen(func(id asset.ID) { viewer.Open(id) }),
    imageviewer.Fullscreen(viewer).Card(details),
)

The gestures are reusable on their own, SwiftUI style: ui/gesture has tap, long press, drag, pinch, rotation, hover and wheel recognizers, ui/motion the rubber band, deceleration and springs of UIKit. On Linux the backend reads the touch screen directly, so a pinch works on a Raspberry Pi display, and resamples the finger to the frame as Android does, so that nothing judders on a 50 Hz panel. Try it with go run ./cmd/example-photos.

A photo full screen on a Raspberry Pi 5 with a sheet of its file name and size slid over it, opened through a virtual touch screen
Menus And Popovers

ui.Menu, ui.ContextMenu and ui.Popover work like their iOS counterparts: a menu opens on the press of its label and a finger can slide onto an item and lift to choose it, a long press dims the window around a picture and offers its actions, a secondary click opens the same menu at the pointer, and a popover points at its anchor with an arrow. They live in a top layer above the whole window (gift.Element.Overlay), so no scroll view clips them, and cost nothing while closed.

ui.Menu(ui.Icon(outline.DotsHorizontal),
    ui.MenuItem("Share", share).Icon(outline.ShareNodes),
    ui.MenuSection("",
        ui.MenuItem("Delete", remove).Icon(outline.TrashBin).Destructive(),
    ),
)

Wheels choose values the way iOS does: ui.WheelPicker with any number of wheels that turn, flick and settle on a row, and ui.DateWheel and ui.TimeWheel built from it. ui.Calendar is the graphical date picker: a month grid that pages sideways under a finger, with month and year wheels behind its title. ui.SecureField takes a password or a PIN: dots drawn the way iOS draws them, no copy or cut, and on a kiosk the character just typed shown for a moment. ui.TextArea edits text of several lines: it wraps, grows with its text up to a number of lines and then scrolls, keeps the column on up and down, and lets a finger scroll where a mouse selects. Smaller pieces of the same kind: ui.Stepper (minus and plus, repeating while held), ui.SearchField (glass, clear button, escape clears), ui.PageControl (dots to tap or scrub along) and ui.Toast (a capsule that slides in, stays two seconds and can be swiped away). Lists get the two gestures of iOS: ui.Swipe uncovers actions behind a row dragged sideways, with full swipe, and VScroll(...).Refreshable starts a refresh when the content is pulled down past its top, with the spinner of UIRefreshControl. ui.DisclosureGroup is a row that shows or hides content below it, with a turning chevron, and ui.Accordion a list of them with at most one open. Rings are a shape of the renderer like rounded rectangles, drawn from a distance field in the same draw call: ui.ProgressRing (and its indeterminate spinner), ui.Gauge in the circular styles of SwiftUI and ui.CircularSlider for a thermostat or a dial. ui.ColorPicker is UIColorPickerViewController with its grid, spectrum and sliders, opacity and hex code, and ui.ColorWell the swatch of SwiftUI's ColorPicker that opens it in a popover.

Video

ui/videoplayer plays a video with sound and touch controls laid out like the player of iOS: play and pause, ±10 seconds, a position bar to drag, volume, mute and an audio track menu, sized for a five inch panel. Decoding goes through the FFmpeg libraries of the system, loaded at run time without CGO: Raspberry Pi OS has them, Homebrew's ffmpeg brings them to macOS.

p, err := video.Open("film.mp4", video.Options{HW: true}) // github.com/worldiety/gift/video
if err != nil { ... }
p.Play()
return videoplayer.New(p)                                 // github.com/worldiety/gift/ui/videoplayer

A Raspberry Pi 5 shows full HD at 60 frames a second with backend.Config{DirectToScreen: true, DrawOnDemand: true}: H.264 in software, HEVC in software or, with HW, in hardware. Try it with go run ./cmd/example-video -width 800 -height 480.

A video with round play, back and forward buttons, a volume slider and an open audio track menu, captured on a Raspberry Pi 5 at 800x480
SVG

ui/svg draws SVG documents: paths and shapes, groups and use, colours, gradients, strokes with joins, caps and dashes, transforms and clip paths, the subset drawing programs and icon sets write. A picture is rasterised at the resolution of the screen, a large one on a goroutine, and an icon can be tinted like text.

logo, err := svg.ParseFile("logo.svg") // github.com/worldiety/gift/ui/svg
return svg.View(logo).Frame(120, 0)
Login and payment

web runs an OAuth login or a checkout in the system browser and hands the application the redirect it ends with, on a loopback port as RFC 8252 asks; the protocol is golang.org/x/oauth2's. There is no web view: identity providers block logins in one, and payment providers forbid them. On a kiosk, web/handoff moves the flow to the user's phone with a QR code: the device flow of RFC 8628 for a login, a payment link for a payment. ui/qr draws the code, and none of it needs cgo.

ret, err := web.Open(ctx, web.Flow{ // github.com/worldiety/gift/web
	Callbacks: []web.Callback{web.Loopback("/oauth")},
	Start: func(_ context.Context, redirect string) (string, error) {
		conf.RedirectURL = redirect
		return conf.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)), nil
	},
})
code, err := ret.AuthCode(state)

handoff.Login(conf, func(tok *oauth2.Token) { session.Set(tok) }) // github.com/worldiety/gift/web/handoff
Charts

ui/charts draws charts after Swift Charts: lines, areas, bars, dots, rules, bands and annotations on axes that fit the data, bars and areas stacked or grouped, pies and rings, with a legend and the values under the pointer. A missing value (NaN) breaks a line, and every chart with a continuous x axis zooms: pinch, double tap, a range dragged with the mouse; linked charts zoom and inspect together.

temps := charts.Times(times, values) // github.com/worldiety/gift/ui/charts
return charts.Chart(
	charts.Line(temps).Name("Hall 2"),
	charts.RuleY(23).Label("Limit").Dash(4, 3),
).YTitle("°C")

A chart draws no more than its width can show: of every pixel column the first, the last, the lowest and the highest point, the M4 aggregation, which covers the same pixels as all of them. Data in memory keeps an index for it, so ten million points zoom like a hundred, and a charts.Source is asked, off the user interface goroutine, for just the points of what is shown, the way a map asks for tiles.

Testing And Automation

go test ./...
go test -tags giftauto ./...
# Pixel tests using a real GPU and existing reference images:
GIFT_REQUIRE_GOLDEN=1 go test -tags giftgpu ./...

For automation and screenshots, the build tag alone is enough. No extra source file or side-effect import is needed in the application:

go run -tags giftauto ./cmd/example-kitchensink
# In a second terminal:
curl -fsS http://127.0.0.1:7391/tree
curl -fsS 'http://127.0.0.1:7391/screenshot?settle=8' -o screenshot.png

Do not ship with giftauto: It enables an unauthenticated local debugging interface for input and screen capture. Without the tag, it is not included in the backend. See auto, reproducing the screenshots and architecture and goals (in German).

License

BSD-2-Clause. Embedded fonts and icons have their own license notices in their package directories.

Documentation

Overview

Package gift is the runtime core of the gift UI toolkit.

It owns the view contract, the build context, component scopes, typed state and bindings, the retained tree, layout and the frame lifecycle. Concrete widgets live in ui, concrete rendering lives in backend/ebiten; see the project plan, section 3.

Frame model

Ebitengine decouples Update from Draw: several updates may run before a frame is drawn, and the whole screen is cleared and redrawn every drawn frame. The backend may skip a refresh in which nothing asked for a paint (App.NeedsPaint) and leave the previous frame standing; a frame that is drawn is still drawn whole. gift therefore does all of its CPU work in App.Update and only produces the display list in App.Paint. Build and layout in Paint are a contract violation and panic. See the project plan, section 6.

Invalidation

The three levels build, layout and paint are kept separate. A build implies a layout and a layout implies a paint, never the other way round. Invalidation saves CPU work, not fill rate: a frame that changes nothing still redraws everything, but it neither calls view functions nor measures anything.

The layout level is partial, not all or nothing. A node that is rebuilt marks itself and every node up to the root as needing layout, and the layout pass descends along that path only: a clean node that is offered the constraints it was last measured with returns its cached size without running its layouter, so a state write in one subtree does not measure a sibling subtree. App.Diagnostics makes that observable.

The build level has a top down boundary too, but only where the application asks for one. Component is always rebuilt when its parent is, because its inputs are captured in a closure that gift cannot compare. Memo takes its inputs as an explicit comparable value and is skipped when they are unchanged.

Allocation

The frame path without build is allocation free after warmup: an update that finds nothing dirty plus a paint over an unchanged tree performs zero allocations. Build is explicitly excluded from that contract, because composing views boxes them into interfaces by construction. See the project plan, section 11.

Input

Input is polled by the backend, turned into events and dispatched into the App during Ebitengine's Update, before build and layout. Event handlers therefore run where state writes are allowed and are picked up by the build of the same tick; see the project plan, section 6.

A node takes part in hit testing only if its Element carries an Interactor. Hit testing walks the retained tree front to back and honours the clips and transforms of ancestors, using the same device space convention as the display list, so what is visible and what is clickable cannot disagree. Hover, press and focus are kept in the retained node as Interaction and are read by painters; changing them repaints and does not rebuild anything, which is what the project plan, section 5, requires of presentation state.

Logging

Nothing in the frame path logs, not even behind a level check, because constructing the attributes alone would allocate. The frame path only increments plain numeric counters, which are read out of band through App.Diagnostics. gift never logs to slog.Default; without a logger in Options logging is off. See the project plan, section 15.

Concurrency

An App, its state and its tree belong to one goroutine, the UI executor: the goroutine that called New. Touching state from anywhere else is a programming error. A build with the giftdebug tag rejects it with a panic; a release build does not check, because the check cannot be made cheap enough for the event handler path, and the race detector covers the same ground.

Exactly two App methods may be called from any goroutine. App.Post hands a closure to the UI executor, which runs it at the beginning of the next update; that is how a worker result reaches the interface, and Token is how a result that has since gone stale is rejected — Token.Post is safe from any goroutine too. App.Diagnostics returns a synchronised snapshot of the frame counters.

Index

Constants

View Source
const (
	// LongPressDelay is how long a pointer has to stay down, without moving
	// further than DragSlop, before [EventLongPress] is delivered.
	LongPressDelay = 500 * time.Millisecond
	// DragSlop is how far a pointer may travel from the press position and
	// still count as a tap rather than a drag, in logical pixels.
	DragSlop float32 = 8
	// MaxTouches is the number of fingers gift tracks at once. Ten is what
	// two hands have and what every multitouch panel reports at most; a
	// finger beyond it is counted in [Diagnostics.DiscardedTouches].
	MaxTouches = 10
)

Gesture recognition constants.

They are package level values rather than configuration because a gesture threshold is a property of the human hand, not of the application, and because making them per App would put a pointer dereference in the dispatch path for a number that never changes.

View Source
const (
	// KeyRepeatDelay is how long a key must be held before the first
	// synthetic press.
	//
	// 400 ms sits between the two conventions it has to live with: X11's
	// default auto repeat delay is 500 ms and macOS's slider ranges from
	// roughly 250 to 900 ms. Shorter than about 300 ms and a deliberate
	// single press of an arrow key occasionally moves two cells; longer than
	// about 500 ms and holding a key feels broken before it starts.
	KeyRepeatDelay = 400 * time.Millisecond

	// KeyRepeatInterval is the period between synthetic presses once the
	// delay has passed, that is thirty per second.
	//
	// It is deliberately below the frame rate: at 60 Hz every second tick
	// produces one repeat, so the rate is stable under the tick loop rather
	// than being whatever the tick rate happens to be. A rate at or above the
	// frame rate would make the speed of a held arrow key depend on how busy
	// the machine is.
	KeyRepeatInterval = time.Second / 30
)
  • Why gift has to do this at all
  • Only keys, never characters

Key repeat constants.

Why gift has to do this at all

Ebitengine has no key repeat. A search of the pinned module for "repeat" finds nothing outside gamepad code and one GLFW mouse callback that discards glfw.Repeat; the closest thing on offer is inpututil.KeyPressDuration, which counts the ticks a key has been held and leaves the delay and the rate to the caller. The backend bridge declines inpututil for allocation reasons, so that tick count is not available there either — and the backend would be the wrong place anyway, because the repeat is a property of gift's event model and every backend would otherwise reinvent it. It therefore lives here, driven by the same injected clock as the long press, which is what makes it testable without a sleep.

Only keys, never characters

gift repeats KeyBackspace, KeyDelete, the four arrows and the two page keys, and nothing else; see [repeatsWhenHeld] for the exclusions. The important one is that gift never synthesises a repeated *character*. It could not do it correctly: it does not know what layout, dead key or compose sequence produced the last rune, and re-emitting that rune would type the wrong thing after every dead key. Where the platform repeats characters — GLFW's character callback fires again for an operating system auto repeat — they arrive through the ordinary rune channel and gift passes them on without noticing that they are repeats.

View Source
const MaxDensity = 4

MaxDensity is the largest device density gift accepts.

Four is two steps beyond the 2x displays this feature exists for and one beyond anything a desktop currently reports. It is a sanity bound and not a promise: the cost of a density is quadratic in pixels and in glyph atlas area, so a factor arriving from a misconfigured compositor should be refused at the boundary rather than turn into a framebuffer nobody can afford.

Variables

View Source
var (
	// DefaultScrollFriction is the decay rate k of a fling, in 1/s.
	DefaultScrollFriction float32 = 4
	// DefaultScrollFlingVelocity is how fast the pointer has to be moving on
	// release, in logical pixels per second, before a fling starts at all.
	// Below it the release is a stop.
	DefaultScrollFlingVelocity float32 = 120
	// DefaultScrollStopVelocity is the speed at which a running fling is
	// considered finished, in logical pixels per second.
	DefaultScrollStopVelocity float32 = 20
	// DefaultScrollMaxVelocity caps the fling speed, in logical pixels per
	// second. A velocity estimate over a very short window can be absurd;
	// this is what keeps a jittery sample from launching the content into
	// the far end of the document.
	DefaultScrollMaxVelocity float32 = 8000
	// DefaultScrollWheelStep is how far one unit of [Event.Delta] of an
	// [EventWheel] scrolls, in logical pixels: one notch of a mouse wheel.
	DefaultScrollWheelStep float32 = 48
	// DefaultScrollPreciseWheelStep is how far one unit of a precise wheel,
	// a trackpad, scrolls: the ten points of finger travel GLFW makes one
	// unit of, so that the content follows the fingers. See [Event.Precise].
	DefaultScrollPreciseWheelStep float32 = 10
)
  • The friction model

Default scroll gesture constants.

They are variables rather than constants so that an application can retune the feel globally, and every one of them can also be overridden per container through ScrollConfig; the project plan asks for adjustable values rather than magic numbers in the middle of the gesture code.

The friction model

A fling decays exponentially: v(t) = v0 * exp(-k*t), where k is DefaultScrollFriction in reciprocal seconds. Integrating it gives the distance travelled in one tick, v0 * (1 - exp(-k*dt)) / k, which is what App.BeginInput applies — the closed form and not v*dt, so the result does not depend on the tick rate. The total distance of an undamped fling is v0/k, so a flick at 2000 px/s with k = 4 travels 500 logical pixels and is visually over after about 700 ms.

Exponential decay rather than a constant deceleration because it has no discontinuity at the end: the velocity approaches zero smoothly and the fling is ended by DefaultScrollStopVelocity rather than by the curve crossing zero, which is what makes a stop look like a stop rather than a stall.

View Source
var ScrollIndicatorLinger = 900 * time.Millisecond

ScrollIndicatorLinger is how long after the last movement a scroll container keeps asking for repaints.

It is what makes a scroll indicator that fades out possible at all. gift redraws the whole visible list every frame anyway — the project plan, section 6 — so this changes no pixels by itself; what it changes is App.NeedsPaint, and with it the idle tick policy of the backend. Without it the backend would drop to [Config.IdleTPS] the moment the content stopped moving, and an indicator that fades over a quarter of a second would do it in three visible steps.

It bounds the linger and the fade of every indicator built on ScrollInfo.IdleFor. ui's default scroll bar holds for 500 ms and fades for 250 ms, so it is finished 150 ms — nine frames at sixty hertz — before this window closes; a style that lingers longer than this gets a last frame at the idle rate, which looks like the bar snapping away rather than fading. TestDefaultScrollBarFitsInsideTheIndicatorLinger in package ui pins that margin, so raising the hold cannot silently ship a snapping bar.

It is charged to every scroll container, on purpose

[App.setScroll] enrols a container here unconditionally, including one whose decoration is hidden and one in an application that never imports ui. That is a real cost — up to 900 ms of full rate ticking after the last movement — and it is deliberate. The alternative is for the core to learn whether anybody is watching, which means either a flag on ScrollSpec that every view type has to thread through and keep in sync with its own style, or a registration call from the decoration; both put a second source of truth next to a number that is already only a heuristic. The cost is bounded, it settles by itself, and it lands in the window right after a gesture the user just made, which is the one window in which the application is least likely to be asleep — a fling is still running through most of it anyway.

An application that wants none of it sets this to zero before the first frame: a container is then dropped from the list on the very next tick, after the one repaint that draws the frame in which the indicator is gone. That is the opt out, and it is a value rather than an API because "how long does an indicator linger" is exactly the question being answered.

View Source
var ShortcutModifier = defaultShortcutModifier()

ShortcutModifier is the modifier that turns a letter into an editing command: command on macOS, control everywhere else.

Why this is one variable and not a rule each view invents

Copy is command-C on a Mac and control-C on Linux. Both modifiers already exist in Mods, so every view *could* decide for itself — and then every view would decide slightly differently, and a kiosk with a Mac keyboard attached to a Raspberry Pi would work in half of them. The decision is made once, here.

The default follows runtime.GOOS, because that is the only evidence gift has: Ebitengine reports the physical modifier keys and says nothing about what the desktop means by them. An application whose hardware disagrees — the Mac keyboard on the Pi above — assigns to this variable before the first frame.

Why a package variable and not a field of Options

Not for the reason ScrollIndicatorLinger is one. That argument is about the dispatch path, and it does not transfer, because gift never reads this value at all: it is read by *views*, in their own event handlers, and a handler has an Event and no application handle. A field of Options would therefore have to be copied onto every event or reached through a context that gift's Interactor contract does not pass, and both of those are real costs paid so that a value which is a property of the keyboard could pretend to be a property of a window.

The keyboard is the right scope. A machine has one convention for what "the shortcut key" is, and two windows of one process disagreeing about it would be a defect, not a feature. Section 1 of the project plan has one window anyway.

It is convention, not enforcement

gift dispatches shortcuts nowhere. The five letters in backend/ebiten's key table reach a view as ordinary key presses with modifiers, and what copy means is the view's business. This variable is the shared answer to "which modifier", so that two views in one program cannot answer differently.

A view uses it as e.Mods.Has(gift.ShortcutModifier), which reads the same on both platforms and is right on both.

View Source
var WordModifier = defaultWordModifier()

WordModifier is the modifier that turns a caret move into a word move: option on macOS, control everywhere else.

It is the sibling of ShortcutModifier and everything that variable says about itself applies here: it is a property of the keyboard and not of a window, gift dispatches nothing on it, and an application whose hardware disagrees with runtime.GOOS assigns to it before the first frame.

The two are deliberately *different* modifiers on macOS and deliberately the *same* one on everything else, and that is not an inconsistency — it is what the two conventions are. On macOS command-left is the start of the line and option-left is the previous word; on X11 and Windows control-left is the previous word and there is no chord for the start of the line beyond home. A field that hard coded either convention would be wrong on the other platform, and a field that invented a third would be wrong on both.

When the two collide, the word wins

Binding rule, and it belongs here rather than in each view, because the collision is a property of these two values and not of any one widget: where this modifier and ShortcutModifier are the same key — which is every platform except macOS, and therefore the platform of the project plan, section 1 — a caret motion key held with it means the *word* motion. Home and end are then the only way to the ends of the line, which is exactly what the paragraph above says the X11 convention is.

It cannot be resolved here, by making the two values distinct. Control is genuinely both "the shortcut key" and "the word key" on X11: copy is control-C and the previous word is control-left, and moving either of them to another key would invent the third convention that is wrong on both platforms. So what is fixed here is the precedence, and a view spells it as testing WordModifier before ShortcutModifier in its motion keys; see ui.TextField, whose key handler is the only consumer today. Shortcuts that are not caret motions — the letters of copy, cut, paste and select all — are unaffected: there is no word motion on a letter key to collide with.

Functions

func RegisteredTypeNames

func RegisteredTypeNames(dst []string) []string

RegisteredTypeNames appends the names of every registered view type to dst and returns the extended slice.

It exists for diagnostics and for one specific test: the modifier check the project plan, section 4, requires instead of a generator needs a way to notice that a *new* view type appeared. Reflection cannot enumerate the types of a package, but every view type has to register itself here to be reconcilable at all, so this registry is the one place that knows the complete list.

The order is registration order and the names are the ones passed to RegisterType. Never persist them.

func RoundDensity

func RoundDensity(factor float64) float32

RoundDensity turns a raw device scale factor into the integer density gift works in. It is the *one* place the rounding of the project plan, section 18, happens.

Why there is a rounding at all

The project plan, section 14, excludes fractional DPI scaling and section 18 repeats the exclusion: integer factors are promised, a fractional one is rounded and the result is documented rather than dressed up as accuracy gift does not have. A 1.5 factor therefore becomes 2, and what the user sees is a frame rendered at twice the logical size and then resampled by Ebitengine to three halves of it. That is supersampling: it is slightly soft, it costs more fragments than 1.5 would, and it is unambiguously sharper than the 1x frame Ebitengine used to upscale. A factor of 1.4 becomes 1 and the output is what it has always been.

Why it is a single function and not a conversion at each use

Because a density that is 2 in the glyph atlas, 1.5 in the image ladder and 2 in the root transform is three different pictures of one display. Every consumer — App.SetDensity, the Ebitengine backend, gifttest — calls this and nothing else rounds.

A non-finite or non-positive factor is 1, and anything above MaxDensity is clamped to it.

func TypeName

func TypeName(id TypeID) string

TypeName returns the name a type was registered under, or a placeholder for an unknown ID. It is meant for diagnostics only.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App owns one user interface: the retained tree, the component scopes, the state and the display list.

An App belongs to exactly one goroutine, the UI executor: the goroutine that called New. In a windowed application that is the goroutine Ebitengine drives; in tests it is the test goroutine. The only methods that may be called from anywhere are App.Post and App.Diagnostics.

func New

func New(opts Options) *App

New creates an App and mounts the root component. It does not build anything yet; the first App.Update does.

The calling goroutine becomes the UI executor of this App. Everything except App.Diagnostics must be called from it, and the state of every component belongs to it.

func (*App) Audio

func (a *App) Audio() render.AudioOutput

Audio returns the sound device installed by App.SetVideos, or nil.

func (*App) BeginInput

func (a *App) BeginInput(now time.Duration)

BeginInput opens the input phase of one tick and advances the clock to now.

It must be called once per Ebitengine update, before the pointer and key methods and before App.Update. It is where time based gestures fire: a long press is recognised here, because nothing else happens while a finger rests on the screen and a gesture that needed an event to notice the passage of time would never fire at all.

It is also where kinetic scrolling is advanced, for exactly the same reason: a fling has to keep moving while the user does nothing at all. The clock is the parameter and never the wall clock, so gifttest.Advance can step a fling deterministically; the project plan, section 13, requires that.

now is a monotonic timestamp; differences are meaningful, the absolute value is not. A backend normally passes time.Since of a start instant.

func (*App) ContextMenuAt

func (a *App) ContextMenuAt(pos geom.Point)

ContextMenuAt reports a click of the secondary mouse button at pos: an EventContextMenu for the node under it, bubbling to its ancestors.

func (*App) Density

func (a *App) Density() float32

Density returns the device density in force, which is always an integer of at least one. See App.SetDensity.

func (*App) Diagnostics

func (a *App) Diagnostics() Diagnostics

Diagnostics returns a snapshot of the counters as of the end of the last Update or Paint.

It is safe to call from any goroutine, which is the whole point: it is the out of band measurement source of the project plan, section 15, and a measurement harness that has to run on the UI executor is not out of band. It never blocks the frame path for longer than a struct copy.

func (*App) DismissSoftKeyboard

func (a *App) DismissSoftKeyboard() bool

DismissSoftKeyboard puts gift's on-screen keyboard away by blurring whatever asked for it, and reports whether there was a keyboard up. It is [App.dismissSoftKeyboard] as an application can reach it — a kiosk with a "done" affordance of its own calls it — and it is what the escape key and the keyboard's own dismiss key do.

func (*App) DrainPosts

func (a *App) DrainPosts()

DrainPosts runs everything posted since the last update, outside a frame.

An application needs it exactly once: after the last frame and after closing the asset pipeline. [asset.Config.Deliver] requires that every closure it is handed eventually runs, including the ones produced during Close, because a delivered thumbnail releases its reference *inside* the closure — an executor that drops one holds those pixels against the pixel budget until the process exits. The frame loop has stopped by then, so nothing would run them.

defer func() {
    pipe.Close()
    app.DrainPosts()
}()

It must be called from the goroutine that ran the frame loop, like everything else that touches state. Calling it during a frame is pointless but harmless: App.Update drains the same queue.

func (*App) Focus

func (a *App) Focus() (NodeRef, bool)

Focus returns the node that currently holds the keyboard focus, and whether anything does.

func (*App) FocusVisible

func (a *App) FocusVisible() bool

FocusVisible reports whether the current focus should be shown as a ring, which is the case when it was installed by the keyboard rather than by a pointer; see Interaction.FocusVisible.

It answers false when nothing is focused, because a ring nobody owns cannot be visible.

func (*App) HitTest

func (a *App) HitTest(p geom.Point) (NodeRef, bool)

HitTest returns the topmost interactive node at the device space point p, and whether there is one.

It is exported for tests, for diagnostics and for an application that wants to answer the question itself; the dispatcher uses the same walk for every pointer event.

func (*App) Images

func (a *App) Images() render.Images

Images returns the service installed by App.SetImages, or nil.

func (*App) Invalidate

func (a *App) Invalidate()

Invalidate forces a rebuild of the root component in the next update. It is the blunt instrument for tests, for a resize and for external changes that gift cannot observe.

Like every other entry point that touches tree state, it asserts under the giftdebug tag that it was called from the UI goroutine; see [App.assertUIGoroutine]. It is worth naming why a *core invalidation* entry point carries that cost, because the check parses the goroutine header and costs roughly a microsecond. Invalidate is not in the frame path: it is called once per external change, and the release build has no check at all. Against that, it is the natural thing for a goroutine watching the desktop appearance or a file to call, it is the second half of [ui.SetTheme], and the two flag writes it performs are exactly the racy state the guard exists for. A -race run only reports it when another goroutine happens to touch the same flags at the same moment; this reports it the first time.

func (*App) InvalidateAll

func (a *App) InvalidateAll()

InvalidateAll forces a rebuild of *every* mounted component instance in the next update, not only of the root.

Why [App.Invalidate] is not enough, and when that matters

Invalidate marks the root scope. A plain Component has no rebuild boundary — its props live in a closure gift cannot compare — so a root rebuild does reach every plain component under it. A Memo is exactly the thing that stops it: [App.updateChild] returns without calling the component function when the props are equal and no state the instance read has fired, which is the boundary section 6 of the project plan asks for. A change that is neither a prop nor a state, but which nevertheless decides what the subtree looks like, therefore does not reach it.

The change that is like that today is the colour theme. Colours are resolved during build and stored literally in the retained node — that is what keeps the frame path free of theme lookups — so a subtree which is not rebuilt keeps the palette it was built with. The result on a screen is not subtle: the background turns light while every memoised card stays dark, and the text on it becomes unreadable. [ui.SetTheme] calls this method for that reason.

The cost, stated plainly

One walk of the retained tree, which is O(nodes) and allocates nothing, plus a rebuild of every component instance in the next update — which is the most expensive update the application can have, and is exactly what "every colour on the screen is resolved again" means. It is the right price for a theme switch, which happens when a person presses a button, and it would be the wrong price for anything per frame. Nothing in gift calls it from the frame path, and nothing should.

The blunter alternatives were weighed and rejected. Making the theme a State that every widget reads would put a dependency registration on every colour lookup, in ui, which has no Context to register against; a generation counter compared during reconciliation would have to be consulted for every node in the tree, once per update, to save work in the one update per hour where a theme changes. This is the cheap version of the honest answer: a change nobody can attribute invalidates everything that could have consumed it.

Like App.Invalidate it asserts under the giftdebug tag that it was called from the UI goroutine, and for the same reason.

func (*App) IsScrollable

func (a *App) IsScrollable(r NodeRef) bool

IsScrollable reports whether r is a scroll container.

func (*App) KeyDown

func (a *App) KeyDown(k Key, mods Mods)

KeyDown reports a key press.

The event goes to the focused node and bubbles to its ancestors. Tab and shift-tab move the focus when nothing consumed them, which is why a focused node that wants to handle tab itself simply returns true for it.

A key in the repeat set arms the repeat; see KeyRepeatDelay. The backend calls this once per physical press, not once per tick the key is held: the repeat is gift's and edge detection stays the backend's.

func (*App) KeyUp

func (a *App) KeyUp(k Key, mods Mods)

KeyUp reports a key release, delivered like App.KeyDown but never acted on by gift itself beyond ending a repeat.

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger returns the logger passed in Options, or nil. It is never slog.Default.

func (*App) MoveFocus

func (a *App) MoveFocus(forward bool) bool

MoveFocus moves the keyboard focus to the next focusable node in document order, or to the previous one when forward is false, and reports whether the focus changed.

Order

Document order is the pre order traversal of the retained tree: a node comes before its children, and a child before its later siblings. That is the logical tree the application wrote, not the order things happen to be drawn in and not the geometry on screen. The project plan, section 10, makes the same choice for the gallery — "Selektion und Tastaturnavigation folgen stabilen IDs und der logischen Collection-Reihenfolge" — and there is no reason for the general case to disagree with it.

The traversal wraps around, and a node that declined to be focusable, or is disabled, is skipped rather than focused and immediately passed over.

func (*App) NeedsPaint

func (a *App) NeedsPaint() bool

NeedsPaint reports whether the tree changed since the last Paint.

By default it is informational: the ebiten backend redraws the whole screen every frame, because Ebitengine clears it; see the project plan, section 6. With [ebiten.Config.DrawOnDemand] it becomes load bearing — a frame is drawn only while this, or the renderer, says so. Every change that shows on the screen must therefore mark it: through a state write, a layout, a hover or press, or PaintContext.Animate for anything that moves with the clock.

func (*App) NodeBounds

func (a *App) NodeBounds(r NodeRef) geom.Rect

NodeBounds returns the absolute rectangle of the referenced node as of the last layout pass, or the zero Rect for a stale reference.

func (*App) NodeChildren

func (a *App) NodeChildren(r NodeRef, dst []NodeRef) []NodeRef

NodeChildren appends the children of r to dst, in order, and returns the extended slice.

It appends rather than allocating so that a traversal can reuse one buffer, which is what keeps a repeated tree walk — a test that runs a selector after every frame, say — from generating garbage. Pass nil for a fresh slice.

func (*App) NodeDeviceBounds

func (a *App) NodeDeviceBounds(r NodeRef) geom.Rect

NodeDeviceBounds returns the rectangle of the node in device space: its layout bounds mapped through the transforms of its ancestors, which today means through the scroll offsets above it.

App.NodeBounds is the layout answer and this is the on screen answer. They differ exactly when something above the node is scrolled or transformed, and the difference is what makes a test that aims at a node inside a scroll container hit the node rather than the place it used to be.

func (*App) NodeFocusable

func (a *App) NodeFocusable(r NodeRef) bool

NodeFocusable reports whether the node may currently hold the keyboard focus, that is whether it would be visited by App.MoveFocus.

func (*App) NodeInteraction

func (a *App) NodeInteraction(r NodeRef) Interaction

NodeInteraction returns the hover, press, focus and disabled state of the referenced node. It is how a test asserts that a button looks pressed without going through the display list.

func (*App) NodeInteractive

func (a *App) NodeInteractive(r NodeRef) bool

NodeInteractive reports whether the node takes part in hit testing, that is whether its element carried an Interactor.

A disabled node is still interactive by this definition: it is a hit target that swallows clicks, which is exactly what Element.Disabled promises. Ask App.NodeInteraction for the Disabled bit.

func (*App) NodeKey

func (a *App) NodeKey(r NodeRef) string

NodeKey returns the reconciliation key of the referenced node, or the empty string for an unkeyed or stale reference. It is meant for diagnostics.

func (*App) NodeLabel

func (a *App) NodeLabel(r NodeRef) string

NodeLabel returns Element.Label of the node, or the empty string for a node that declared none and for a stale reference.

This is the string a test's ByText selector matches against and the string an accessibility bridge would read out. It is not the text that was drawn: a node that draws its label as glyphs sets both from the same value, but nothing enforces that, and nothing can — a view may legitimately say more than it shows.

func (*App) NodeOverflow

func (a *App) NodeOverflow(r NodeRef) (geom.Size, bool)

NodeOverflow returns by how much the content of r exceeds the size r reported, and whether it overflows at all.

It is the per node half of Diagnostics.OverflowNodes and Diagnostics.OverflowExtent: those two say how much overflow the tree carries, this says which node carries it. Without it the counters are a smoke alarm without a room number, and the only way to get the room number was a build with the giftdebug tag reading a log line — which an automation client on the far end of an HTTP connection cannot do.

Like the counters it describes the retained tree and not the last pass: a node that overflowed and was then skipped by the layout cache still reports its overflow. A stale reference reports no overflow.

func (*App) NodeParent

func (a *App) NodeParent(r NodeRef) NodeRef

NodeParent returns the parent of r, or the zero NodeRef for the root and for a stale reference.

func (*App) NodeScroller

func (a *App) NodeScroller(r NodeRef) (NodeRef, bool)

NodeScroller returns the nearest scroll container at or above r, and whether there is one. It is how a caller that holds a node asks "what would scroll if I scrolled here".

func (*App) NodeType

func (a *App) NodeType(r NodeRef) TypeID

NodeType returns the TypeID of the view the node was built from, or the zero TypeID for a stale reference. Pass it to TypeName for the name the view type registered under, such as "ui.Button".

func (*App) NodeValid

func (a *App) NodeValid(r NodeRef) bool

NodeValid reports whether r still refers to a live node. A reference goes stale when its node is unmounted, and the slot may then be reused by an unrelated node — the generation in the handle is what keeps the two apart.

func (*App) NodeValue

func (a *App) NodeValue(r NodeRef) string

NodeValue returns the value the node's control reports, or the empty string for a node whose interactor is not a ValueReporter and for a stale reference.

Like App.NodeLabel it is for tests and for automation, never for the frame path: it calls into the control, and nothing caches the answer.

func (*App) NodeVisibleBounds

func (a *App) NodeVisibleBounds(r NodeRef) (geom.Rect, bool)

NodeVisibleBounds returns the part of the node that survives the clips of its ancestors, and whether any of it does.

It is App.NodeDeviceBounds intersected with the inherited clip. A node scrolled out of its viewport returns false, which is the question "is this on screen at all" answered without a GPU. It is not an occlusion test: a node covered by an opaque sibling is still visible by this definition, and App.HitTest is the verb for that question.

A node inside a subtree that declared Element.Hidden returns false. That is the same question with the same answer: an inactive tab is mounted, keeps its layout bounds — App.NodeBounds still reports them — and is not on the screen.

func (*App) Paint

func (a *App) Paint() *render.List

Paint produces the display list of this frame.

The returned list is borrowed: it is valid until the next call to Paint, which resets and refills it. Copy what has to outlive the frame.

Building or laying out during Paint is a contract violation. State writes from a painter panic, and so does a nested Update. Calling Paint from inside Update is rejected as well: Ebitengine drives Update and Draw separately and gift relies on that separation, so a paint nested in an update means the backend is wired up wrongly.

func (*App) PointerCancel

func (a *App) PointerCancel(id PointerID)

PointerCancel ends the pointer id without a release.

It is the right call when the window loses focus, when the platform takes a gesture over or when a touch is lost. The capturing node hears EventPointerCancel and must not treat it as an activation.

func (*App) PointerDown

func (a *App) PointerDown(id PointerID, kind PointerKind, pos geom.Point)

PointerDown reports a press of the pointer id at pos.

The node that is hit captures the pointer: every move, the release and a cancel go to it and to nobody else, until the pointer comes up. That is what makes "press on the button, release next to it" not an activation while still telling the button that the press ended.

A touch that arrives while another touch is already down is accepted only when the node it hits, or an ancestor of that node, declared Element.MultiTouch. Otherwise it is counted in Diagnostics.DiscardedTouches and ignored, which is all the project plan, section 7, originally allowed and what a view that never asked for more still gets.

func (*App) PointerDownAt

func (a *App) PointerDownAt(id PointerID, kind PointerKind, pos geom.Point, t time.Duration)

PointerDownAt is App.PointerDown for a press the backend measured at t; see App.PointerMoveAt.

func (*App) PointerMove

func (a *App) PointerMove(id PointerID, kind PointerKind, pos geom.Point)

PointerMove reports that the pointer id moved to pos, in device space.

For the mouse this also maintains the hover state: the node under the cursor gets EventPointerEnter, the one it left gets EventPointerLeave and Interaction.Hover follows. While a pointer is *down* the hover set is frozen, because a drag that passes over other buttons must not light them up. That holds whether or not anything captured the press: a drag that started on empty space, and a drag whose captured node was unmounted underneath it, both leave the hover alone until the release.

func (*App) PointerMoveAt

func (a *App) PointerMoveAt(id PointerID, kind PointerKind, pos geom.Point, t time.Duration)

PointerMoveAt is App.PointerMove for a sample the backend measured at t, on the clock of App.BeginInput, and Event.Time is then t rather than the tick time.

Why the time of a sample matters

A velocity is a distance over a time, and the time that matters is when the panel measured the finger, not when the tick that delivered it ran. A tick that delivers samples of a 100 Hz digitiser at 60 Hz sees one sample in some ticks and two in others; stamped with the tick time, the same steady finger is measured as moving at alternately half and twice its speed, and a fling starts at whichever the last tick happened to show. Android and iOS hand every sample to the application with the time the hardware reported, and a backend that knows that time passes it here. A t in the future is read as now, and one before the pointer's previous sample as that sample's time.

func (*App) PointerUp

func (a *App) PointerUp(id PointerID, kind PointerKind, pos geom.Point)

PointerUp reports a release of the pointer id at pos.

The event goes to the capturing node with Event.Inside telling it whether the release happened over it. A touch pointer stops existing here; the mouse keeps its position and its hover.

It also ends the gesture, which for the mouse is not the same thing as ceasing to exist: see [pointer.endGesture] for what that costs when it is forgotten.

func (*App) PointerUpAt

func (a *App) PointerUpAt(id PointerID, kind PointerKind, pos geom.Point, t time.Duration)

PointerUpAt is App.PointerUp for a release the backend measured at t; see App.PointerMoveAt.

func (*App) PointerWheel

func (a *App) PointerWheel(pos geom.Point, delta geom.Point)

PointerWheel reports a scroll of delta at pos.

The event goes to the node under pos and bubbles up to the root until an interactor handles it, because the node that scrolls is an ancestor of the leaf the cursor happens to be over.

A scroll container consumes it when it can still move in the requested direction and lets it bubble when it cannot; see the overscroll chaining rule on gift's scroll handler. An event that reaches the root unconsumed is simply dropped.

func (*App) PointerWheelPhase

func (a *App) PointerWheelPhase(pos geom.Point, delta geom.Point, phase WheelPhase)

PointerWheelPhase is App.PointerWheel for a platform that reports the stages of a trackpad gesture, as macOS does in NSEvent.phase and NSEvent.momentumPhase. A delta with a phase is precise; see Event.Precise.

Why the stage matters

Because the momentum of macOS is a stream of ordinary deltas that goes on for a second or more after the fingers left, and a scroll container that cannot tell it from the fingers stretches past its end under it as under a finger pulling all that way. AppKit instead lets content that the momentum carries into an end bounce off it with the speed it had, and drops the rest; and it springs a stretch back the moment the fingers that made it lift, not once the deltas stop. Both need the stage, and GLFW, and with it Ebitengine, drops it; gift's Ebitengine backend reads it from AppKit itself.

func (*App) Post

func (a *App) Post(fn func())

Post queues fn to run on the UI executor at the beginning of the next App.Update.

This is the only method besides App.Diagnostics that may be called from another goroutine. It is how an asynchronous result reaches the user interface: the worker never touches state, it posts, and the posted closure runs where state is allowed to be touched. Callbacks are never invoked while a lock is held; see the project plan, section 5.

Posting from inside a posted closure is allowed and defers the new closure to the following update, so a feedback loop cannot stall a frame.

func (*App) Root

func (a *App) Root() NodeRef

Root returns the reference to the root node, which is the component instance mounted from Options.Root.

It is the entry point of every traversal. The tree it heads is the retained tree, not the view tree: a component appears as one node of type "gift.Component" with the view it returned as its only child.

func (*App) ScrollBy

func (a *App) ScrollBy(r NodeRef, d float64) bool

ScrollBy moves the scroll container r by d document units and reports whether the offset changed.

func (*App) ScrollInfo

func (a *App) ScrollInfo(r NodeRef) (ScrollInfo, bool)

ScrollInfo returns the state of the scroll container r, and false when r is stale or is not a scroll container.

func (*App) ScrollIntoView

func (a *App) ScrollIntoView(r NodeRef) bool

ScrollIntoView adjusts every scroll container above r so that r is inside the visible part of each of them, and reports whether anything moved.

It moves as little as possible: a node that is already visible is left alone, a node above the viewport is brought to the leading edge and a node below it to the trailing edge. A node larger than the viewport is aligned with the leading edge, because that is where its beginning is.

The containers are adjusted from the innermost outwards, which is the order in which the work is independent: moving an inner container changes where r sits inside an outer one, while moving an outer container moves the inner viewport and r together.

It is a jump; see App.ScrollTo for why.

A node inside a subtree that declared Element.Hidden is not revealed and the answer is false. It is the same answer App.NodeVisibleBounds gives about the same node, and the two used to disagree in the same frame: one said "not on screen", the other said "done". Scrolling a container in an inactive tab to show something nobody can see is work with no result, and worse, it destroys the offset the tab was keeping — which is the whole reason an inactive tab stays mounted. The case is ordinary rather than exotic: ui.TextField calls EventContext.ScrollIntoView from its focus handler, and an application may call this one from a background result.

func (*App) ScrollTo

func (a *App) ScrollTo(r NodeRef, off float64) bool

ScrollTo moves the scroll container r to the document offset off, clamped to its bounds, and reports whether the offset changed.

It is a jump, not an animation, and so is App.ScrollIntoView. Two reasons, and neither is laziness. An animated scroll is presentation state that changes every frame, so every caller — including every test — would have to pump frames until it settled before asserting anything, and the natural mistake is to assert too early and get a flaky test. And gift has no animation curves yet: the project plan, section 14, used to exclude them and its "Aufgehobene Ausschluesse" has since lifted that, but nothing has been built, so the only curve available today is the fling this package already implements for a gesture. An application that wants an animated jump drives App.ScrollTo from its own tick and owns the curve, which is one loop and no new concept. When the animation primitives arrive, the first reason above still stands and this is expected to stay a jump, with an animated variant next to it rather than in place of it.

Any running fling is cancelled: a programmatic jump and a fling disagreeing about where the content should be is the one thing worse than either.

func (*App) SetDensity

func (a *App) SetDensity(factor float64) float32

SetDensity sets the device density, that is how many physical pixels one logical pixel of the layout covers along each axis.

factor is the raw value of the platform — on Ebitengine ebiten.Monitor().DeviceScaleFactor() — and is rounded by RoundDensity here, so that a caller never has to know the rounding rule. It reports the density that is now in force.

What changes and what does not

Nothing about layout. The application keeps declaring sizes, paddings and font sizes in logical pixels, a Layouter keeps measuring in them, and App.Update keeps being handed the viewport in them. What changes is the *presentation*: App.Paint pushes a scale of the density at the root of the display list, so every operation reaches the backend with a transform that maps its local rectangle onto physical pixels. The backend already bakes radius, stroke width, the antialiasing pad, the shadow sigma and the glass refraction out of that transform, so shapes become sharp rather than larger.

Input and hit testing are deliberately *not* scaled. Pointer positions and App.HitTest stay in logical pixels, which is the space the bounds a test or an application reads are in. The backend divides the physical cursor position by the density once, at the boundary, and everything above it — drag slop, fling velocity, scroll offsets — keeps one unit system. The alternative, carrying the density into the input path as well, would have made a drag of a hundred physical pixels scroll twice as far on a 2x display than on a 1x one.

A density of 1 is the identity in every one of these places: the root transform is not pushed at all, so a 1x frame produces exactly the display list it produced before densities existed.

func (*App) SetImages

func (a *App) SetImages(im render.Images)

SetImages installs the image resource service a painter reaches through PaintContext.Images.

It is called once by the backend, before the first frame. gift itself never uploads anything and never looks at the service; it only carries it from the backend, which owns the textures, to the views, which own the keys — because the project plan, section 3, has ui depending on neither backend/ebiten nor a GPU object, and there is no other route between the two.

A nil service is the headless case and is normal: every view that draws a picture falls back to its placeholder, which is what a layout test and a gifttest harness without a graphics context want.

func (*App) SetModifiers

func (a *App) SetModifiers(m Mods)

SetModifiers records which modifier keys are currently held. Every event dispatched afterwards carries them.

func (*App) SetVideos

func (a *App) SetVideos(v render.Videos, out render.AudioOutput)

SetVideos installs the video picture service and the sound device a painter reaches through PaintContext.Videos and PaintContext.Audio.

It is App.SetImages for moving pictures and their sound, carried the same way and for the same reason: ui/videoplayer draws and plays, the backend owns the textures and the audio device, and gift is the only route between the two. Either may be nil, which is again the headless case: a video then shows no picture, and its clock runs on the wall clock without sound.

func (*App) SoftKeyboardRequested

func (a *App) SoftKeyboardRequested() bool

SoftKeyboardRequested reports whether the focused node asked for an on-screen keyboard; see EventContext.RequestSoftKeyboard.

func (*App) TypeRune

func (a *App) TypeRune(r rune)

TypeRune reports one character the user typed, as the platform's keyboard layout produced it.

It is the counterpart of App.KeyDown and deliberately not a variant of it. A key press says *which key*; this says *which character*, and on every keyboard outside the United States the two are different questions. The backend gets the answer from the platform — Ebitengine's AppendInputChars, whose own documentation calls it "the environment's locale-dependent translation of keyboard input to Unicode characters" — and hands it here one rune at a time. The event is EventRune; it goes to the focused node and bubbles exactly like a key event.

No modifier parameter, on purpose

App.KeyDown takes the modifiers because a key press without them is ambiguous. A rune is the *result* of the modifiers: shift-a is already 'A', and AltGr-q is already '@'. Passing them again would invite a handler to apply them a second time. The modifiers of the current tick are still on the event, from App.SetModifiers, for the one honest use — telling a typed character from the tail of a shortcut — and gift itself does not filter on them, because the platform already does: Ebitengine's character callback "skips the characters that are produced with the modifier combinations the platform treats as shortcuts".

What this is not

It is not an IME entry point. There is no composition, no preedit string and no candidate window; the project plan, section 14, excludes CJK composition and this does not sneak it in. What it does cover is everything a Latin keyboard needs and the previous design dropped on the floor: umlauts, accents, AltGr and dead keys are not an IME, they are the ordinary translation of key presses to characters, and they arrive here fully resolved — a dead key produces no rune of its own and the following key produces the composed one.

func (*App) Update

func (a *App) Update(viewport geom.Size) error

Update runs build, reconciliation and layout. The backend calls it once per Ebitengine update.

Build and layout happen here and nowhere else. An update that finds no dirty scope and no layout invalidation does nothing at all and, in particular, allocates nothing.

The error return is part of the Ebitengine update contract and is currently always nil. It exists so that a fatal application error can travel out of the frame loop later without changing every call site.

func (*App) Videos

func (a *App) Videos() render.Videos

Videos returns the service installed by App.SetVideos, or nil.

func (*App) Viewport

func (a *App) Viewport() geom.Size

Viewport returns the logical size the application was last laid out at, that is the size handed to the most recent App.Update.

It is the denominator of every coordinate this package hands out: node bounds, hit test points and pointer positions are all in this space, and a caller outside the frame loop — a diagnostic dump or the automation interface of the gift/auto package — otherwise has no way to relate them to the pixels of a framebuffer, which are this times App.Density.

It reports the zero size before the first update.

type Binding

type Binding[T comparable] struct {
	// contains filtered or unexported fields
}

Binding connects reading and writing of one state for a control, without giving the control ownership of the state.

The zero Binding is unusable; obtain one from State.Binding.

func (Binding[T]) Get

func (b Binding[T]) Get() T

Get returns the current value. If a build is in progress, the building scope is registered as a dependency, so a control that renders a bound value is rebuilt when the value changes.

func (Binding[T]) IsZero

func (b Binding[T]) IsZero() bool

IsZero reports whether the binding is the unusable zero value.

func (Binding[T]) Set

func (b Binding[T]) Set(v T)

Set stores v, with the same semantics as State.Set.

type BuildContext

type BuildContext struct {
	// contains filtered or unexported fields
}

BuildContext is passed to View.Build.

It is owned and reused by the App and is valid only for the duration of the call. Keeping it is a bug. It exists so that build time services can be added without changing the view contract; the one it exposes so far is BuildContext.SoftKeyboardRequested.

func (*BuildContext) SoftKeyboardRequested

func (b *BuildContext) SoftKeyboardRequested() bool

SoftKeyboardRequested reports whether the focused node asked for an on-screen keyboard. It is App.SoftKeyboardRequested read from inside a build, which is where a view that draws one has to decide.

A nil BuildContext answers false. Views in this project take the context by pointer and most of them ignore it entirely, so a unit test that calls Build directly passes nil; answering false there is the same as "nobody is typing" and is strictly better than a nil dereference inside a widget.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context is the interface of a component function to the runtime.

It is owned by the scope of the component instance and is valid only for the duration of the build that received it. Keeping it and using it later, for example from an event handler or a goroutine, is a programming error and is rejected.

func (*Context) Key

func (c *Context) Key() string

Key returns the key this component instance was mounted under. It is meant for diagnostics, not for building identity on top of it.

func (*Context) OnUnmount

func (c *Context) OnUnmount(name string, fn func())

OnUnmount registers fn to run when this component instance is unmounted: taken out of the tree, with its state, because its parent stopped building it or built something else in its place. It is where a component lets go of what it opened and keeps in its state — a video player, a file, a subscription — which nothing else would close.

Functions are kept by name, like states: a build that registers name again replaces the function, so a component may call this on every build with a closure over its current values. They run on the UI goroutine during the update that unmounts the instance, children before parents, and before the state is dropped, so a function may still read the states it closes over. An instance that is merely hidden, an inactive tab say, is not unmounted.

func (*Context) Path

func (c *Context) Path() string

Path returns the slash separated path of component keys from the root to this component instance. It is meant for diagnostics and appears in the panic messages of this package.

func (*Context) Read

func (c *Context) Read[T comparable](s *State[T]) T

Read returns the current value of s and registers the state as a dependency of the component instance currently being built.

The dependency set is replaced after every successful build, not accumulated: a state that is no longer read stops invalidating this component.

func (*Context) Request

func (c *Context) Request() Token

Request starts a new asynchronous request for this component instance and returns the Token that identifies it.

Every call invalidates the tokens of all earlier requests of this instance, so the newest request always wins and the answers of the ones it replaced are dropped when they arrive.

func (*Context) State

func (c *Context) State[T comparable](name string, initial T) *State[T]

State returns the typed state with the given name, creating it with the value initial on first use.

The name is local to the component instance: two instances of the same component function, and two different component functions, may use the same name without interfering. The initial value is evaluated by the caller on every build but only used on the first one.

Using the same name with a different element type in the same component instance panics. That situation always means the identity of the component instance is wrong, and silently starting over with a fresh value would hide it.

This is a generic method on a concrete type, which is what Go 1.27 makes possible. It keeps the value typed all the way through, without a type assertion at the call site and without boxing the value on every read.

type ControlState

type ControlState struct {
	// Grabbed reports whether this control has taken hold of a pointer and is
	// dragging something.
	Grabbed bool

	// Grab is the offset, along the control's own axis and in the device
	// space of [Event.Pos], between the point the gesture started at and the
	// leading edge of the thing being dragged.
	//
	// It is written once, when the grab is taken, and read on every move. It
	// is never recomputed from the control's value; see the type
	// documentation for what happens when it is.
	Grab float32

	// Target is the value the control is animating towards, normally in
	// [0, 1]. It is the value the last build declared.
	Target float32

	// From is the value the running animation started at, and Start the
	// timestamp it started at, on the clock [App.BeginInput] is given.
	From  float32
	Start time.Duration

	// At is where the pointer touches the control, relative to the top left
	// of its bounds, in the space of [Event.Pos]. A control that lights up
	// under the finger, as a pressed button does, reads it; see
	// ui.ButtonView.Lift.
	At geom.Point

	// Armed reports whether Target has ever been written.
	//
	// It separates "the control is at zero" from "the control has not been
	// built yet", and without it every control that is born switched on would
	// animate from off to on in the first frames of the application.
	Armed bool

	// Entry is the editing state of a control that is typed into one part at
	// a time; see [EntryState]. Every other control leaves it zero.
	Entry EntryState
}

ControlState is the retained presentation state of one control: the gesture it is in the middle of, and the animation it is in the middle of.

Why it lives here and not in the view or in the node object

It is the same argument ScrollIndicatorState makes, applied to a widget that rebuilds far more often than a scroll container does. A control that reports its value through a callback causes a rebuild on every step of the gesture *by construction*: dragging the knob of a ui.Slider is a continuous stream of rebuilds, and a rebuild constructs a fresh layouter, painter and interactor object and installs it. Anything the gesture remembers in that object is therefore gone between two moves of the same finger.

The consequence of losing it is worse than losing the gesture. The pointer capture and the Event.Dragged flag belong to the dispatcher and survive, so the next move still arrives — at a handler whose grab offset has been reset to zero. The knob then jumps to the finger, and if the offset is instead recomputed from the *current* value the drag inverts, which is the defect review gate 7 of this project found in the scroll bar thumb. So the grab offset is recorded once, at the press, in the one place that outlives a build.

Why the fields are this generic

Because the alternative is a payload field per widget in gift's own node data, and gift must not grow a field every time package ui gains a control. They are named for what a control does with them rather than for one widget: a grab is a pointer gesture in progress, and a phase is a number being interpolated towards a target. That is deliberately the same trade ScrollIndicatorState makes, which carries Grabbed and Grab for exactly one caller.

The zero value is the state of a control that has never been touched: no grab, no animation, and ControlState.Armed false so that the first value a control sees is adopted without animating towards it from zero.

type Diagnostics

type Diagnostics struct {
	// Frames counts the calls to App.Paint.
	Frames uint64
	// Updates counts the calls to App.Update.
	//
	// It exists because Frames alone cannot tell "the application is wedged"
	// from "nobody is looking at it". Ebitengine calls Update for a window
	// that is not visible and does not call Draw for one, so a program
	// launched into the background has Updates climbing at the tick rate
	// while Frames stands still — which looks exactly like a frozen frame
	// path if Frames is the only number on the table. Every automation
	// answer therefore carries both; see the gift/auto package.
	Updates uint64
	// Builds counts the component scopes that were rebuilt.
	Builds uint64
	// Layouts counts the nodes whose layouter actually ran. A node that was
	// skipped because it is clean and its constraints did not change is not
	// counted, which is what makes partial relayout observable from a test.
	Layouts uint64
	// PaintedNodes counts the nodes whose painter actually ran.
	//
	// A node without background, border and clip supplies no painter at all
	// and gift descends straight into its children; such a node is visited
	// but not counted. That difference is the whole point of the counter: it
	// is the only externally visible evidence that the nil painter fast path
	// is still in place.
	PaintedNodes uint64
	// PaintedOps counts the drawing operations emitted.
	PaintedOps uint64
	// LiveNodes is the number of nodes currently in the retained tree.
	LiveNodes uint64
	// LiveScopes is the number of mounted component instances.
	LiveScopes uint64

	// OverflowNodes is the number of nodes in the retained tree whose
	// content does not fit into the size they reported.
	//
	// It answers "is any container lying about its size" with a single
	// integer, which is the question the project plan, section 7,
	// "Overflow-Modell", makes a first class one. Zero is the expected
	// value of a healthy scene, so a test can assert on it directly.
	//
	// It describes the tree, not the last pass: a node that overflowed and
	// was then skipped by the layout cache still counts. A per pass tally
	// would drop to zero as soon as partial relayout started working, which
	// is precisely when it would be needed.
	OverflowNodes uint64

	// HitTests counts the hit tests performed, that is roughly the number of
	// pointer events that had to find a target.
	HitTests uint64
	// InputEvents counts the calls into an [Interactor], including the ones
	// that happened while an event bubbled towards the root.
	InputEvents uint64
	// Scrolls counts the times a scroll container's offset actually
	// changed: a wheel notch, a drag step, a fling tick or a programmatic
	// jump. It moves while Builds and Layouts do not, which is the whole
	// claim of the scroll fast path stated as two numbers.
	Scrolls uint64
	// DiscardedTouches counts the additional fingers that were recognised
	// and dropped: every finger that came down while another was down and
	// did not land in a node that declared [Element.MultiTouch], and every
	// finger beyond [MaxTouches]. A counter is how "it was dropped on
	// purpose" is told apart from "it was never seen".
	DiscardedTouches uint64

	// RunesTyped counts the characters that entered through [App.TypeRune],
	// whether or not anything was focused to receive them.
	//
	// It is the counter that answers the question the rune channel exists
	// for: "does the keyboard reach the framework at all". Before that
	// channel the answer was no and nothing said so. A zero here while the
	// user types means the backend is at fault; a non zero here with nothing
	// happening on screen means the view is.
	RunesTyped uint64
	// KeyRepeats counts the synthetic key presses gift produced from a held
	// key; see [KeyRepeatDelay]. [Event.Repeat] is set on each of them, so
	// this counter is how a test tells a repeat from a second real press.
	KeyRepeats uint64

	// Animations is the number of repaint enrolments in force: the
	// [EventContext.Animate] windows, the kinetic flings and the scroll
	// indicator lingers, plus the [TransitionSpec] movements, which share
	// the first list.
	//
	// It exists because "the picture stopped changing" was, until it did,
	// the only external evidence that anything was moving at all, and a
	// transition shorter than the jitter of a screenshot over HTTP is
	// indistinguishable from no transition by that evidence. This number is
	// not a picture: it is non zero for exactly as long as something has
	// asked to be redrawn, so an automated driver can wait for motion to
	// start and wait for it to stop instead of guessing frame counts. See
	// the gift/auto package, which serves it as "animations".
	Animations uint64

	// Animating reports whether Animations is non zero. It is the same fact
	// as a bool because that is the question a test asks — "has it gone
	// idle" — and a test that spells it as a comparison against zero is a
	// test that has to be changed when a fourth enrolment kind appears.
	Animating bool

	// OverflowExtent is the sum over those nodes of their horizontal plus
	// vertical overflow, in logical pixels.
	//
	// The count alone cannot distinguish a row that is half a pixel too tall
	// from one that is eight hundred pixels too tall. The extent is what
	// makes "it got better" and "it got worse" different numbers, which is
	// what a remediation needs; the count is what a regression test asserts
	// on. Both are cheap, so both are here.
	OverflowExtent float32
}

Diagnostics is a snapshot of the frame path counters.

The frame path writes nothing but these plain numbers: no formatting, no interface boxing, no locks. Everything that would be a log line in a less performance sensitive toolkit is a counter here. The snapshot is taken out of band, by the application or by a low rate diagnostic sink; see the project plan, section 15.

type Element

type Element struct {
	// Key is the optional stable identity of this element among its
	// siblings. Siblings with keys are matched by key, siblings without keys
	// are matched by position. A key only has to be unique among siblings,
	// not globally.
	Key string

	// Label is the human readable text this node stands for.
	//
	// It is a *semantic* field, not a visual one: nothing in the layout or
	// the paint path reads it, and setting it changes no pixel. A view sets
	// it to whatever a person would call the node — [ui.Text] sets it to its
	// string, a [ui.Button] with an icon sets it to the name of the command
	// — so that something outside the frame path can find a node by what it
	// says rather than by where it happens to sit.
	//
	// Today the only consumer is the test harness in package gifttest, whose
	// ByText selector is the whole reason coordinate free UI tests are
	// possible. It is deliberately declared here on Element rather than as a
	// private field of one ui view, because the next two consumers are known
	// in advance and want exactly the same string: a button that wants an
	// accessible name distinct from its label view, and the accessibility
	// bridge the project plan, section 14, excludes from the MVP but does not
	// rule out forever.
	//
	// # Cost
	//
	// A string header, copied from the element onto the node during a build
	// and never touched again. A build already copies eight other fields out
	// of the same struct; this is a ninth, and it allocates nothing — the
	// bytes belong to the view, which owns them for as long as the node does.
	// Update, layout and paint never read it. See the allocation contract of
	// the project plan, section 11, and TestLabelCostsNothingPerFrame.
	Label string

	// Layouter measures this node and places its children. A nil Layouter
	// means the node has zero size and its children are neither measured nor
	// placed.
	Layouter Layouter

	// Painter emits the drawing operations of this node.
	//
	// A nil Painter draws nothing of its own and paints all children in
	// order. That is the fast path for a purely structural container: a
	// stack without background, border or clip has nothing to draw, and
	// making it invisible together with its whole subtree would be a trap
	// whose only symptom is a blank screen.
	//
	// A non nil Painter is fully responsible for its subtree: children are
	// drawn only where it calls [PaintContext.PaintChildren] or
	// [PaintContext.PaintChild], which is what makes the background,
	// content and border ordering explicit.
	Painter Painter

	// Children are the child views, in order.
	//
	// Ownership passes to gift on return. The caller must neither modify nor
	// reuse the slice afterwards. Whoever hands over a reused buffer must
	// copy first. A violation is detected in builds with the giftdebug tag;
	// see the project plan, section 4.
	Children []View

	// Flex is the flexibility of this element along the main axis of an
	// enclosing stack. Zero, the default, means inflexible: the element is
	// measured with an unbounded main axis and takes what it asks for, before
	// any space is shared out. A positive value means the element takes a
	// share of the space the inflexible siblings left, proportional to it.
	//
	// It is a plain field rather than an interface or a type assertion in
	// the layout path on purpose: a stack recognises a [ui.Spacer] by
	// reading one float from the retained node, see
	// [LayoutContext.ChildFlex]. A layouter that is not a stack ignores it.
	Flex float32

	// Interactor makes this node a hit target for input. A nil Interactor,
	// the default, makes the node transparent to input without affecting its
	// children; see [Interactor] for why that polarity is the safe one.
	Interactor Interactor

	// Focusable declares that this node may hold the keyboard focus and
	// therefore takes part in the tab order. It is only honoured together
	// with a non nil Interactor: focus exists to deliver key events, and a
	// node that cannot receive them would be a black hole in the tab order.
	Focusable bool

	// Disabled takes the node out of input without taking it out of the
	// layout. A disabled node is still a hit target — so a click on it does
	// not fall through to whatever is behind it — but receives no events,
	// acquires no hover or press state and is skipped by the focus order.
	Disabled bool

	// Obstructs declares that this node covers part of the window on top of
	// the ordinary layout, so that [App.ScrollIntoView] keeps its target
	// clear of it.
	//
	// It exists for the on-screen keyboard of the project plan, section 19,
	// which is an overlay in a [ui.ZStack] and therefore takes no space away
	// from the form behind it. Without this flag a field revealed at the
	// bottom of a scrolling form would be revealed to a viewport the keyboard
	// is sitting on, which is the one failure the whole feature exists to
	// prevent.
	//
	// What it is not: it is not a safe area, not an inset system and not a
	// general occlusion model. Exactly one node can be the obstruction at a
	// time — the last *unhidden* one built wins — and the only reader is the
	// reveal. Layout, hit testing and painting ignore it completely. gift has
	// no second consumer for a larger idea and inventing one here would be
	// inventing it in the wrong place.
	//
	// "Unhidden" rather than simply "last" because a [ui.OnScreenKeyboard]
	// inside every tab of a [ui.TabBar] is an ordinary composition, and all
	// of them are built: without the qualification the obstruction would be
	// the keyboard of an *inactive* tab, and the reveal would push a focused
	// field up around a rectangle that is not on the screen. A node that
	// declares it and is inside a subtree that declared [Element.Hidden] is
	// not the obstruction, and one that becomes hidden while holding the
	// record gives it up; see [App.applyHidden].
	Obstructs bool

	// Clip confines this node's subtree to its bounds, for painting and for
	// input alike.
	//
	// It is one flag with one reader on each side, and both readers use this
	// node's bounds: [App.hitNode] intersects them into the inherited input
	// clip, and [PaintContext.PaintChildren] pushes them onto the display
	// list's clip stack on the way into the subtree. A widget therefore
	// cannot clip input without clipping paint or the other way round, which
	// is exactly what ui.Button used to do — Clip(true) confined its hit area
	// and let a 400x400 label paint over the window.
	//
	// The clip surrounds the subtree and not the node's own drawing, so a
	// shadow still extends past the bounds and a background still covers
	// them; see [PaintContext.paintKids]. A leaf whose content is not its
	// children has to apply the clip to that content itself, and ui.Text
	// does.
	//
	// The clip is the bounding rectangle, rounded by ClipRadius; hit
	// testing always uses the rectangle.
	Clip bool

	// ClipRadius rounds the corners of the paint clip of [Element.Clip], so
	// that a child's square background does not cover the inside of a
	// rounded container's corners. It does nothing without Clip.
	//
	// Two rounded rectangles do not intersect to a rounded rectangle, so the
	// clip stack cannot carry a radius. The subtree is drawn into a layer
	// instead, as UIKit does for masksToBounds with a corner radius, and the
	// layer is composited through the rounded rectangle; see [Element.Layer]
	// for what a layer costs and keeps. A layer with glass in it has no
	// picture of its own and keeps the square clip.
	ClipRadius float32

	// Transform maps this node and its subtree into the space of its parent.
	// A nil Transform, the default, is the identity.
	//
	// It is shared between the two halves of a frame: [App.paintNode] pushes
	// it into the display list and [App.hitNode] composes the same matrix, so
	// input and output cannot disagree about where a node is. The project
	// plan, section 7, requires exactly that.
	//
	// The pointer is retained for as long as the node lives and must not be
	// modified afterwards; build a new one instead.
	//
	// It is a *build time* declaration, which is why scrolling does not use
	// it: a scroll offset changes without a rebuild, and gift would then have
	// to mutate a value the view owns. See [Element.Scroll].
	Transform *geom.Affine2D

	// Scroll declares this node to be a scroll viewport along one axis.
	//
	// A viewport clips its subtree to its own bounds and translates its
	// children by the scroll offset, which gift keeps in the retained node as
	// presentation state. Both halves are read exactly once, on the way into
	// the subtree, by [App.beginSubtree] for paint and by [App.hitNode] for
	// input — the same single reader rule [Element.Clip] follows, and for the
	// same reason.
	//
	// The translation deliberately applies to the children and not to the
	// node itself: the viewport has to stay where the layout put it, because
	// that is the rectangle it clips against. A node that also carries a
	// Transform composes normally, with its own transform applied first.
	//
	// gift installs its own [Interactor] on a scroll node that declares none,
	// which is what makes wheel, drag and kinetic scrolling work without a
	// view writing any gesture code. A nil Scroll, the default, is an
	// ordinary node.
	Scroll *ScrollSpec

	// Hidden takes this node and its whole subtree out of the frame without
	// taking it out of the tree.
	//
	// It is the mechanism navigation is built on. An inactive tab and a
	// screen that has been pushed over stay mounted — so their state, their
	// scroll offsets and their half typed text fields survive — and stop
	// costing anything per frame:
	//
	//   - [App.paintNode] returns immediately, so nothing in the subtree is
	//     drawn and no painter runs. gift has no paint culling of any kind,
	//     which is why this flag has to exist at all: without it every
	//     mounted node is painted every frame whether or not anybody can see
	//     it.
	//   - [App.hitNode] returns immediately, so nothing in the subtree is a
	//     pointer target. That includes a pointer that captured a node in the
	//     subtree *before* it was hidden: [App.applyHidden] drops the capture
	//     and tells the node its gesture was cancelled, because a captured
	//     pointer is otherwise delivered to unconditionally and a ui.Slider
	//     would go on writing the application's value from a finger moving
	//     over a screen it is not on.
	//   - [App.appendFocusable] skips the subtree, so nothing in it is in the
	//     tab order, and the focus is taken away from it in the build that
	//     hides it; see [App.applyHidden]. The node that held the focus is
	//     told so, after the build — not during it, because no application
	//     handler may run inside a reconciliation, and not never, because the
	//     handler is where a caret enrolment is cancelled. See pending.go.
	//   - the hover and the press look go the same way, and for the same
	//     reason they are on a descendant rather than on the node that
	//     carries this flag.
	//   - [App.ScrollIntoView] and [App.NodeVisibleBounds] both answer "not
	//     on screen", which is one answer and not two.
	//
	// What it deliberately does *not* do is skip build and layout. A hidden
	// subtree that nothing marked dirty is neither rebuilt nor measured
	// anyway — that is the three level invalidation of the project plan,
	// section 6 — and skipping the layout of one that *is* dirty would only
	// move the work to the frame in which it becomes visible again, where it
	// would be a hitch the user can see.
	//
	// The cost that is real and is not solved by this flag, only named: a
	// state write inside a hidden subtree still rebuilds and re-lays it out.
	// A background task writing into an inactive tab is the case; it costs a
	// build and a layout and no paint. A layouter that schedules work for
	// somebody else can ask [LayoutContext.OffScreen] and lower its priority
	// accordingly, which is what ui.ImageView does with the asset pipeline.
	//
	// What used to be a second real cost is not one any more. Every "keep
	// repainting" enrolment in this project — an [EventContext.Animate]
	// window, a kinetic fling, a scroll indicator linger — is *ended* when
	// the subtree is hidden rather than left to run to its deadline; see
	// [App.stopHiddenWork] for what each of them was measured to cost and why
	// none of them is resumed when the subtree comes back. An enrolment taken
	// out from a layouter, which is how ui.Toggle animates, is refused while
	// the node is hidden; see [App.animate].
	Hidden bool

	// Layer lets the backend keep the drawing of this subtree as a picture
	// of its own and composite that picture while the subtree does not
	// change – a cached layer, in the sense of Core Animation.
	//
	// # When it pays
	//
	// When a large subtree moves without changing, which is exactly what a
	// page transition is: without a layer every frame of the slide draws
	// every card, label and picture of both pages again; with one it draws
	// two textured rectangles. On a Raspberry Pi at full HD that is the
	// difference between a slide that stutters and one that does not.
	//
	// # Transitions do it by themselves
	//
	// A node that its [TransitionSpec] is moving is painted as a layer for
	// as long as it moves, whatever this field says, which is why the
	// navigation containers of package ui slide cheaply without asking.
	// Set Layer for a subtree that moves by other means – a Transform that
	// an animation changes – or that sits unchanged under something that
	// changes every frame.
	//
	// # How it knows
	//
	// It does not track anything. The subtree is painted every frame as
	// usual, in the layer's own coordinate space, and the backend compares
	// the operations with those of the frame before; see [render.OpLayer].
	// Equal operations are an equal picture. A change of any kind – a
	// picture that arrived, a caret, a new label – draws the layer again,
	// which costs one extra composite over not having a layer at all.
	//
	// # What it cannot do
	//
	// The picture is the element's bounds: a shadow or a badge that paints
	// outside them is cut off. A glass material shows what lies behind it,
	// and in a picture of its own nothing does; a layer with glass in it is
	// therefore drawn as if it were none, and saves nothing. A layer is drawn whole even where it is
	// clipped, so that it can move into view without being redrawn; a layer
	// much larger than the screen is therefore a waste of memory.
	Layer bool

	// FocusTrap confines the keyboard focus order to this node's subtree for
	// as long as the node is mounted.
	//
	// It is what makes a modal modal on the keyboard side. A scrim stops a
	// finger from reaching the button behind an alert; without this, tab
	// would still walk straight into it.
	//
	// The rule is one sentence: the *last* node in document order that
	// declares it wins, and [App.MoveFocus] then enumerates only that node's
	// subtree. Last in pre order means an alert stacked on top of a sheet
	// traps inside the alert, and a nested trap beats the one it is nested
	// in, because a child comes after its parent. In addition, the build that
	// applies a trap takes the focus away from anything outside it, so an
	// alert that opens over a focused text field leaves the focus nowhere
	// rather than on a field the user can no longer reach.
	//
	// What it is not: it does not stop a key event from *bubbling* out of the
	// trap to an ancestor interactor, because bubbling follows the tree and
	// not the focus order, and it does not block [EventContext.RequestFocus]
	// from a node outside — nothing outside can be reached by a pointer under
	// a scrim, so there is no caller.
	FocusTrap bool

	// Transition makes a change of Hidden a movement rather than a cut; see
	// [TransitionSpec].
	//
	// It is the one declaration in this struct that is read *after* the build
	// that made it, on every frame until the movement is over, and it is
	// therefore the one that has retained state behind it. The zero value is
	// the behaviour every node had before it existed.
	Transition TransitionSpec

	// KeyFallback declares that this node receives key events while *nothing*
	// holds the keyboard focus.
	//
	// Keys go to the focused node and bubble upwards, which is the right rule
	// and is no rule at all on the touchscreen kiosk of the project plan,
	// section 1: a panel with no keyboard attached never focuses anything, so
	// an application-wide key such as escape reaches nobody. The symptom was
	// a navigation stack that told the user to press escape and did nothing
	// when they did. A node that declares this flag is where such a key goes
	// instead; the event then bubbles from it exactly as it would have
	// bubbled from a focused node, so an outer handler still sees what the
	// inner one declined.
	//
	// The rule is [Element.FocusTrap]'s, deliberately: the *last* node in
	// document order that declares it wins, hidden subtrees are skipped, and
	// a node that is disabled or has no [Interactor] is not a candidate. A
	// nested navigation stack therefore beats the one it is nested in, and a
	// covered screen is not a candidate at all. See [App.keyFallbackNode].
	//
	// It changes nothing while something *is* focused. A keyboard user gets
	// the ordinary focused delivery and this flag never comes into play.
	KeyFallback bool

	// Overlay is a view this node presents above everything else in the
	// window, or nil: a popover, a menu, a context menu. It is mounted in the
	// top layer, which covers the whole window, is painted after the whole
	// application and is hit tested before it, so neither a clipping
	// ancestor nor a sibling painted later can hide it, wherever in the tree
	// the presenting node is.
	//
	// The declaration belongs to the node: the overlay is rebuilt with it,
	// withdrawn when a build declares nil, and gone when the node is
	// unmounted or hidden. A node inside the overlay finds where its
	// presenter is with [LayoutContext.Presenter]. Several overlays are
	// painted in the order they were presented, the latest on top.
	Overlay View

	// OverlayExit is how long a withdrawn Overlay stays on the screen,
	// without taking input, so that it can animate away; see
	// [PaintContext.OverlayLeaving]. Zero removes it at once. An overlay of
	// a node that is unmounted or hidden is removed at once regardless.
	OverlayExit time.Duration

	// MultiTouch declares that this node, and every node inside it, takes
	// further fingers while one is already down.
	//
	// gift delivers one finger at a time unless asked otherwise: a touch that
	// arrives while another is down is counted in
	// [Diagnostics.DiscardedTouches] and dropped. That is the right default
	// for a kiosk, where a second hand resting on the glass must not press a
	// second button, and it is the default UIKit has for the same reason —
	// UIView.isMultipleTouchEnabled is false unless a view sets it. A view
	// that recognises a pinch sets this flag, and an additional finger whose
	// hit target is this node or lies inside it is then accepted, captured by
	// that target like any first finger, and delivered with its own
	// [Event.Pointer]. Up to [MaxTouches] fingers are tracked at once.
	//
	// The flag is consulted when the finger comes down and not afterwards: a
	// finger that was accepted stays accepted until it lifts, whatever the
	// next build declares.
	MultiTouch bool

	// PreservesFocus declares that a press landing on this node, or anywhere
	// inside its subtree, leaves the keyboard focus where it is.
	//
	// A press moves the focus: onto the node it hit when that node is
	// focusable, and to nowhere when it is not — see [App.PointerDown] and
	// [App.setFocus] — which is what makes a tap on the background of a form
	// put an on-screen keyboard away. Exactly one kind of surface must be
	// exempt from that, and it is the surface that is typing *into* the
	// focused node: gift's own on-screen keyboard is not focusable, so
	// without this flag the first tap on a letter key would blur the field,
	// withdraw the keyboard and deliver the character to nobody.
	//
	// It is about focus and about nothing else. The node is an ordinary hit
	// target, gets the press, and takes the focus by asking for it if it
	// wants it.
	PreservesFocus bool
}

Element is the description of one retained node.

It is produced by View.Build and applied to a node by the reconciler.

type EntryState

type EntryState struct {
	// Part is the index of the part that has the keyboard.
	Part int8

	// Digits is how many digits have been typed into Part since it became
	// active, and TypedAt when the last one was. A digit that arrives while
	// Digits is not zero continues the number; zero starts it afresh.
	Digits  int8
	TypedAt time.Duration

	// Values holds the parts as they are on screen, and Filled which of them
	// hold anything, one bit per part. A part the user cleared is not filled.
	Values [3]int32
	Filled uint8

	// Advanced reports that Part became active because the part before it
	// was full. A separator typed next is the one that would have ended that
	// part, and moves nowhere.
	Advanced bool

	// Wheel is the part of the wheel movement that has not yet amounted to a
	// whole step.
	Wheel float32
}

EntryState is the retained state of a control whose value is entered part by part: the hour and the minute of a ui.TimeField, the day, the month and the year of a ui.DateField.

It lives in ControlState for the reason the grab offset does. Every digit such a control accepts is a change of its value, a change of its value is a rebuild, and a rebuild replaces the node object — so the part that has the keyboard and the first digit of a number still being typed would be gone before the second digit arrived. The draft has to be here as well, because a value with an empty part is no value at all and cannot be handed to the application to keep.

The fields are named for what an entry control does and not for one widget, like the rest of ControlState; three parts is the most any of them needs. The zero value is a control with every part empty and the first one active.

type Event

type Event struct {
	// Kind selects which of the remaining fields mean anything.
	Kind EventKind

	// Pointer and Device identify the pointer of a pointer event.
	Pointer PointerID
	Device  PointerKind

	// Pos is the position of a pointer event in device space, the space the
	// clip rectangles of the display list and [EventContext.DeviceBounds] live
	// in; see the project plan, section 7. It is not the local space of
	// [EventContext.Bounds]: the two agree until something above the node
	// scrolls, and then differ by that offset.
	Pos geom.Point

	// Delta is the movement since the previous event for a move, and the
	// scroll amount for [EventWheel].
	//
	// For a wheel, positive Y is the wheel pushed *away* from the user, which
	// is what GLFW and therefore Ebitengine report, and it moves towards the
	// beginning of the document — so a scroll container subtracts it from its
	// offset. The previous wording, "positive Y scrolls the content up", said
	// nothing a reader could act on: content moving up on screen and the view
	// moving up the document are opposite directions and it did not say
	// which. The sign is fixed here because [scrollHandler] is now the first
	// consumer and something had to be true.
	Delta geom.Point

	// Precise reports, for [EventWheel], that the wheel measures a distance
	// instead of counting notches: a trackpad. Its Delta is then a distance
	// in units of ten points of finger travel, which is what GLFW, and so
	// Ebitengine, makes of the precise deltas of macOS and of Wayland, and a
	// scroll container moves [ScrollConfig.PreciseWheelStep] per unit of it
	// rather than [ScrollConfig.WheelStep].
	//
	// GLFW does not say which device scrolled, so gift infers it in
	// [App.PointerWheel]: a notch is reported as a whole number and a
	// trackpad mostly in fractions, so a fractional delta is precise, and so
	// is every delta for a short while after one, which covers the whole
	// numbers a trackpad reports in passing.
	Precise bool

	// Phase is, for [EventWheel], the stage of the trackpad gesture the delta
	// belongs to, where the platform reports it, and [WheelPhaseUnknown]
	// everywhere else; see [App.PointerWheelPhase]. It is never
	// [WheelPhaseLift], which carries no delta and is no event.
	Phase WheelPhase

	// Inside reports whether Pos lies inside the receiving node, taking the
	// clips of its ancestors into account.
	//
	// It is the whole point of pointer capture: a press that starts on a
	// button and is released somewhere else still reaches the button, with
	// Inside false, so the button can decline to activate instead of never
	// hearing about the release at all.
	Inside bool

	// Dragged reports whether the pointer moved further than [DragSlop] from
	// the position of the press. A tap is an up with Inside true and Dragged
	// false.
	Dragged bool

	// Key and Mods describe a key event. Mods is filled in for pointer
	// events too, so that a modified click can be told apart.
	Key  Key
	Mods Mods

	// Rune is the character of an [EventRune] and is meaningless otherwise.
	//
	// It is always a printable rune: the platform filters control characters
	// out before gift ever sees them, because backspace and enter are keys
	// and arrive as such, and a text model that had to reject '\b' as well as
	// handle [KeyBackspace] would have two code paths for one user action.
	Rune rune

	// Repeat is set on an [EventKeyDown] that gift synthesised because the
	// key is being held; see [KeyRepeatDelay].
	//
	// A handler that moves a cursor ignores it — that is the point of the
	// repeat. A handler that *activates* something must not: holding enter on
	// a button would otherwise submit a form thirty times a second. The flag
	// is what lets both be written, and it is on the event rather than
	// suppressed inside the runtime because gift cannot know which of the two
	// a given interactor is.
	Repeat bool

	// Time is the timestamp the backend supplied in [App.BeginInput], as a
	// duration since an arbitrary but fixed origin. Differences are
	// meaningful, the absolute value is not.
	//
	// For a move, a press and a release it is the time the pointer was
	// sampled, when the backend knows it — see [App.PointerMoveAt] — and is
	// then a little earlier than the tick. A velocity is computed from it.
	Time time.Duration
}

Event is one input event as an Interactor sees it.

It is a plain value and is passed by value. Nothing in it points at anything, which is what keeps dispatch inside the zero allocation contract of the project plan, section 11.

func (Event) IsPointer

func (e Event) IsPointer() bool

IsPointer reports whether e is a pointer event, that is one with a meaningful Pos.

type EventContext

type EventContext struct {
	// contains filtered or unexported fields
}

EventContext is the interface of an Interactor to the runtime.

There is exactly one instance per App; it is reused for every node and every event and is valid only for the duration of a Interactor.HandleEvent call.

func (*EventContext) After

func (c *EventContext) After(d time.Duration)

After asks for one EventTimer to be delivered to the receiving node d from now, on the clock App.BeginInput is given.

What it is for

A recogniser has decisions that only time can make, and nothing else happens while it waits: a single tap is a single tap only once the second tap of a double tap has failed to arrive, and a press is a long press once it has rested long enough. gift fires its own long press from App.BeginInput for exactly this reason; this is the same mechanism made available to a node, so a recogniser with its own durations does not have to poll from a painter.

Rules

A node has at most one pending timer. Asking again replaces it, and a d of zero or less cancels it. The timer is dropped when the node is unmounted or hidden, like an EventContext.Animate enrolment, and it keeps the node marked for repaint until it fires, so a backend that idles between frames still ticks in time. The event goes to the node and does not bubble.

func (*EventContext) Animate

func (c *EventContext) Animate(d time.Duration)

Animate keeps the receiving node marked for repaint for the next d, on the clock App.BeginInput is given.

What it does and does not promise

It does not run anything. There is no interpolation, no easing and no callback: gift marks the node for paint on every tick inside the window and the node's painter decides what to draw from PaintContext.Now. That keeps the whole mechanism to one deadline per animating node and keeps the arithmetic of an animation in the view that owns it, which is where a designer can read it.

The window is a *deadline and not a duration left to run*: calling it again with the same d before the first one expires extends the window rather than queueing a second one, so a handler may call it on every event without counting. A d of zero or less cancels the enrolment, and that is how a view that lost the focus stops asking — one repaint still happens, so that the frame in which the caret disappears is actually drawn.

Why a deadline is mandatory

Because the alternative is an idle application that never sleeps. The backend drops to Config.IdleTPS when nothing needs painting; an enrolment without an end would defeat that for the lifetime of the node, on a kiosk that may sit untouched for hours. ScrollIndicatorLinger makes the same trade for the same reason and states the same cost. A view whose animation is genuinely unbounded — a spinner — re-arms from its own handler or accepts that it stops; a blinking caret stops, on purpose, because a caret that has blinked for a minute has said everything it has to say and the user is not looking.

An enrolment does not outlive its reason

It used to. A node that was animating because it held the focus, and that declared itself [Disabled] or was hidden in the very build in which it still held it, was never told EventFocusLost: [App.setFocus] dropped the notification when it ran during a build. The node therefore never called Animate(0), and its enrolment stood until the deadline it last asked for. Measured with ui.TextField: hiding the tab of a focused field cost 100 of 120 sampled ticks — the full ten second window at 60 Hz, drawing no caret — where an ordinary blur of the same field cost none.

The notification is now deferred rather than dropped, and delivered at the first point in the update where an application handler may run; see pending.go. It reaches a disabled node too, through [App.notifyDirect], because the node is the only thing in the process that can let go of what it is holding. On top of that, [App.stopHiddenWork] ends every enrolment inside a subtree that becomes hidden, and [App.animate] refuses a new one for a node nobody can see — which is the half that matters for ui.Toggle, whose enrolment is re-armed from its *layouter* and would otherwise come straight back on the next pass.

The deadline is still mandatory, and for the reason above rather than as a backstop: a view that re-arms for ever from its own handler is still allowed to, and the deadline is what bounds everything else.

It allocates once per node that has ever animated, and nothing afterwards: the enrolment list is a reused slice compacted in place, exactly like [inputState.flings] and [inputState.indicators].

func (*EventContext) Bounds

func (c *EventContext) Bounds() geom.Rect

Bounds returns the absolute rectangle of the receiving node, as computed by the last layout pass.

It is in *local* space and is therefore not the space Event.Pos lives in: the two are the same rectangle until something above the node scrolls, and then they differ by that offset. An interactor that compares a pointer position against a rectangle of its own wants EventContext.DeviceBounds.

func (*EventContext) Capture

func (c *EventContext) Capture(id PointerID) bool

Capture is EventContext.StealPointer for the pointer id rather than the one whose event is being dispatched: for a recogniser that decides on a timer, when no pointer event is on its way — a long press that turns into a context menu takes the finger from the tile it rests on, so that the tile does not treat the release as a tap.

func (*EventContext) ClearFocus

func (c *EventContext) ClearFocus()

ClearFocus removes the keyboard focus from whatever holds it.

func (*EventContext) ControlState

func (c *EventContext) ControlState() ControlState

ControlState returns the control state of the node receiving the event.

func (*EventContext) DeviceBounds

func (c *EventContext) DeviceBounds() geom.Rect

DeviceBounds returns the rectangle of the receiving node mapped through the transforms of its ancestors, which is the space Event.Pos lives in.

It is the input half of PaintContext.DeviceBounds and exists for the same reason: the two rectangles are the same one until something above the node scrolls, and then they differ by that offset. An interactor that compares a pointer position against a rectangle of its own — a scroll indicator does — has to compare in one space, and Event.Pos fixes which one.

func (*EventContext) DismissSoftKeyboard

func (c *EventContext) DismissSoftKeyboard() bool

DismissSoftKeyboard puts the on-screen keyboard away from inside an event handler, by blurring whatever asked for it, and reports whether there was one up. It is App.DismissSoftKeyboard reached the way a widget reaches everything else; ui.OnScreenKeyboard's own dismiss key is the caller it exists for.

func (*EventContext) Interaction

func (c *EventContext) Interaction() Interaction

Interaction returns the hover, press, focus and disabled state gift maintains for the receiving node.

func (*EventContext) KeyDown

func (c *EventContext) KeyDown(k Key, mods Mods)

KeyDown injects a key press, as App.KeyDown does for the backend. It is the other half of EventContext.TypeRune and exists for the keys that are positions rather than characters: backspace and enter carry no rune at all.

A press injected here arms gift's key repeat exactly like a physical one, so a finger resting on a drawn backspace deletes at KeyRepeatInterval without the widget owning a timer. The release must be injected too, or the repeat runs until something else cancels it; see EventContext.KeyUp.

func (*EventContext) KeyUp

func (c *EventContext) KeyUp(k Key, mods Mods)

KeyUp injects a key release; see EventContext.KeyDown.

func (*EventContext) Now

func (c *EventContext) Now() time.Duration

Now returns the timestamp of the event being dispatched, on the clock App.BeginInput is given: the clock a control's animation is started on.

func (*EventContext) PointerIsDown

func (c *EventContext) PointerIsDown(id PointerID) bool

PointerIsDown reports whether the pointer id is currently pressed: the mouse with its primary button held, or a finger on the glass.

It is what a recogniser that tracks several fingers consults when a new one arrives, to forget the fingers it still remembers but that went away while it could not hear about it — while its node was disabled, say, when gift delivers nothing at all. See Element.MultiTouch.

func (*EventContext) Presenter

func (c *EventContext) Presenter() (geom.Rect, bool)

Presenter is LayoutContext.Presenter for the node receiving the event.

func (*EventContext) Repaint

func (c *EventContext) Repaint()

Repaint marks the receiving node as needing to be drawn again, without rebuilding or re measuring anything.

gift redraws the whole visible list every frame anyway — the project plan, section 6 — so this changes no pixels by itself. What it does is keep App.NeedsPaint, and therefore the idle tick policy of the backend, honest about the fact that something changed. An interactor that keeps its own presentation state calls it; one that only reads EventContext.Interaction does not have to, because the dispatcher already did.

func (*EventContext) RequestFocus

func (c *EventContext) RequestFocus()

RequestFocus moves the keyboard focus to the receiving node, if it declared itself focusable. A disabled node cannot take the focus.

func (*EventContext) RequestLayout

func (c *EventContext) RequestLayout()

RequestLayout marks the receiving node as needing another layout pass in the next update, without rebuilding anything.

Most interactors do not need it: hover, press and focus are read back by the painter and change no size. A *virtualising* container does, because its layouter is what turns state into placed children — the gallery of the project plan, section 10, decides in its layouter which tile stands for which item and where the viewport has to move so that a keyboard cursor is on screen. An interactor that moves such a cursor has changed a layout input, and this is how it says so.

It is deliberately not the same as EventContext.Repaint. Asking for a layout when a repaint would do costs a measure pass every keystroke; asking for a repaint when a layout was needed shows the previous frame's geometry, which is the harder bug to see.

func (*EventContext) RequestSoftKeyboard

func (c *EventContext) RequestSoftKeyboard(want bool)

RequestSoftKeyboard declares whether the receiving node wants an on-screen keyboard, and asks for a rebuild when the answer changed.

What it is

It is a declaration and not a command. gift has no keyboard to raise: Ebitengine offers no way to open a native one and the kiosk of the project plan, section 1, has no operating system keyboard to open. What this does is publish one bit that BuildContext.SoftKeyboardRequested reads, so that a view somewhere else in the tree — ui.OnScreenKeyboard is the one gift ships — can decide to draw one. A backend on a platform that *does* have a native keyboard would read the same bit.

The rebuild is included for the reason [ui.SetTheme] includes its App.Invalidate: nothing on the screen changes until the views are built again, and a caller who had to remember that separately would have a keyboard that appears one unrelated interaction late.

Policy is not here

gift does not decide whether the keyboard is appropriate. A text field on a desktop calls this only when the application turned the kiosk switch on; see ui.SetOnScreenKeyboard for why that switch exists and why it is emphatically not Event.Device or PointerKind.

func (*EventContext) Retained

func (c *EventContext) Retained[T any]() *T

Retained returns the value of type T the receiving node keeps across builds, creating a zero T on first use.

What it is for

ControlState is the retained state of a control, and it is deliberately a small fixed struct: one grab, one animation. A recogniser that follows two fingers, keeps the last hundred milliseconds of samples for a velocity and runs a spring after the release does not fit into it, and widening it for one caller is what its own documentation argues against. This is the open ended sibling: one value of any type per node, owned by the node and not by the view, so it survives every rebuild for the reason ControlState does — a rebuild replaces the view and the node object, not the node.

It is the identity rule of SwiftUI's state, applied to a node: the value lives as long as the node at this position with this type and key, and a node that is unmounted takes it along. A fresh node in a recycled slot starts from a zero T; see [nodeData.release].

Rules

One value per type. A node may keep several, one for each type it asks for, so that a recogniser that keeps its fingers here and the view that uses it, which keeps its zoom here, do not have to know of each other. A type that is unexported in its package cannot collide with anybody else's.

Writing the value never builds, lays out or repaints anything by itself. A handler that changed what is drawn says so with EventContext.Repaint or EventContext.Animate, exactly as with its own fields.

The value is a pointer into the node's payload, valid while the node is mounted. Keeping it beyond that is the same mistake as keeping a PaintContext: it then belongs to nobody.

func (*EventContext) ScrollBy

func (c *EventContext) ScrollBy(d float64) bool

ScrollBy moves the scroll container receiving the event by d document units and reports whether the offset changed; see EventContext.ScrollTo.

func (*EventContext) ScrollIndicator

func (c *EventContext) ScrollIndicator() (ScrollIndicatorState, bool)

ScrollIndicator returns the indicator state of the scroll container receiving the event, and false when this node is not one.

func (*EventContext) ScrollInfo

func (c *EventContext) ScrollInfo() (ScrollInfo, bool)

ScrollInfo returns the state of the scroll container receiving the event, and false when this node is not one.

func (*EventContext) ScrollIntoView

func (c *EventContext) ScrollIntoView() bool

ScrollIntoView adjusts every scroll container above the receiving node so that the node is visible, and reports whether anything moved.

It is App.ScrollIntoView for the node whose handler is running, and it exists because an interactor has no NodeRef of itself and no application handle: a text field that takes the focus deep inside a scrolling form has to be able to say "show me" without the application wiring a reference through. The movement is a jump, not an animation; see App.ScrollTo.

func (*EventContext) ScrollTo

func (c *EventContext) ScrollTo(off float64) bool

ScrollTo moves the scroll container receiving the event to the document offset off, clamped, and reports whether the offset changed. It is App.ScrollTo for the current node, and like it, it cancels a running fling: a thumb the user has taken hold of and a fling still coasting would otherwise fight over the same number.

It returns false for a node that is not a scroll container, rather than panicking, because an interactor that delegates is entitled to ask.

func (*EventContext) SetControlState

func (c *EventContext) SetControlState(v ControlState)

SetControlState writes the control state of the node receiving the event and marks it for repaint when it changed.

It never rebuilds and never relayouts: this is presentation state, and the project plan, section 5, requires presentation state not to force a build.

func (*EventContext) SetScrollIndicator

func (c *EventContext) SetScrollIndicator(v ScrollIndicatorState) bool

SetScrollIndicator writes the indicator state of the scroll container receiving the event and reports whether it changed. It answers false for a node that is not a scroll container, like EventContext.ScrollTo.

A change marks the node for repaint — the decoration is about to look different — and also counts as activity, exactly as a movement of the content does. That second half is not bookkeeping but the fade rule: a pointer resting on the track emits no events at all, so without it ScrollInfo.IdleFor would keep growing underneath a bar that is held visible by the hover, and the moment the pointer left the bar would be several hundred milliseconds idle already and would vanish in one frame instead of fading. Marking the *end* of a hover as activity is what gives that transition its hold and its fade.

func (*EventContext) StealPointer

func (c *EventContext) StealPointer() bool

StealPointer moves the capture of the pointer whose event is being dispatched to the receiving node, and reports whether the node holds it afterwards.

What it is for

The classic mobile interaction: a finger comes down on a button inside a scrolling list, travels further than DragSlop and turns into a scroll. The button took the press, so every move is delivered to it and bubbles up to the scroller; the scroller recognises the drag and calls this. The button is then told EventPointerCancel — its gesture really is over — loses its pressed look and never sees the release, so it cannot activate. Without it the release would arrive at the button with Event.Dragged set, which a well written control declines, but a control that merely checks Event.Inside would fire, and the pressed highlight would follow the finger down the whole list.

What it is not

It is not a gesture arena. There is no negotiation, no deferred resolution and no way to hand the pointer back: whoever calls it last wins, and the previous holder is told the gesture ended. That is enough for the one conflict gift has — a scroll against everything else — and a real arena is the kind of thing to build when there is a second conflict to arbitrate.

The cancel is delivered synchronously, which means the previous holder's HandleEvent may still be on the stack above this call: it is the frame that bubbled the event here in the first place. A handler therefore has to tolerate receiving a cancel from inside its own dispatch, which is why cancel handling is required to be a state reset and not a state machine transition.

It does nothing when no pointer is down, so calling it from a key handler is harmless rather than a way to corrupt the capture.

func (*EventContext) TypeRune

func (c *EventContext) TypeRune(r rune)

TypeRune injects one character as though the platform's keyboard had produced it. It is App.TypeRune called from inside an event handler.

This is how a drawn key becomes text, and the indirection is the whole point: the rune travels the ordinary EventRune path to the focused node and bubbles like any other, so a widget cannot tell it from a physical key press and therefore cannot handle the two differently. There is no second code path to keep in step.

The one honest difference from a physical key, stated rather than hidden: a physical 'a' also produces a EventKeyDown carrying KeyA, because a keyboard reports a position as well as a character. A drawn letter key sends only the character. Nothing in gift reads a letter key except as the operand of ShortcutModifier — see Key — and an on-screen keyboard offers no such modifier, so the missing press has no reader. A drawn key that *is* a position, backspace and enter and space, sends the press as well; see EventContext.KeyDown.

Re-entrancy is fine and is what this relies on: [App.deliver] saves and restores the receiving node of the context around every handler, so dispatching from inside a handler leaves this one exactly as it found it.

type EventKind

type EventKind uint8

EventKind discriminates an Event.

const (
	// EventNone is the zero value and is never dispatched.
	EventNone EventKind = iota

	// EventPointerEnter says the mouse moved onto this node. It is never
	// produced by a touch; see [PointerKind].
	EventPointerEnter
	// EventPointerLeave says the mouse moved off this node.
	EventPointerLeave
	// EventPointerMove is a position change. While a pointer is captured it
	// goes to the capturing node even when the position is outside it; see
	// [Event.Inside].
	EventPointerMove
	// EventPointerDown is a press. The node that is hit captures the
	// pointer until the matching up or cancel.
	EventPointerDown
	// EventPointerUp is a release. It is delivered to the capturing node,
	// inside or not.
	EventPointerUp
	// EventPointerCancel says the pointer went away without a release. gift
	// sends it itself when another node takes the pointer with
	// [EventContext.StealPointer] and when the node holding the press is
	// hidden; a backend adds its own causes — the window lost focus, the
	// platform took the gesture over — by calling [App.PointerCancel], which
	// the Ebitengine backend does not do today. A node that treats it like an
	// up would activate on something the user never completed.
	//
	// A node that is unmounted while it holds a press does *not* receive one,
	// and cannot: by the time gift knows, the node is gone and there is
	// nobody to deliver to. gift ends the press instead — see [App.forgetNode]
	// — so the pointer is genuinely up as far as the rest of the runtime is
	// concerned, and the release that eventually arrives is dropped. This
	// list used to name that case as a cause, and no code path produced it.
	EventPointerCancel
	// EventLongPress is delivered once per press to the capturing node, when
	// the pointer has been down for [LongPressDelay] without moving further
	// than [DragSlop], and bubbles to its ancestors until one handles it: a
	// context menu around a button sees the long press the button ignores.
	// [EventContext.StealPointer] works while it is delivered.
	EventLongPress
	// EventWheel is a scroll wheel or two finger scroll. It is delivered to
	// the node under the pointer and bubbles to its ancestors, because the
	// node that scrolls is usually not the leaf under the cursor.
	EventWheel

	// EventKeyDown is a key press, delivered to the focused node and bubbled
	// to its ancestors.
	EventKeyDown
	// EventKeyUp is a key release, delivered the same way.
	EventKeyUp
	// EventRune is one character the user typed, delivered to the focused
	// node and bubbled like a key event. [Event.Rune] carries it.
	//
	// It is a different channel from [EventKeyDown] on purpose, and the
	// difference is the whole reason this event exists. A [Key] is a
	// *physical* key identified by its position on a US keyboard —
	// Ebitengine says so of its own Key type, "KeyQ represents Q key on US
	// keyboards and ' (quote) key on Dvorak keyboards" — while a rune is
	// what the platform's keyboard layout, its dead keys and its AltGr
	// combinations produced from that key press. On a German layout the key
	// in the position of the US semicolon yields 'ö', and no mapping of key
	// codes to characters inside gift could know that.
	//
	// So: a view that edits text listens to EventRune for the text and to
	// EventKeyDown for the commands — backspace, the arrows, the shortcuts.
	// A view that never listens to EventRune sees no characters at all,
	// which is the correct behaviour for a button.
	//
	// There is no guaranteed one to one relation with key events in either
	// direction. One key press can produce no rune (a dead key waiting for
	// the next one), one rune (the normal case), or a rune that arrives on a
	// later key press (the dead key resolving). Holding a key produces
	// further runes at whatever rate the platform repeats at, which is not
	// gift's [KeyRepeatInterval]; see [App.TypeRune].
	EventRune

	// EventFocusGained and EventFocusLost bracket the time a node holds the
	// keyboard focus.
	EventFocusGained
	// EventFocusLost is the counterpart of EventFocusGained.
	EventFocusLost

	// EventTimer is the wake up a node asked for with [EventContext.After].
	// It is delivered to that node only and does not bubble. It carries no
	// position; [Event.Time] is the tick it fired in.
	EventTimer

	// EventContextMenu asks for the context menu of what is at [Event.Pos]:
	// a click with the secondary mouse button. It goes to the node under the
	// pointer and bubbles until a node handles it, like a wheel event, and
	// it is the only thing the secondary button does in gift; see
	// [App.ContextMenuAt]. On a touch screen the same request is a long
	// press, which a view recognises itself.
	EventContextMenu
)

type Interaction

type Interaction struct {
	// Hover is set while a mouse is over the node and no other node has
	// captured the pointer. A touch never sets it.
	Hover bool
	// Pressed is set while a pointer this node captured is down and inside
	// it. A press that has wandered off the node clears it and sets it again
	// when the pointer comes back, which is the behaviour every platform
	// button has.
	Pressed bool
	// Focused is set while the node holds the keyboard focus.
	Focused bool
	// FocusVisible is set while the node holds the keyboard focus *and* that
	// focus arrived from the keyboard, so that it has to be shown.
	//
	// # Why a second bit and not just Focused
	//
	// Because focus and the focus ring answer different questions. Focus is
	// "where do key events go", and a pointer has to move it: a press on a
	// text field must put the caret there, and a press on a button must make
	// the space bar activate that button. The ring is "where would the
	// keyboard go if you used it", and it is noise for a user who is not
	// using one — on the touchscreen kiosk of the project plan, section 1,
	// every single tap would otherwise leave a permanent ring behind on the
	// tab bar, because [Interactor] implementations take the focus on
	// [EventPointerDown].
	//
	// So gift records where the focus came from. A focus installed while a
	// key event is being dispatched, or by [App.MoveFocus], is visible; one
	// installed while a pointer event is being dispatched is not. That is
	// the rule CSS spells :focus-visible and the rule Apple's platforms use,
	// and it is applied here once, in the core, rather than being
	// re-litigated by every widget.
	//
	// Every focus ring in package ui reads this bit. Everything that is
	// about *where the keys go* — a text field's caret, its selection
	// highlight, the key routing itself — reads Focused, because a field
	// tapped with a finger is being typed into and must show a caret.
	FocusVisible bool
	// Disabled mirrors [Element.Disabled]. It is here so that a painter
	// needs to consult one value rather than two.
	//
	// It does not clear Hover or Pressed. A disabled node is still a hit
	// target that captures the press it swallows, and gift tracks hover and
	// press on it exactly as on an enabled one; a painter that draws the
	// disabled look gives Disabled precedence, which is what every painter in
	// package ui does. The consequence for a test is that
	// [App.NodeInteraction] can report a disabled control as hovered or
	// pressed: assert Disabled, not the absence of the other two.
	Disabled bool
}

Interaction is the presentation state gift maintains for an interactive node.

It is owned by the retained node, not by the view, which is what makes hover and press free of rebuilds: the dispatcher writes these three bools into the node payload and marks the node for repaint, and the painter reads them back. No view function runs and Diagnostics.Builds does not move. The project plan, section 5, requires exactly that — "Scrolloffset, Hover, Pressed und Animation sind lokaler Praesentationszustand. Sie sollen keinen fachlichen Root-Rebuild erzwingen."

It also survives a rebuild that happens for an unrelated reason, because it lives in the node and a rebuild replaces the view, not the node.

type Interactor

type Interactor interface {
	// HandleEvent processes one event and reports whether it was handled.
	//
	// An unhandled pointer or key event bubbles to the nearest ancestor with
	// an Interactor, which is how a scroll container sees a wheel event that
	// happened over a label inside it. Enter, leave, focus and cancel events
	// do not bubble: they are statements about one node.
	//
	// It runs inside Ebitengine's Update, never during Draw, so writing
	// state from here is normal and is the documented way to react; see the
	// project plan, sections 5 and 6. It must not allocate: the project plan,
	// section 11, puts input processing inside the zero allocation contract.
	HandleEvent(ctx *EventContext, e Event) bool
}

Interactor is the retained half of a view that wants input.

Opting in

A node takes part in hit testing exactly when its Element carries an Interactor. A nil Interactor makes the node *transparent* to input: it is not a hit target itself, and its children are tested exactly as they would be otherwise.

That is deliberately the opposite polarity of the nil painter rule, and the reason is which mistake each default causes. A nil painter that drew nothing would hide a whole subtree with no symptom but a blank window, which is why a nil painter paints its children. A nil interactor that *swallowed* input would make every plain container eat the clicks meant for what is behind it, again with no symptom; so a nil interactor swallows nothing. In both cases the default is the permissive one, and in both cases the failure of forgetting the non nil case is local and loud: a button with no Interactor does nothing when clicked, one view away from the mistake.

A node with an Interactor is a hit target over its whole [Node bounds], intersected with the clips of its ancestors. Rounded corners are not followed; a click in the corner of a rounded button hits the button.

func ScrollInteractor

func ScrollInteractor() Interactor

ScrollInteractor returns the Interactor gift installs on a scroll container that brought none of its own: wheel, drag past DragSlop and the kinetic fling, with the overscroll chaining rule described on it.

It exists because the two are otherwise exclusive. A scroll container that declares an Interactor — because it also wants the keyboard, which a gallery does — replaces gift's, and would then have to reimplement the gesture. The intended shape is delegation:

func (n *myNode) HandleEvent(ctx *gift.EventContext, e gift.Event) bool {
    if e.Kind == gift.EventKeyDown { ... }
    return gift.ScrollInteractor().HandleEvent(ctx, e)
}

The returned value is a shared, stateless singleton — everything it touches lives in the node payload — so calling this in the event path allocates nothing.

type Key

type Key uint8

Key is a physical key, in the small set gift's own navigation, activation and text editing need.

It is still not a keyboard map, and the boundary moved once

A Key names the *position* of a key on a US keyboard, not the character it produces; that is Ebitengine's definition and gift inherits it rather than inventing a second one. Characters travel on their own channel, EventRune, because only the platform knows what a layout makes of a key press. See App.TypeRune.

The original boundary was "what focus order and activation need": tab, space, enter, escape, the arrows, home, end and the two page keys. The project plan, section 19, widened it, and the new boundary is:

  • the keys that move a cursor or delete text, because a text field cannot be written without them and because they are the keys gift itself repeats; see KeyRepeatDelay.
  • the letters that appear in the five editing shortcuts every platform agrees on — select all, copy, cut, paste, undo — and nothing else. They are here as the operand of a modifier, never as a source of text: a handler that reads KeyA to insert an 'a' is wrong on every keyboard outside the United States, and that is why the alphabet is not here in full.

Everything else is KeyOther, which nothing in gift acts on. A backend may report a key it has no name for as KeyOther, and an application may then act on it, but the Ebitengine backend polls only the named keys and never reports it, so on that backend an application cannot observe it. The cost of widening the enum is a larger table in every backend and one more poll per tick per entry, so it is widened for a named need and not for completeness.

const (
	// KeyOther is any key gift has no name for.
	KeyOther Key = iota
	KeyTab
	KeySpace
	KeyEnter
	KeyEscape
	KeyLeft
	KeyRight
	KeyUp
	KeyDown
	KeyHome
	KeyEnd
	KeyPageUp
	KeyPageDown

	// KeyBackspace deletes backwards, KeyDelete forwards. Both repeat; see
	// [KeyRepeatDelay].
	KeyBackspace
	KeyDelete

	// KeyA, KeyC, KeyV, KeyX and KeyZ are the operands of select all, copy,
	// paste, cut and undo. They are physical key positions, so on a Dvorak or
	// an AZERTY keyboard they sit wherever that keyboard's US-QWERTY
	// equivalent sits — which is deliberately what every toolkit does with
	// these five shortcuts, because the user learned the finger position and
	// not the letter.
	//
	// Redo has no sixth letter here: the combination that redoes is shift
	// plus the undo one on macOS and on GTK, and KeyY would only be needed
	// for the Windows convention, which section 1 of the project plan does
	// not list as a target.
	KeyA
	KeyC
	KeyV
	KeyX
	KeyZ
)

type LayoutContext

type LayoutContext struct {
	// contains filtered or unexported fields
}

LayoutContext gives a Layouter access to its children.

Instances are pooled per tree depth and reused across frames; a context is valid only for the duration of the Layouter.Layout call that received it.

func (*LayoutContext) After

func (l *LayoutContext) After(d time.Duration)

After is EventContext.After for the node being laid out. A layouter that starts something with an end — a transition it was asked for by a model the application changed between frames — arms the timer that finishes it.

func (*LayoutContext) AnchorScroll

func (l *LayoutContext) AnchorScroll(off float64) bool

AnchorScroll pins this scroll container's viewport to the document coordinate off, clamped to the content reported so far, and reports whether the offset changed.

It is the write half of LayoutContext.ScrollOffset and it exists for one job: the scroll anchor of the project plan, section 10. A reflow — a resize, a layout switch, a batch of corrections — moves every item in the document, and keeping the viewport on the same picture means computing a new offset from the new position of that picture. That computation needs the new layout, so it happens inside the layouter, and its result has to go somewhere.

It is restricted to the top of the pass, and that is enforced

It may only be called before this layouter has measured its first child, and calling it afterwards panics. That restriction is the answer to the obvious objection, which is that this writes state during layout and invalidates nothing. Nothing needs to be invalidated, because nothing downstream has read the offset yet: the children of this node have not been measured or placed in this pass, the transform gift pushes and the hit test walk both read the offset after the pass has finished, and the caller is required to consume the new value itself — that is what makes it a parameter of the placement rather than a side effect on the frame after next.

Before WU-O this was a general purpose SetScrollOffset whose own documentation said it "exists for exactly one caller" and whose correctness rested on that caller's discipline. The name now says what it is for and the panic makes the discipline the framework's rather than the caller's.

Call LayoutContext.ReportScrollContent first, or the clamp has nothing to clamp against.

Any running fling is cancelled, for the same reason App.ScrollTo cancels one: a reflow that moves the content and a fling that also moves it would fight over the same number.

func (*LayoutContext) Animate

func (l *LayoutContext) Animate(d time.Duration)

Animate keeps the node being laid out marked for repaint for the next d, on the clock App.BeginInput is given. It is EventContext.Animate for a layouter, and every word of that documentation applies — in particular that d is a deadline and that an animation without one never lets the machine sleep again.

A layouter needs it because a value may change without any event at all: an application that sets a toggle from a timer, from App.Post or from a network reply rebuilds the control, and the control has to animate towards the new value just as it does when a finger moved it. The layout pass is where that change becomes visible to the retained side; see LayoutContext.SetControlState.

func (*LayoutContext) ChildBaseline

func (l *LayoutContext) ChildBaseline(child int) (float32, bool)

ChildBaseline returns the baseline the given child reported during this layout pass, and whether it reported one at all. See LayoutContext.ReportBaseline.

func (*LayoutContext) ChildCount

func (l *LayoutContext) ChildCount() int

ChildCount returns the number of children of the node being laid out.

func (*LayoutContext) ChildFlex

func (l *LayoutContext) ChildFlex(child int) float32

ChildFlex returns Element.Flex of the given child, zero for an inflexible one. The index must be in [0, ChildCount).

This is how a stack tells a spacer from an ordinary child: one float read from the retained node, no interface dispatch and no type assertion in the layout path.

func (*LayoutContext) ChildSize

func (l *LayoutContext) ChildSize(child int) geom.Size

ChildSize returns the size a child chose in a preceding LayoutContext.Measure. It is the zero size if the child has not been measured in this pass.

func (*LayoutContext) ControlState

func (l *LayoutContext) ControlState() ControlState

ControlState returns the control state of the node being laid out.

func (*LayoutContext) Density

func (l *LayoutContext) Density() float32

Density returns the device density, that is how many physical pixels one logical pixel covers. It is always an integer of at least one; see App.SetDensity.

A layouter must not use it to measure anything. Layout is in logical pixels at every density, and a size that changed with the display would make the same application two different applications. What it is for is choosing a *resource* whose resolution has to match the screen: the image ladder of the project plan, section 18, asks for a rung in device pixels, and the rung is chosen in layout because that is where the request is scheduled. See ui.ImageView.

func (*LayoutContext) Invalidator

func (l *LayoutContext) Invalidator() func()

Invalidator returns a function that marks this node for layout from outside the frame, and nil for a node that is no longer part of the tree.

Why a closure and not a NodeRef

Because the caller is a view's retained object, not the application. A virtualising container holds a model the application mutates directly — a catalogue correction arrives from a worker, a keyboard cursor is moved by a command — and that model has to say "my layout is stale" without having been handed the App, the node reference and the knowledge of how the two combine. Handing it one function is the smallest interface that does the job, and it keeps the App out of a package that must not import the frame loop.

The closure is created once per node and cached in the node, so calling this on every layout pass allocates only on the first one. It is safe to call after the node has been unmounted: it then does nothing, because [App.markNeedsLayout] stops at an invalid handle. It must be called from the UI executor like everything else.

func (*LayoutContext) Measure

func (l *LayoutContext) Measure(child int, c geom.Constraints) geom.Size

Measure lays out the given child with the constraints c and returns the size the child chose. The index must be in [0, ChildCount).

A clean child that is asked for the constraints it already has answers from its cache without running its own layouter, so measuring a subtree that did not change costs one comparison.

func (*LayoutContext) Node

func (l *LayoutContext) Node() NodeRef

Node returns a reference to the node being laid out.

It exists for a layouter that keeps retained state *outside* gift — the gallery of the project plan, section 10 keeps a spatial index and a tile pool in an application owned object — and therefore has to be able to tell which node it is currently running for. Such an object is shared by construction, and two mounted views over one of them would silently interleave their writes into the same pool. Comparing this reference across a pass is how that is turned into a diagnosis instead of a symptom.

The reference is stable for as long as the node is mounted and becomes stale when it is unmounted; it is comparable with ==.

func (*LayoutContext) Now

func (l *LayoutContext) Now() time.Duration

Now returns the timestamp of the input phase this layout pass belongs to, on the clock App.BeginInput is given. It is PaintContext.Now for a layouter.

A layouter that reads it is one that starts animations; see LayoutContext.SetControlState. Nothing else in a layout may depend on the clock, because a layout that changes with time and is not driven by an enrolment would be a size that moves under a tree nobody asked to re-measure.

func (*LayoutContext) OffScreen

func (l *LayoutContext) OffScreen() bool

OffScreen reports whether the node being laid out is inside a subtree that declared Element.Hidden.

A layouter is run for a hidden subtree — that is deliberate, see Element.Hidden — so this is the question "is any of this going to be looked at". It exists for exactly one kind of decision: the *priority* of work a layouter schedules for somebody else.

The case that forced it is ui.ImageView, which issues its request to the asset pipeline from Layout. A ui.TabBar builds every tab before the first frame, so without this every picture on every inactive tab competed at asset.Visible priority with the pictures the user is actually looking at, on a Pi 4 with one core of decode budget.

It must not be used to measure anything. A layout that depended on it would make a tab a different shape depending on whether it happens to be the selected one, and the tab would then have to be re-measured on the frame it becomes visible, which is a hitch on the one frame that must not have one.

func (*LayoutContext) Pass

func (l *LayoutContext) Pass() uint64

Pass returns the ordinal of the layout pass this call belongs to.

It increases once per App.Update that lays anything out, and it is constant for every layouter that runs inside that pass. Together with LayoutContext.Node it is what lets a layouter with external retained state distinguish "I am being laid out again, next frame" from "a second node is laying me out in the same frame". It is not a frame counter and not a time source, and it is deliberately not exposed as either.

func (*LayoutContext) Place

func (l *LayoutContext) Place(child int, at geom.Point)

Place positions the given child at the offset at, relative to the origin of the node being laid out. Absolute bounds are derived from these relative offsets once the whole tree has been measured.

func (*LayoutContext) Presenter

func (l *LayoutContext) Presenter() (geom.Rect, bool)

Presenter returns the rectangle, in device space, of the node that presents the overlay the node being laid out is in, and whether it is in one; see Element.Overlay. A popover places itself against it.

The top layer is laid out after the application, so the rectangle is the presenter's in this frame; when the presenter moved in the very layout that is running, the overlay is laid out once more in the same update with the new rectangle.

func (*LayoutContext) ReportBaseline

func (l *LayoutContext) ReportBaseline(v float32)

ReportBaseline records the distance from the top of this node to the baseline its content sits on, in logical pixels.

The channel, its writers and its reader

The project plan, section 7, makes baselines part of the shared layout contract, and this is the channel they travel through: a text node reports the first baseline of its paragraph, and a container that wants to align siblings on it reads LayoutContext.ChildBaseline.

The writers are the views that carry text: [ui.Text] reports the first baseline of its paragraph, ui.TextField the baseline of its line, and ui.Button passes on the baseline of its label. The reader is the stack algorithm in internal/layout, which a horizontal ui.Stack asks for with ui.Stack.AlignBaseline: it measures every child, takes the largest baseline, then places each child shifted by the difference. The channel was added before that algorithm existed, while a label was the only view with a baseline, so that the algorithm could later be added to the stack without changing any text view.

A layouter that never calls it reports no baseline, which is the right answer for a box: a rectangle has no baseline, and a container that guesses one — the bottom edge, say — would silently misalign every row it touched.

func (*LayoutContext) ReportOverflow

func (l *LayoutContext) ReportOverflow(over geom.Size)

ReportOverflow records that this node's content did not fit into the size it reported, by over logical pixels per axis.

Why this exists

The project plan, section 7, "Overflow-Modell", settles the question "the content does not fit" as follows: the children keep their honest sizes and positions, the container reports the size its constraints permit, nothing is clipped automatically, and the excess is carried as a number rather than swallowed. This method is where the number leaves the layout algorithm.

A layouter calls it at most once per Layouter.Layout call. Passing the zero size is how a container that used to overflow says it no longer does; a layouter that never calls it is treated as never overflowing.

Cost

It writes two floats and, when the value changed, adjusts two counters. It allocates nothing and takes no lock, so it is inside the zero allocation contract of the frame path.

func (*LayoutContext) ReportScrollContent

func (l *LayoutContext) ReportScrollContent(extent, origin float64)

ReportScrollContent tells gift how far the content of this scroll container reaches and where local coordinate zero sits in the document.

It may only be called by the layouter of a node whose Element declared a ScrollSpec; anything else is a contract violation and panics.

extent is the document extent along the scroll axis, which for ordinary content is simply the measured extent of the children. origin is the document coordinate that local coordinate zero of the children corresponds to, and for ordinary content it is zero.

Why origin exists before anything uses it

The project plan, section 10, requires that very large document positions are converted to float32 only after the viewport origin has been subtracted. A virtualising layouter — the masonry gallery — will place its visible items at local coordinates measured from a document origin it picks near the current offset, report that origin here, and gift then pushes a translation of origin - offset, which is a small number. The float32 conversion happens on the difference and nowhere else; see [scrollState].

The offset itself is always float64, so a caller that reports origin zero for a 30 000 000 pixel document still gets exact offsets, exact bounds and exact scroll-into-view arithmetic; only the translation it hands the GPU is then as coarse as float32 is at that magnitude, which is unavoidable when the content really is a single node that tall.

The offset is re-clamped here, so content that shrank under a resize pulls the viewport back rather than leaving it looking at nothing.

func (*LayoutContext) RequestBuild

func (l *LayoutContext) RequestBuild()

RequestBuild asks gift to rebuild the component instance that produced this node, in the next update.

It is the one thing a layouter cannot do for itself: change the *number* of its children. A virtualising container sizes its tile pool from the viewport, and the viewport is only known once the constraints have arrived, which is after the build that had to declare the tiles. So it lays out what it has, asks for a rebuild here, and has the right pool one update later.

The rebuild happens in the next App.Update, never inside this one: build during layout would mean a view function running while the tree it produces is being measured. The frame this is called in is therefore laid out with the old child count, which for a tile pool means one frame with fewer children than there is content to show.

What that frame looks like is the caller's problem and a real one. The obvious implementation — take the first n of the content — is wrong whenever the content is not produced in screen order, and a virtualised gallery's is not: WU-O measured a masonry gallery whose entire visible band collapsed into the leftmost 430 pixels of a 2400 pixel viewport for exactly this one frame, because the first n of a column grouped list is the first few columns. Thin the content across the pool instead; see ui's keepStride.

A layouter that calls this on every pass is an infinite rebuild loop and is caught by [gifttest.Harness.Settle]. Guard it with a comparison.

func (*LayoutContext) RequestLayout

func (l *LayoutContext) RequestLayout()

RequestLayout asks for another layout pass over this node in the next update, without rebuilding anything.

It exists for a layouter whose work is *incremental*: the gallery of the project plan, section 10, may not rebuild a 100 000 entry masonry index in one frame, so it advances the rebuild by a bounded chunk per pass and asks for the next pass from here. Section 10 requires exactly that — "Initiales Layout, Sortierung und Spaltenwechsel duerfen O(N) Arbeit benoetigen; sie werden ausserhalb des Frame-Hotpaths oder inkrementell berechnet".

A layouter that calls this unconditionally never lets the application idle and fails [gifttest.Harness.Settle] with a diagnosis, which is the intended outcome: "I am still working" must be a statement with an end.

Why the mark is deferred

Marking the node immediately would be undone three lines later: [App.layoutNode] clears scene.FlagNeedsLayout on the node as soon as its layouter returns, and the ancestors it would have marked are in the middle of being laid out and clear theirs on the way back up. The request is therefore queued and applied after the whole pass, which is the only point at which "again, next update" can be expressed at all.

func (*LayoutContext) Retained

func (l *LayoutContext) Retained[T any]() *T

Retained is EventContext.Retained for the node being laid out.

func (*LayoutContext) ScrollOffset

func (l *LayoutContext) ScrollOffset() float64

ScrollOffset returns the current offset of this scroll container, for a layouter that virtualises its content and therefore has to know which part of the document it is being asked to produce.

It panics for a node that is not a scroll container, for the same reason LayoutContext.ReportScrollContent does: reading zero from a node that has no offset would make a virtualising layouter silently produce the top of the document forever.

func (*LayoutContext) SetChildLabel

func (l *LayoutContext) SetChildLabel(child int, s string)

SetChildLabel replaces Element.Label of the given child until that child is built again. The index must be in [0, ChildCount).

It exists for a virtualising container whose pooled children are bound to items during layout rather than during build: a row of ui.VirtualList that scrolls from item 3 to item 40 is not rebuilt, so without this its label would keep naming item 3 and a test or an accessibility bridge would find the wrong row. The container's next build has to declare the same label again, or the build resets it; ui.VirtualList does so from its bindings.

Like the label itself it is read by nothing in the frame path and costs a string header.

func (*LayoutContext) SetControlState

func (l *LayoutContext) SetControlState(v ControlState)

SetControlState writes the control state of the node being laid out.

Why a layouter may write it and a painter may not

Because the layout pass is the one moment at which a control can notice that a *build* changed its value. A build produces a fresh view and a fresh node object, and neither of them can read what the previous value was — that is the whole reason this state exists — so the comparison has to happen somewhere that sees both the new declaration and the retained state. [App.applyElement] marks every rebuilt node for layout, so the layouter runs exactly when a value may have changed, and it runs before the frame in which the new value would be drawn.

A painter must not, and the prohibition is not a convention: Painter.Paint is documented as free of side effects, it runs with the display list open, and a painter that mutated state would make the picture depend on the order in which nodes happen to be visited. A painter reads this value and interpolates from it; see PaintContext.Now.

It marks nothing dirty. A layouter that starts an animation says so with LayoutContext.Animate, which is the call that keeps the frames coming.

type Layouter

type Layouter interface {
	// Layout measures the children through ctx and returns the size of this
	// node.
	//
	// It must measure and place every child exactly once. Measuring a child
	// twice is not detected as an error, but it doubles the cost of the
	// subtree; not measuring a child leaves it at its previous size, and not
	// placing it leaves it at its previous position.
	//
	// It must be a pure function of its constraints, its children and the
	// view it came from. gift caches the size a node returned for a given
	// set of constraints and returns it again, without calling Layout, as
	// long as neither the node nor anything below it was invalidated; see
	// [App.layoutNode].
	//
	// The returned size should satisfy c. A size that violates the
	// constraints is passed through unchanged, because clamping silently
	// would hide layout bugs.
	Layout(ctx *LayoutContext, c geom.Constraints) geom.Size
}

Layouter measures a node and places its children.

Layout is single phase, like Flutter: constraints go down, sizes come back up and the parent decides the positions. There is no separate arrange pass in which children could change their mind.

type Mods

type Mods uint8

Mods is the set of modifier keys held while an event happened.

const (
	// ModShift is held. gift itself reads it for reverse tab order.
	ModShift Mods = 1 << iota
	// ModControl is held.
	ModControl
	// ModAlt is held.
	ModAlt
	// ModMeta is the command or windows key.
	ModMeta
)

func (Mods) Has

func (s Mods) Has(m Mods) bool

Has reports whether every modifier in m is set in s.

type NodeRef

type NodeRef struct {
	// contains filtered or unexported fields
}

NodeRef refers to one node of the retained tree.

It is an opaque, comparable value: an application may hold it, compare it and ask the App about it, but it cannot construct one and it cannot reach into the node. The zero NodeRef refers to nothing.

It exists so that the few methods which have to name a node — App.HitTest, App.Focus — do not expose the handle type of an internal package in the public API.

func (NodeRef) IsZero

func (r NodeRef) IsZero() bool

IsZero reports whether r refers to no node.

type Options

type Options struct {
	// Logger is used outside the frame path only: construction, lifecycle
	// and error paths. A nil Logger means logging is off. gift never falls
	// back to slog.Default, because a library that logs into a destination
	// the application did not choose is a nuisance; see the project plan,
	// section 15.
	Logger *slog.Logger

	// Root is the root component function. It is mounted as a component with
	// the key "root" and therefore owns a state scope like any other
	// component.
	//
	// # The view it returns paints the window background, or nothing does
	//
	// gift draws exactly what the tree says. It has one display list, no
	// window chrome and no appearance of its own — it does not even know what
	// a colour role is — and Ebitengine clears the screen to transparent
	// black at the top of every Draw; see the project plan, section 6. A root
	// view that does not cover the viewport therefore ships a window with
	// holes in it: a composited hole on a desktop, and on a Raspberry Pi
	// kiosk whatever was in the framebuffer before the program started.
	//
	// The obligation is the application's and ui has the one line that meets
	// it:
	//
	//	gift.New(gift.Options{Root: func(ctx *gift.Context) gift.View {
	//		return ui.Window(content)
	//	}})
	//
	// gifttest's Harness.AssertOpaque is the assertion that it was written.
	// This is not a theoretical hazard: cmd/example-kitchensink shipped for
	// three work units with thirty to forty per cent of every screen at
	// {0, 0, 0, 0}, behind eight golden images that were all green, because
	// the test harness used to fill its own canvas first.
	Root func(*Context) View
}

Options configures a new App.

type PaintContext

type PaintContext struct {
	// contains filtered or unexported fields
}

PaintContext is the interface of a Painter to the display list.

There is exactly one instance per App; it is reused for every node and every frame and is valid only for the duration of a Painter.Paint call.

func (*PaintContext) Add

func (p *PaintContext) Add(op render.Op)

Add appends op to the display list.

The clip and transform indices of op are overwritten with the currently active ones, so a painter never has to know about the clip stack or about the scroll offset of a container above it. [Op.Bounds] therefore stays in the node's own local space and the backend applies the transform, which is what makes scrolling a matrix change rather than a rewrite of every rectangle.

func (*PaintContext) AddMaterial

func (p *PaintContext) AddMaterial(m render.Material) uint32

AddMaterial appends a material to the side table of the display list and returns its index, for render.Op.Material.

The usual sequence is one call immediately before the render.OpMaterial that references it, at the point of the node's drawing order where the background belongs — which is after the shadow and before the content. A material reads the backdrop, and the backdrop is whatever is already in the list; emitting it after the children would hand it a picture of them. See render.OpMaterial.

func (*PaintContext) Animate

func (p *PaintContext) Animate(d time.Duration)

Animate keeps the node being painted marked for repaint for the next d.

This is the unbounded case, and it is the only one

EventContext.Animate says that an animation whose end is not known "re-arms from its own handler or accepts that it stops". A determinate control has a handler: a toggle animates because something toggled it. An *indeterminate* progress indicator has none — nothing happens to it at all, which is the entire message it exists to convey — so the only place it can re-arm from is the frame it just drew.

The cost is exactly what the deadline elsewhere exists to prevent: a node that calls this on every frame holds the backend at its full tick rate for as long as it is on screen. That is acceptable only because it is what the widget *means*. An indeterminate indicator that is left on screen after the work finished is an application defect with a measurable price, and ui.ProgressBar says so in as many words.

It is a deadline like every other enrolment, so a node that stops calling it stops being drawn at the full rate one window later, and a node that is unmounted is dropped by [App.tickAnimations] on the next tick.

How a call from inside a paint can possibly work

It cannot work the way the other Animate methods do, and the difference is load bearing rather than incidental. Like them it calls [App.markNeedsPaint] on the node — but this one runs *during* App.Paint, and the last thing App.Paint does before returning the display list is set a.needsPaint back to false. So the dirty mark a painter makes is thrown away without exception, every single time.

What survives is the enrolment: the node and its deadline are appended to a.in.anims, and the next App.BeginInput runs App.tickAnimations, which marks every node whose deadline has not passed. That is the whole mechanism, and it is why d matters here even though this method is called again on the very next frame — a painter that stops calling it goes quiet once the last window it asked for has elapsed, not immediately.

The consequence for a caller is that this never repaints the frame it is called from, only the ones after it, and that it is useless outside a backend that calls BeginInput on a tick.

There is no paint culling in gift: App.Paint descends into every mounted node, so "while it is being drawn" and "while it is mounted" are the same window. A node that is scrolled out of the viewport keeps painting and keeps re-arming; see ui.ProgressBarView.

func (*PaintContext) AppendGlyph

func (p *PaintContext) AppendGlyph(g render.Glyph)

AppendGlyph appends one positioned glyph to the display list.

A text painter remembers PaintContext.GlyphsLen, appends the glyphs of its run and then emits a single render.OpGlyphs spanning them. The glyph is copied into the list here and now, which is what confines the borrowed shaping result to the frame that produced it; see render.List.AppendGlyph.

func (*PaintContext) AppendVertex

func (p *PaintContext) AppendVertex(v render.Vertex)

AppendVertex appends a vertex to the vertex side table of the display list; see render.OpTriangles.

func (*PaintContext) Audio

func (p *PaintContext) Audio() render.AudioOutput

Audio returns the backend's sound device, or nil when there is none.

func (*PaintContext) Bounds

func (p *PaintContext) Bounds() geom.Rect

Bounds returns the absolute rectangle of the node being painted, as computed by the last layout pass.

It is in *local* space, the space the operations a painter emits are interpreted in: an operation carries the index of the active transform, and the backend applies it. A painter that needs the device space rectangle — because it is about to push a clip, which is device space by the convention of the project plan, section 7 — asks PaintContext.DeviceBounds.

func (*PaintContext) ChildCount

func (p *PaintContext) ChildCount() int

ChildCount returns the number of children of the node being painted.

func (*PaintContext) ChildInteraction

func (p *PaintContext) ChildInteraction(i int) Interaction

ChildInteraction returns the Interaction of child i of the node being painted, the zero value for an index out of range.

It is for a container that draws something between its children which has to give way to their state: a list hides the hairlines on either side of a row the pointer is over, so that the row's face is not cut by them. A change of that state repaints the container, because a repaint of a node is a repaint of its ancestors.

func (*PaintContext) ControlState

func (p *PaintContext) ControlState() ControlState

ControlState returns the control state of the node being painted.

func (*PaintContext) DeviceBounds

func (p *PaintContext) DeviceBounds() geom.Rect

DeviceBounds returns the rectangle of the node being painted mapped through the transform that is active for it, which is the space clips live in.

Before scrolling existed every transform was the identity and this was the same rectangle as PaintContext.Bounds; ui.Text pushed its clip with the latter and was right by accident. Under a scroll transform it was wrong by the scroll offset — the label's glyphs were clipped against the rectangle the label occupied *before* the container scrolled — and the symptom was text that faded out in the wrong place.

func (*PaintContext) GlyphsLen

func (p *PaintContext) GlyphsLen() uint32

GlyphsLen returns the current length of the glyph side table of the display list, which is the index the next PaintContext.AppendGlyph writes to.

func (*PaintContext) Images

func (p *PaintContext) Images() render.Images

Images returns the backend's image resource service, or nil when there is none.

It is the only way a painter reaches the GPU side of the image pipeline, and it is deliberately available in Paint and nowhere else. Uploads are budgeted per *drawn* frame — the project plan, section 11 — and painting is the one thing that happens exactly once per drawn frame; a layouter that uploaded would spend the budget several times over for one frame, because Ebitengine may update more often than it draws.

A nil return is normal and is what a headless test sees. A painter must draw its placeholder for it rather than skipping the node.

func (*PaintContext) Interaction

func (p *PaintContext) Interaction() Interaction

Interaction returns the hover, press, focus and disabled state gift maintains for the node being painted.

This is how a control draws itself differently per state without a rebuild. The dispatcher wrote these bools during Update and marked the node for repaint; nothing was built and nothing was measured. See Interaction.

func (*PaintContext) Now

func (p *PaintContext) Now() time.Duration

Now returns the timestamp of the input phase this frame was drawn after, on the clock App.BeginInput is given.

It is the only clock a painter may read. Reading the wall clock instead would make every animation untestable — the project plan, section 13, requires deterministic time and gifttest moves this one — and would make two painters in one frame disagree about what "now" is.

A painter that uses it is almost certainly also a caller of EventContext.Animate; without that, the value only changes when something else in the application causes a frame.

func (*PaintContext) OverlayLeaving

func (p *PaintContext) OverlayLeaving() (float32, bool)

OverlayLeaving reports how far the overlay the node being painted is in has gone on its way out, from zero to one, and whether it is leaving at all: a withdrawn overlay stays on the screen for Element.OverlayExit so that it can animate away, and takes no input meanwhile.

func (*PaintContext) PaintChild

func (p *PaintContext) PaintChild(i int)

PaintChild paints the child with the given index. The index must be in [0, ChildCount).

func (*PaintContext) PaintChildren

func (p *PaintContext) PaintChildren()

PaintChildren paints all children of the current node, in order.

When the node declared Element.Clip the children are painted under that clip; see [PaintContext.paintKids].

func (*PaintContext) PopClip

func (p *PaintContext) PopClip()

PopClip removes the innermost clip pushed by this painter.

func (*PaintContext) PopTransform

func (p *PaintContext) PopTransform()

PopTransform ends the innermost PaintContext.PushTransform of this painter. Every push must be popped before the painter returns.

func (*PaintContext) Presenter

func (p *PaintContext) Presenter() (geom.Rect, bool)

Presenter is LayoutContext.Presenter for the node being painted.

func (*PaintContext) PushClip

func (p *PaintContext) PushClip(r geom.Rect)

PushClip intersects r, which is in device space, with the active clip and makes the result active for all following operations until the matching PaintContext.PopClip.

Device space, not local space. The project plan, section 7, fixes that convention: render.List keeps one clip stack and one intersection, and intersecting rectangles that live in different spaces produces nonsense. A painter that wants to clip to its own node uses PaintContext.DeviceBounds.

A painter must pop every clip it pushed before it returns. An unbalanced stack is a contract violation and panics.

func (*PaintContext) PushTransform

func (p *PaintContext) PushTransform(m geom.Affine2D)

PushTransform applies m to everything this painter adds from now on and to the children it paints, until the matching PaintContext.PopTransform. m maps the node's local space onto itself: a scale about the centre of PaintContext.Bounds makes a pressed button grow in place.

It moves the picture and not the node: hit testing, layout and the bounds a painter reads are unchanged, as they are for a transition.

func (*PaintContext) Retained

func (p *PaintContext) Retained[T any]() *T

Retained is EventContext.Retained for the node being painted.

A painter may create the value — a zero T is what it would have read anyway — but must not change it: painting is free of side effects, and a painter that wrote here would make the picture depend on the order nodes are visited in.

func (*PaintContext) ScrollIndicator

func (p *PaintContext) ScrollIndicator() (ScrollIndicatorState, bool)

ScrollIndicator returns the indicator state of the scroll container being painted, and false when this node is not one.

func (*PaintContext) ScrollInfo

func (p *PaintContext) ScrollInfo() (ScrollInfo, bool)

ScrollInfo returns the state of the scroll container being painted, and false when this node is not one.

func (*PaintContext) TransitionPhase

func (p *PaintContext) TransitionPhase() float32

TransitionPhase returns how far the subtree being painted is through a transition: 0 at rest, 1 fully parked.

It is the phase of the nearest ancestor that is transitioning, this node included, and it is how a painter draws something that has to *fade* rather than move — a modal's scrim, which cannot slide with its dialog because it covers the window. A painter that only has to move needs nothing from here; the movement is applied to its whole subtree by [App.paintNode].

It is zero in every application that declares no transition, which is the steady state, and reading it costs one field load.

func (*PaintContext) VerticesLen

func (p *PaintContext) VerticesLen() uint32

VerticesLen returns the number of vertices in the vertex side table of the display list: the first index of the next render.OpTriangles.

func (*PaintContext) Videos

func (p *PaintContext) Videos() render.Videos

Videos returns the backend's video picture service, or nil when there is none. It is PaintContext.Images for a picture that changes every frame, with the same rule: available in Paint and nowhere else, and nil is normal.

type Painter

type Painter interface {
	// Paint appends drawing operations to the display list of the current
	// frame.
	//
	// It is called once per drawn frame for every visible node. It must not
	// allocate, must not log and must not change any state, because doing so
	// would either break the zero allocation contract of the frame path or
	// build during paint. See the project plan, sections 11 and 15.
	//
	// Children are not drawn automatically. A painter that wants its
	// children on screen calls [PaintContext.PaintChildren] at the point in
	// its own drawing order where they belong, which is what makes
	// background, content and border ordering explicit.
	//
	// A node without a painter is the other case: a nil [Element.Painter]
	// paints all children in order and nothing else.
	Paint(ctx *PaintContext)
}

Painter emits the drawing operations of one node.

type PointerID

type PointerID int64

PointerID identifies one pointer.

The mouse is always MousePointer. A touch carries whatever identifier the platform assigned to the finger; gift only ever compares it for equality.

const MousePointer PointerID = -1

MousePointer is the identifier of the mouse pointer. Platform touch identifiers are non negative, so a negative constant cannot collide.

type PointerKind

type PointerKind uint8

PointerKind tells a mouse from a finger.

The project plan, section 7, asks for "ein gemeinsames Pointer-Modell" that abstracts mouse and touch, and in the same sentence forbids touch from synthesising hover states. Both halves live here: every pointer travels through the same events and the same capture rules, and the one place the two differ — hover — is a switch on this value in exactly one function, [App.updateHover].

const (
	// PointerMouse is a mouse or trackpad. It has a position even when no
	// button is down, and therefore a hover state.
	PointerMouse PointerKind = iota
	// PointerTouch is a finger. It exists only while it is down and never
	// produces a hover state.
	PointerTouch
)

func (PointerKind) String

func (k PointerKind) String() string

String returns "mouse" or "touch".

type ScrollAxis

type ScrollAxis uint8

ScrollAxis is the single axis a scroll container moves along.

One axis and not two. A two dimensional scroller needs a second offset, a second set of bounds and a rule for what a diagonal fling does, and the gallery of the project plan, section 10, is a column layout that scrolls one way. Two nested containers express the two dimensional case honestly.

const (
	// ScrollNone is the zero value and declares no scrolling at all.
	ScrollNone ScrollAxis = iota
	// ScrollVertical moves the content along y. The offset grows downwards:
	// an offset of 100 shows the content that starts 100 logical pixels
	// below the top of the document.
	ScrollVertical
	// ScrollHorizontal moves the content along x, with the offset growing
	// towards the trailing edge.
	ScrollHorizontal
)

func (ScrollAxis) String

func (a ScrollAxis) String() string

String returns "none", "vertical" or "horizontal".

type ScrollConfig

type ScrollConfig struct {
	// Friction is the exponential decay rate of a fling in 1/s; see the
	// documentation of [DefaultScrollFriction].
	Friction float32
	// FlingVelocity is the release speed below which no fling starts.
	FlingVelocity float32
	// StopVelocity is the speed at which a fling ends.
	StopVelocity float32
	// MaxVelocity caps the fling speed.
	MaxVelocity float32
	// WheelStep is the distance one unit of wheel delta scrolls, and
	// PreciseWheelStep the distance one unit of a precise wheel scrolls; see
	// [Event.Precise].
	WheelStep, PreciseWheelStep float32
	// HardStop turns off the rubber band. By default the content can be
	// pulled past either end with growing resistance and springs back when
	// it is let go, and a fling that reaches an end bounces off it, as on
	// iOS; see bounce.go. With HardStop it stops dead at the ends, as gift
	// did before.
	HardStop bool
}

ScrollConfig tunes the gesture behaviour of one scroll container. A zero field takes the corresponding package default, and so does a negative or non finite one.

type ScrollIndicatorState

type ScrollIndicatorState struct {
	// Grabbed says the indicator's thumb is currently being dragged.
	Grabbed bool
	// Grab is where along the axis inside the thumb it was taken hold of, in
	// the space [Event.Pos] lives in. Keeping that offset — rather than
	// recentring the thumb on the pointer — is what makes the content not
	// jump at the moment of the press. It is meaningless while Grabbed is
	// false.
	Grab float32
	// Hover says the pointer is resting over the indicator, which is what
	// lets a faded decoration wake up before it is clicked.
	Hover bool
}

ScrollIndicatorState is the presentation state of the decoration that draws a scroll container's position — the grab of its thumb and the hover of its track. It is read with PaintContext.ScrollIndicator and written with EventContext.SetScrollIndicator.

Why the core holds it

Because gift, and not the decoration, owns the lifetime it has to have. A scroll bar is drawn and grabbed by the container itself — see the accessors below for why it cannot be a child — so the only object the decoration can keep state in is the one a build hands to Element.Layouter, and that object is *replaced* on every rebuild. A component that rebuilds from a timer while the user holds the thumb would therefore lose the grab in the middle of the gesture, and the next move would fall through to ScrollInteractor and be read as a content drag — which moves the content the opposite way from the thumb. The scroll offset is kept across a rebuild for exactly the same reason (see [App.applyScroll]), and the grab that drives the offset cannot be held to a weaker standard than the offset.

So it lives here, beside the offset, in the state object a rebuild preserves. gift does not interpret any of these fields: it stores them, hands them back, and treats a change to them as activity — see EventContext.SetScrollIndicator.

func (ScrollIndicatorState) Active

func (s ScrollIndicatorState) Active() bool

Active reports whether the indicator is engaged by the pointer in either sense. A decoration that fades on idleness stays fully visible while this is true; both halves are the same rule and separating them is how one of the two ends up without hysteresis.

type ScrollInfo

type ScrollInfo struct {
	// Axis is the axis the container scrolls along.
	Axis ScrollAxis
	// Offset is the document coordinate at the leading edge of the viewport.
	Offset float64
	// MaxOffset is the largest legal offset, that is the content extent
	// minus the viewport extent, or zero when the content is smaller.
	MaxOffset float64
	// ContentExtent is the document extent along the axis, as reported by
	// the layouter.
	ContentExtent float64
	// ContentOrigin is the document coordinate of local coordinate zero of
	// the children; see [LayoutContext.ReportScrollContent].
	ContentOrigin float64
	// ViewportExtent is the extent of the container along the axis.
	ViewportExtent float32
	// Velocity is the current fling velocity in document units per second,
	// and Flinging says whether a fling is running.
	Velocity float32
	Flinging bool
	// Dragging says whether a pointer drag is currently driving this
	// container.
	Dragging bool
	// Virtual mirrors [ScrollSpec.Virtual].
	Virtual bool
	// Overscroll is how far the content is displaced past an end by the
	// rubber band, a hold or a bounce, in offset units: negative past the
	// leading end, where a pull-to-refresh spinner shows, positive past the
	// trailing one.
	Overscroll float32

	// IdleFor is how long ago this container last moved, measured on the
	// clock passed to [App.BeginInput].
	//
	// It is what a scroll indicator fades on: visible while the content
	// moves, held for a moment afterwards so the user can see where they
	// are, then gone. A container that has never moved reports a very large
	// duration rather than zero, so "has been idle for longer than the hold"
	// is true from the first frame and a bar does not flash on at startup.
	//
	// It is not a substitute for Dragging and Flinging. A drag that is held
	// still, or a fling that has reached the end of the document, moves
	// nothing and would otherwise start fading under the user's finger.
	IdleFor time.Duration
}

ScrollInfo is a snapshot of the state of one scroll container.

type ScrollSpec

type ScrollSpec struct {
	// Axis is the axis the content moves along. [ScrollNone] declares no
	// viewport at all and is rejected at build time.
	Axis ScrollAxis
	// Config tunes the gesture constants of this container.
	Config ScrollConfig

	// Virtual declares that this container's layouter *produces* its content
	// from the scroll offset instead of merely being translated by it.
	//
	// # What it changes
	//
	// An ordinary scroll container lays its children out once and then moves
	// them with a matrix; changing the offset marks the node for repaint and
	// nothing else, which is the whole claim of the scroll fast path. A
	// virtualising container — the gallery of the project plan, section 10 —
	// has no node for most of its content: it decides on every offset which
	// of its bounded set of tiles stands for which item, and that decision is
	// made in [Layouter.Layout] through [LayoutContext.ScrollOffset]. For
	// such a container an offset change must invalidate *layout*, or the
	// tiles would keep standing for the items they stood for at the offset
	// the last layout saw.
	//
	// So: with Virtual set, [App.setScroll] marks the node for layout rather
	// than only for paint. Nothing is rebuilt either way — no view function
	// runs, [Diagnostics.Builds] does not move — and the layout that follows
	// descends along the marked path only, so no sibling subtree is touched.
	//
	// # The honest price
	//
	// It is *not* the depth of the tree plus this one layouter, which is what
	// this paragraph claimed until WU-O. The container's own layouter runs,
	// and so does the layouter of every child whose constraints changed,
	// because [App.layoutNode] only answers from its cache when the incoming
	// constraints are identical to the last ones. A virtualising container
	// measures its children with tight constraints derived from the content,
	// so any child that changed size runs — and for the masonry gallery of
	// the project plan, section 10, a scroll changes the height of most tiles
	// in the band, because the tile at a given slot now stands for a
	// different picture.
	//
	// The bound is therefore: the depth of the marked path, plus one, plus
	// one per child of the container. All three terms are bounded by the
	// viewport and none of them by the item count, which is the property that
	// matters and the one TestGalleryScrollPathCost asserts. Measured there
	// at 800x600: twelve layouters for a pool of fourteen, identically at a
	// hundred and at a hundred thousand entries.
	//
	// # Why it is opt in
	//
	// Because relayouting on every wheel notch is exactly what an ordinary
	// scroller must not do, and a default that did it would quietly undo the
	// property ui.ScrollView documents and gifttest asserts. A container that
	// does not read [LayoutContext.ScrollOffset] has nothing to gain from it.
	Virtual bool

	// LeadingHold makes the content rest pulled past its leading end by this
	// many offset units instead of springing back to it, for as long as it is
	// set: the room a pull-to-refresh spinner stays in while the refresh
	// runs, as UIKit's refresh control keeps it with a content inset. The
	// content moves there from wherever it is with the spring of the rubber
	// band, and back when the hold goes to zero. A finger takes hold of it
	// where it shows. It needs the rubber band and does nothing with
	// [ScrollConfig.HardStop].
	LeadingHold float32
}

ScrollSpec declares a node to be a scroll viewport. It is Element.Scroll.

type State

type State[T comparable] struct {
	// contains filtered or unexported fields
}

State is a typed, observable value owned by one component instance.

The element type is constrained to comparable because change detection uses ==. This is deliberate: a deep equality default would make every write unpredictably expensive and would silently accept in place mutation of maps and slices, which cannot be detected at all. Values that are not comparable get an explicit comparator or version based path later; see the project plan, section 5.

A state lives until its scope is unmounted, not until a build stops looking it up. A cached subtree that is not rebuilt keeps its state.

func (*State[T]) Binding

func (s *State[T]) Binding() Binding[T]

Binding returns a read/write handle to this state.

A binding does not transfer ownership: the state stays with its scope and outlives every binding handed to a control.

func (*State[T]) Get

func (s *State[T]) Get() T

Get returns the current value without registering a dependency.

This is the right call inside an event handler, where reading a value must not subscribe anything to it. Inside a build, use Context.Read.

func (*State[T]) Set

func (s *State[T]) Set(v T)

Set stores v and invalidates every scope that read this state in its last build.

Writing the same value again, compared with ==, changes nothing and invalidates nothing. A real change marks the dependent scopes as needing a build; the rebuild happens in the next App.Update, not inside Set. Several writes before the next update are therefore coalesced into one rebuild.

Calling Set from an event handler is the normal case, and that path is allocation free: the UI executor check is compiled only into a build with the giftdebug tag, because obtaining a goroutine id means parsing the runtime stack header. Calling Set from another goroutine is a programming error; with the tag it panics, without it the race detector is the tool that finds it. Use App.Post to hand a worker result to the UI executor.

Calling Set during App.Paint is a contract violation and panics in every build, because it would mean building during draw.

type Token

type Token struct {
	// contains filtered or unexported fields
}

Token identifies one asynchronous request of one component instance.

A worker result must not be applied blindly. Between starting a request and its completion the component may have been unmounted, or it may have started a newer request whose answer supersedes this one. Applying a stale result then shows the wrong image in a recycled tile or resurrects the state of a dead scope. The project plan, sections 5 and 9, keeps content version, request generation and slot generation apart for exactly this reason.

The pattern is:

tok := ctx.Request()
go func() {
    v := expensive()
    app.Post(func() {
        if !tok.Valid() {
            return // unmounted, or a newer request won
        }
        state.Set(v)
    })
}()

Token.Post is the same pattern in one call, for a component that does not hold the App:

tok := ctx.Request()
go func() {
    v := expensive()
    tok.Post(func() { state.Set(v) })
}()

The zero Token is never valid.

func (Token) Post

func (t Token) Post(fn func())

Post hands fn to the UI executor, like App.Post, and runs it there only if the token is still Token.Valid by then: the result of a request that was replaced, or whose component was unmounted, is dropped.

It may be called from any goroutine. For the zero Token it does nothing.

func (Token) Valid

func (t Token) Valid() bool

Valid reports whether the result of this request may still be applied: the component instance is still mounted and no newer request has been started on it.

It must be called on the UI executor, which is where a posted closure runs.

type TransitionSpec

type TransitionSpec struct {
	// Parked is the offset of the node while it is hidden, as a fraction of
	// its own size.
	Parked geom.Point

	// Entry is where the node comes from when it is mounted visible into an
	// application that has already drawn a frame. The zero value means
	// Parked.
	Entry geom.Point

	// Return is where the node comes from when it is *shown again* after
	// having been hidden. The zero value means "from where it went", which is
	// the honest answer and is what a tab bar wants.
	//
	// It exists for the one case where the honest answer is unusable: a
	// navigation stack parks a covered screen a full width away, because the
	// screen that covers it has to be able to slide over it without the two
	// overlapping, and a screen that came back from a full width away would
	// leave the whole window empty for the first frame of a pop — the screen
	// that *was* on top is unmounted by then and cannot fill it. A node at
	// phase 1 is not painted at all, so moving where it waits while it is
	// there is invisible by construction.
	Return geom.Point

	// Duration is how long the movement from one end to the other takes.
	// Zero disables the transition.
	Duration time.Duration
}

TransitionSpec declares where a node sits while it is hidden, so that Element.Hidden becomes a movement instead of a cut.

The zero value is the whole of the previous behaviour: a node with no transition appears and disappears in one frame.

// a tab to the right of the selected one waits off the trailing edge
e.Transition = gift.TransitionSpec{
	Parked:   geom.Pt(1, 0),
	Duration: 180 * time.Millisecond,
}

The model: one number, and where the node is at each end of it

A node carrying a spec has a *phase*: 0 while it is shown, 1 while it is hidden, and everything in between while it is moving from one to the other. The phase is a pure function of the retained state and the clock — the same design as ControlState and for the same reasons — so the painter reads it, nothing writes it per frame, and two nodes moving at once are exactly in step because they read one clock.

Parked is where the node is at phase 1, as a fraction of *its own size*: {X: 1} is one full width towards the trailing edge, {X: -0.12} is an eighth of a width towards the leading edge, {Y: 1} is one full height down. A fraction and not a length, because the thing that moves is a screen and the distance that reads as "off the screen" is the size of the screen.

Duration is how long the movement takes. Zero means no transition at all, whatever Parked says: a spec is opt-in on both fields.

What it costs while it runs, and what it costs afterwards

While a transition runs, the node is painted *even though it is hidden*. That is the point — a screen that is sliding out has to be on the screen — and it is a real, bounded cost: for Duration, an outgoing tab costs a full paint of its subtree per frame. When the phase reaches 1 the node stops being painted and the guarantee Element.Hidden makes is back in force unchanged: a hidden tab costs one field read per frame and nothing else. Nothing here weakens that guarantee, it only postpones it by Duration.

The repaint enrolment is bounded by Duration and is dropped by [App.tickAnimations] like every other one, so a transition always ends and the application always goes idle afterwards.

It allocates once per node that has ever transitioned — the retained state is behind a pointer so that the nodes that never transition, which is almost all of them, carry eight bytes — and nothing per frame.

Input does not move with the picture, and that is deliberate

Element.Transform is documented as shared between the two halves of a frame so that input and output cannot disagree about where a node is. This is the one transform that is not: the hit test ignores the phase entirely. A node that is hidden answers no tap from the first frame of its exit even though it is still drawn, and a node that is appearing answers taps at its *final* position from the first frame of its entrance even though it is still drawn short of it.

The alternative was considered and is worse for the target of this project. A kiosk user who taps during a transition is aiming at the screen that is arriving, and hit testing the moving picture would mean the target under their finger is a different one three frames later — a lottery whose odds change during the gesture. The cost of this choice is a window of at most Duration in which a tap near the leading edge lands on a control that has not visibly arrived there yet. At 180 ms that is measurable and, on a touchscreen, preferable to delaying the input or to aiming at a moving target. A transition must never make the machine feel slower than the cut it replaced.

Two ends and a memory

Parked is where the node goes when it is hidden. Entry is where it comes *from* when it is mounted already visible into an application that is already on the screen, which is what a navigation push is; the zero Entry means "the same place as Parked", which is what every other caller wants.

The two are not the same for a navigation stack and that is the whole reason Entry exists: a screen that is pushed arrives from the trailing edge and a screen that is *covered* waits a little way towards the leading one, so the place a screen came from and the place it will wait are on opposite sides of the window. The node remembers which place it is actually parked at, so a screen that is uncovered again comes back from the side it went to rather than from the side a freshly pushed screen would arrive from.

Interrupting it

A build that flips Element.Hidden back while a transition is running turns the node round from wherever it is at that instant rather than jumping to the end first; that is [retargetTransition], and it is the rule [retarget] already applies to a control whose switch is flicked twice.

func (TransitionSpec) IsZero

func (s TransitionSpec) IsZero() bool

IsZero reports whether the spec asks for no movement at all.

type TypeID

type TypeID uint32

TypeID is a process wide stable identifier of a concrete view type.

It replaces reflection in the reconciliation path: comparing two uint32 values decides whether an existing node can be updated or has to be remounted. IDs are assigned in registration order and are therefore stable within a process, but not across processes. Never persist them.

The zero TypeID is invalid and belongs to no type.

func RegisterType

func RegisterType(name string) TypeID

RegisterType registers a view type under the given name and returns its TypeID. It is meant to be called from a package level variable initialiser, once per concrete view type:

var textType = gift.RegisterType("ui.Text")

Registering the same name twice panics. A duplicate name means two view types would be considered interchangeable by the reconciler, which silently preserves the wrong node and the wrong state.

type ValueReporter

type ValueReporter interface {
	AccessibleValue(st ControlState) string
}

ValueReporter is implemented by an Interactor whose node edits a value, so that a tool outside the frame path can read what the control holds.

Element.Label is the name of a control and deliberately not its value; see ui.TextFieldView. The value is asked for here instead, and asked for at the moment somebody wants it rather than copied into the element during a build, because a control that is typed into changes its value between builds: the digits of a half typed time are in ControlState and nowhere else. That is also why the control state is passed in.

type View

type View interface {
	// ViewType returns the process wide stable ID of the concrete view type.
	//
	// Two views with different ViewType under the same key force a remount
	// instead of an update, which discards the node and the state below it.
	ViewType() TypeID

	// Build describes the element this view stands for. It is called during
	// reconciliation and never during paint.
	//
	// The returned Element is not retained beyond the call, except for the
	// children slice it references; see [Element.Children]. Build must be
	// free of side effects: the reconciler may call it before it has decided
	// whether the resulting element updates an existing node or mounts a new
	// one.
	Build(*BuildContext) Element
}

View is a short lived description of a piece of user interface.

A view is a value, not an object with identity. It is created during a build, consumed by the reconciler and then dropped. Durable identity lives in the scope and in the retained node tree, never in a copy of a view struct. Views are never evaluated in the frame hot path; see the project plan, sections 4 and 6.

func Component

func Component(key string, fn func(*Context) View) View

Component mounts fn as a stateful component instance with a state scope of its own.

A plain helper function that returns a View does not create a scope: it is inlined into the build of its caller and its state, if it used any, would belong to the caller. Only a component introduces the identity that state and dependency tracking are attached to. See the project plan, section 4.

Rebuild semantics, stated honestly

Component has no rebuild boundary of its own. Its props, if it has any, are captured in the closure fn, and gift cannot compare function values, so it cannot tell whether the inputs of this instance changed. Consequently *every* rebuild of an enclosing scope also rebuilds this one, all the way down. What Component does give you is the other direction: a state write inside this instance rebuilds this instance only, not its parent.

When the top down direction matters — a deep tree whose root invalidates often — use Memo, which makes the props explicit and comparable and skips the rebuild when they did not change.

The key identifies the instance among its siblings and must be stable across builds. Two calls with the same key in the same parent refer to the same instance, even if the function differs; gift cannot compare function values, so changing the function under a stable key keeps the existing state instead of remounting.

func Memo

func Memo[P comparable](key string, props P, fn func(*Context, P) View) View

Memo mounts a stateful component that is only rebuilt when props changes or when one of its own state dependencies fires.

It is Component with the top down rebuild boundary that Component cannot have. The inputs of the instance are passed as an explicit comparable value instead of being captured in a closure, so gift can compare them with ==: when they are equal to the props of the previous build and no state this instance read has changed, the component function is not called at all and the whole subtree below it is left untouched. That is the "Build: kein Aufruf der View-Funktionen" row of the project plan, section 6.

P must be comparable for the same reason State requires it: a deep equality default would make every build unpredictably expensive and would silently accept in place mutation. A struct of the values the component actually reads is the intended shape:

gift.Memo("row", rowProps{ID: id, Selected: sel}, renderRow)

Do not put a slice, a map or a freshly allocated closure in P. A slice, a map or a func field makes P not comparable, and the call will not compile; the same values behind an interface typed field do compile, and the comparison then panics at runtime. Both are the signal that the value belongs in state or behind a stable pointer instead.

The key has the same meaning as in Component. Changing the props type under a stable key is a programming error and is rejected, because the instance would otherwise silently keep state that belongs to a different component.

type WheelPhase

type WheelPhase uint8

WheelPhase is the stage of a trackpad gesture a wheel delta belongs to, for a backend whose platform reports it; see App.PointerWheelPhase.

const (
	// WheelPhaseUnknown is a delta whose stage nobody reported: a mouse
	// wheel, or a trackpad behind GLFW, which drops the phases macOS has.
	// A trackpad then stretches a container past its end for as long as the
	// deltas come, and it springs back once they have stopped.
	WheelPhaseUnknown WheelPhase = iota
	// WheelPhaseContact is a delta of fingers on the trackpad.
	WheelPhaseContact
	// WheelPhaseLift says that the fingers left the trackpad. It carries no
	// delta of its own.
	WheelPhaseLift
	// WheelPhaseMomentum is a delta of the momentum the platform adds after
	// the fingers left the trackpad, decaying until it stops.
	WheelPhaseMomentum
)

Directories

Path Synopsis
Package asset is the catalogue and the image pipeline of gift: stable identities, metadata, the ordered collection a gallery is a view of, and the application wide service that turns a path or a URL into a decoded, oriented, cached thumbnail.
Package asset is the catalogue and the image pipeline of gift: stable identities, metadata, the ordered collection a gallery is a view of, and the application wide service that turns a path or a URL into a decoded, oriented, cached thumbnail.
turbojpeg
Package turbojpeg decodes JPEG through libjpeg-turbo's TurboJPEG API, loaded at run time with purego, and decodes it *scaled* when a thumbnail is all that is wanted.
Package turbojpeg decodes JPEG through libjpeg-turbo's TurboJPEG API, loaded at run time with purego, and decodes it *scaled* when a thumbnail is all that is wanted.
Package auto is an opt-in, out-of-band automation interface for a running gift application: HTTP on loopback, in the real process, driving the real window.
Package auto is an opt-in, out-of-band automation interface for a running gift application: HTTP on loopback, in the real process, driving the real window.
backend
ebiten
Package ebiten is gift's Ebitengine backend: the window, the frame loop and the GPU output.
Package ebiten is gift's Ebitengine backend: the window, the frame loop and the GPU output.
Package clipboard connects ui.Clipboard to the clipboard of the operating system, without cgo.
Package clipboard connects ui.Clipboard to the clipboard of the operating system, without cgo.
cmd
example-counter command
Command example-counter is the first gift program to read: the counter from the project plan, section 4, with the styling and the keyboard handling that make it a real control rather than a click handler.
Command example-counter is the first gift program to read: the counter from the project plan, section 4, with the styling and the keyboard handling that make it a real control rather than a click handler.
example-effects command
Command example-effects is the effects demonstration of the project plan, section 12, step 5: border, shadow and a glass panel over the gallery.
Command example-effects is the effects demonstration of the project plan, section 12, step 5: border, shadow and a glass panel over the gallery.
example-gallery command
Command example-gallery is the virtualised image gallery of the project plan, section 10, with the complete image pipeline of section 12, step 4: real files, real HTTP, cold loading, errors and a thumbnail cache that survives a restart.
Command example-gallery is the virtualised image gallery of the project plan, section 10, with the complete image pipeline of section 12, step 4: real files, real HTTP, cold loading, errors and a thumbnail cache that survives a restart.
example-kiosk command
Command example-kiosk is a gift program as the operating system of a device: a small counter of workpieces for a workbench, and the settings of the device after those of iOS, with Wi-Fi, Ethernet with fixed addresses, software updates, storage, the startup and a passcode.
Command example-kiosk is a gift program as the operating system of a device: a small counter of workpieces for a workbench, and the settings of the device after those of iOS, with Wi-Fi, Ethernet with fixed addresses, software updates, storage, the startup and a passcode.
example-kitchensink command
Command example-kitchensink shows every component of gift: a list grouped like the documentation, and beside it, or after it on a narrow screen, the component with the demo of its documentation page, which is live and can be played with.
Command example-kitchensink shows every component of gift: a list grouped like the documentation, and beside it, or after it on a narrow screen, the component with the demo of its documentation page, which is live and can be played with.
example-maps command
Command example-maps shows a map of ui/maps: a self hosted PMTiles archive of OpenStreetMap, or the debug tiles, in a window of its own.
Command example-maps shows a map of ui/maps: a self hosted PMTiles archive of OpenStreetMap, or the debug tiles, in a window of its own.
example-notify command
Command example-notify shows notifications of the operating system with gift/notify: one that is shown, updated under the same ID and withdrawn, and a count of the clicks on it.
Command example-notify shows notifications of the operating system with gift/notify: one that is shown, updated under the same ID and withdrawn, and a count of the clicks on it.
example-photos command
Command example-photos is a photo browser in the manner of iOS's Photos: a gallery of thumbnails, and a full screen viewer that grows out of the tile that was tapped and shrinks back into it.
Command example-photos is a photo browser in the manner of iOS's Photos: a gallery of thumbnails, and a full screen viewer that grows out of the tile that was tapped and shrinks back into it.
example-tray command
Command example-tray puts an icon into the menu bar of macOS, the notification area of Windows or the status area of a Linux desktop with gift/tray, with a menu that uses everything the package offers: entries with a symbol, a check mark that moves, a disabled entry, separators, a submenu two levels deep and Quit.
Command example-tray puts an icon into the menu bar of macOS, the notification area of Windows or the status area of a Linux desktop with gift/tray, with a menu that uses everything the package offers: entries with a symbol, a check mark that moves, a disabled entry, separators, a submenu two levels deep and Quit.
example-typst command
Command example-typst shows Typst as gift's layout engine for paged documents: Typst sets the pages, gift draws them, and the document can be edited in its source and on its pages alike.
Command example-typst shows Typst as gift's layout engine for paged documents: Typst sets the pages, gift draws them, and the document can be edited in its source and on its pages alike.
example-video command
Command example-video plays a video with sound and the touch controls of ui/videoplayer, full window: the program to try on a Raspberry Pi with a five or seven inch touch panel.
Command example-video plays a video with sound and the touch controls of ui/videoplayer, full window: the program to try on a Raspberry Pi with a five or seven inch touch panel.
gift-glassbench command
Command gift-glassbench measures what glass costs on the target hardware.
Command gift-glassbench measures what glass costs on the target hardware.
gift-icongen command
Command gift-icongen turns a directory of SVG icons into an embedded blob and a Go file of package level variables.
Command gift-icongen turns a directory of SVG icons into an embedded blob and a Go file of package level variables.
gift-stress command
Command gift-stress puts the stress scene of internal/stress into a window.
Command gift-stress puts the stress scene of internal/stress into a window.
gift-touchinject command
Command gift-touchinject plays touch gestures on a virtual touch screen, for testing multitouch on a Linux machine that has no touch screen.
Command gift-touchinject plays touch gestures on a virtual touch screen, for testing multitouch on a Linux machine that has no touch screen.
gift-videoproto command
Command gift-videoproto is the feasibility prototype of gift's video player: it plays one file with sound in an Ebitengine window and prints, once a second, what the project plan, section 25, needs measured.
Command gift-videoproto is the feasibility prototype of gift's video player: it plays one file with sound in an Ebitengine window and prints, once a second, what the project plan, section 25, needs measured.
giftbundle command
Command giftbundle runs a Go program as a macOS app bundle.
Command giftbundle runs a Go program as a macOS app bundle.
docs
apigen command
Command apigen writes the API tables of the documentation site.
Command apigen writes the API tables of the documentation site.
capture command
Command capture renders the screenshots of the documentation site.
Command capture renders the screenshots of the documentation site.
gallery command
Command gallery shows one component demo of the documentation per window.
Command gallery shows one component demo of the documentation per window.
font
Package geo is positions on the earth and the Web Mercator projection gift's maps draw them in.
Package geo is positions on the earth and the Web Mercator projection gift's maps draw them in.
Package geom provides the numeric geometry primitives shared by the layout, input and rendering layers of gift.
Package geom provides the numeric geometry primitives shared by the layout, input and rendering layers of gift.
Package gifttest drives a gift application from a Go test.
Package gifttest drives a gift application from a Go test.
icon
outline
Package outline embeds the Flowbite outline icon set as ui.Symbol values.
Package outline embeds the Flowbite outline icon set as ui.Symbol values.
solid
Package solid embeds the Flowbite solid icon set as ui.Symbol values.
Package solid embeds the Flowbite solid icon set as ui.Symbol values.
internal
dbus
Package dbus is the D-Bus client of gift: the wire format, a connection to the session bus, and the exported objects that let other programs call into this one.
Package dbus is the D-Bus client of gift: the wire format, a connection to the session bus, and the exported objects that let other programs call into this one.
example
Package example holds the plumbing the three example programs share.
Package example holds the plumbing the three example programs share.
example/components
Package components holds one small, deterministic scene per component of the ui package: a list, a virtual list, a sheet, a row, a card, a badge, a divider, a toggle, a slider, a segmented control and a progress bar.
Package components holds one small, deterministic scene per component of the ui package: a list, a virtual list, a sheet, a row, a card, a badge, a divider, a toggle, a slider, a segmented control and a progress bar.
exifwrite
Package exifwrite writes JPEG files with the EXIF of a photograph: where and when it was taken.
Package exifwrite writes JPEG files with the EXIF of a photograph: where and when it was taken.
gribwrite
Package gribwrite writes GRIB edition 2 messages of the one kind package weather/grib2 reads — a regular latitude and longitude grid with simple packing and an optional bitmap — for the tests of the decoder and of the weather providers, which need fields nobody has to download.
Package gribwrite writes GRIB edition 2 messages of the one kind package weather/grib2 reads — a regular latitude and longitude grid with simple packing and an optional bitmap — for the tests of the decoder and of the weather providers, which need fields nobody has to download.
icon
Package icon rasterises pre-parsed icon outlines into coverage masks.
Package icon rasterises pre-parsed icon outlines into coverage masks.
iconsvg
Package iconsvg parses the small subset of SVG the icon corpora use and turns it into the byte format of internal/icon.
Package iconsvg parses the small subset of SVG the icon corpora use and turns it into the byte format of internal/icon.
layout
Package layout contains the renderer neutral layout algorithms of gift.
Package layout contains the renderer neutral layout algorithms of gift.
sampleclip
Package sampleclip is three seconds of Big Buck Bunny with two audio tracks, for the demos, the examples and the tests that need a video; see README.md for its licence and how it was cut.
Package sampleclip is three seconds of Big Buck Bunny with two audio tracks, for the demos, the examples and the tests that need a video; see README.md for its licence and how it was cut.
scene
Package scene provides the retained node storage of gift.
Package scene provides the retained node storage of gift.
showcase
Package showcase holds one small demo of every component of gift: the code that draws the screenshots of the component pages, and what the kitchen sink example shows.
Package showcase holds one small demo of every component of gift: the code that draws the screenshots of the component pages, and what the kitchen sink example shows.
stress
Package stress holds the several hundred node windowed scene that the project plan, section 12, "Go/No-Go vor Schritt 2", criterion 2, requires: a non trivial static stack scene that a window can hold at sixty frames per second on a Raspberry Pi 4.
Package stress holds the several hundred node windowed scene that the project plan, section 12, "Go/No-Go vor Schritt 2", criterion 2, requires: a non trivial static stack scene that a window can hold at sixty frames per second on a Raspberry Pi 4.
text
Package text contains the renderer neutral text stack of gift: font loading, shaping, measurement and line breaking.
Package text contains the renderer neutral text stack of gift: font loading, shaping, measurement and line breaking.
system
Package system is the operating system of a device that runs a gift program as its kiosk, as far as a settings app needs it: what the device is, its storage, its network, its updates, how it starts, its screen and its clock.
Package system is the operating system of a device that runs a gift program as its kiosk, as far as a settings app needs it: what the device is, its storage, its network, its updates, how it starts, its screen and its clock.
Package metrics is the measurement half of gift: frame timings, the counters of the frame path and the machine readable report the project plan, section 13, asks for.
Package metrics is the measurement half of gift: frame timings, the counters of the frame path and the machine readable report the project plan, section 13, asks for.
Package notify shows notifications of the operating system: the messages that appear outside the application's window, in the notification centre of the desktop and, if the user allows it, on the lock screen.
Package notify shows notifications of the operating system: the messages that appear outside the application's window, in the notification centre of the desktop and, if the user allows it, on the lock screen.
Package render defines the renderer neutral drawing contract of gift.
Package render defines the renderer neutral drawing contract of gift.
Package tray puts an icon into the menu bar of macOS, the notification area of the Windows taskbar and the status area of a Linux desktop, with a menu that opens when the user clicks it.
Package tray puts an icon into the menu bar of macOS, the notification area of the Windows taskbar and the status area of a Linux desktop, with a menu that opens when the user clicks it.
Package typst sets paged documents with Typst and hands the pages to gift to draw.
Package typst sets paged documents with Typst and hands the pages to gift to draw.
embedded
Package embedded carries libgifttypst inside the program, so that package typst works without a library installed next to it.
Package embedded carries libgifttypst inside the program, so that package typst works without a library installed next to it.
ui
Package ui is the public view layer of gift.
Package ui is the public view layer of gift.
charts
Package charts draws charts after Swift Charts: marks on a pair of axes, with a legend, an inspection of the values under the pointer, and zoom.
Package charts draws charts after Swift Charts: marks on a pair of axes, with a legend, an inspection of the values under the pointer, and zoom.
filepicker
Package filepicker is a file picker made of gift views: it opens one or several files, chooses a directory or picks a path to save to, inside the application's own window and on a touchscreen as well as with a mouse and a keyboard.
Package filepicker is a file picker made of gift views: it opens one or several files, chooses a directory or picks a path to save to, inside the application's own window and on a touchscreen as well as with a mouse and a keyboard.
imageviewer
Package imageviewer shows pictures as large and as interactive as the screen allows: a picture that zooms and pans in place, and a full screen viewer that pages through a catalogue, in the manner of iOS's Photos.
Package imageviewer shows pictures as large and as interactive as the screen allows: a picture that zooms and pans in place, and a full screen viewer that pages through a catalogue, in the manner of iOS's Photos.
motion
Package motion is the arithmetic of things that move under a finger and after it: the rubber band past an edge, the deceleration of a fling, and the spring that brings something home.
Package motion is the arithmetic of things that move under a finger and after it: the rubber band past an edge, the deceleration of a fling, and the spring that brings something home.
qr
Package qr encodes text as a QR code and shows it: a link a phone opens with its camera, the way a kiosk hands a login or a payment to the customer's own device (see web/handoff).
Package qr encodes text as a QR code and shows it: a link a phone opens with its camera, the way a kiosk hands a login or a payment to the customer's own device (see web/handoff).
svg
Package svg draws SVG documents: the common subset that drawing programs and icon sets write, sharp at any size.
Package svg draws SVG documents: the common subset that drawing programs and icon sets write, sharp at any size.
syntax
Package syntax colours source code for ui.CodeEditor.
Package syntax colours source code for ui.CodeEditor.
typstview
Package typstview shows and edits documents that Typst sets, with gift's own renderer: Typst's glyphs through gift's glyph atlas, its rules as rectangles, its other shapes as triangles.
Package typstview shows and edits documents that Typst sets, with gift's own renderer: Typst's glyphs through gift's glyph atlas, its rules as rectangles, its other shapes as triangles.
videoplayer
Package videoplayer shows a video.Player: its picture, its sound and controls a finger can operate.
Package videoplayer shows a video.Player: its picture, its sound and controls a finger can operate.
Package video plays video files with sound: an AVPlayer for gift.
Package video plays video files with sound: an AVPlayer for gift.
ffmpeg
Package ffmpeg demultiplexes and decodes video and audio through the FFmpeg libraries of the system, loaded at run time with purego.
Package ffmpeg demultiplexes and decodes video and audio through the FFmpeg libraries of the system, loaded at run time with purego.
Package weather is the weather a map of package ui/maps can show: the provider seam of the project plan, section 37, the fields of a model run on a regular grid, and a Sky that keeps the latest run of a provider.
Package weather is the weather a map of package ui/maps can show: the provider seam of the project plan, section 37, the fields of a model run on a regular grid, and a Sky that keeps the latest run of a provider.
dwd
Package dwd is the weather of the Deutscher Wetterdienst behind the seam of package weather: the ICON-D2 model, 2.2 km over Germany and its neighbours, from the open data server, free for any use with the source named.
Package dwd is the weather of the Deutscher Wetterdienst behind the seam of package weather: the ICON-D2 model, 2.2 km over Germany and its neighbours, from the open data server, free for any use with the source named.
gfs
Package gfs is NOAA's Global Forecast System behind the seam of package weather: the whole earth at 0.25 degrees, about 28 km, every hour to five days ahead, in the public domain.
Package gfs is NOAA's Global Forecast System behind the seam of package weather: the whole earth at 0.25 degrees, about 28 km, every hour to five days ahead, in the public domain.
grib2
Package grib2 decodes the GRIB edition 2 messages weather models are published in, as far as the providers of package weather need: a regular latitude and longitude grid (grid definition template 3.0), simple packing (data representation template 5.0) and a bitmap of missing points.
Package grib2 decodes the GRIB edition 2 messages weather models are published in, as far as the providers of package weather need: a regular latitude and longitude grid (grid definition template 3.0), simple packing (data representation template 5.0) and a bitmap of missing points.
web
Package web runs logins and payments in a browser that the application does not control, and hands it the redirect they end with.
Package web runs logins and payments in a browser that the application does not control, and hands it the redirect they end with.

Jump to

Keyboard shortcuts

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