# tecs.ui Build retained interfaces from ordinary Tecs entities. `tecs.ui` supplies responsive layout, intrinsic sizing, clipping, scrolling, hit testing, keyboard focus, and semantic interaction events for HUDs, menus, inspectors, inventories, and in-world panels. A UI tree is an ECS hierarchy: spawn entities, relate them with `ChildOf`, describe their boxes with [`Style`](/modules/ui/#tecs.ui.Style), and let the plugin update only the layout and GPU instance data that changed. Screen roots follow window resizing and pixel density automatically. The Compose demo scene combining an intrinsic image, a stretched rectangle, a fixed circle, and text Use the module when several visual entities must size and position one another, when content must scroll or clip, or when pointer and keyboard input should target the same retained boxes that were drawn. It is not a DOM or a separate widget renderer. Existing rectangles, circles, sprites, images, and text remain the visible pieces, with their current materials, shaders, batching, and dirty tracking. `tecs.ui` composes those pieces and gives them layout and interaction. [`Style`](/modules/ui/#tecs.ui.Style) describes layout rather than appearance. Put a drawing component on the same entity or on a child, and add `Paint(true)` when its transform should stretch to the computed box. Drawing remains in the existing instanced producers and shaders. The [complete interface guide](/ui/) contains 37 copyable recipes for layout, visual composition, intrinsic sizing, images, scrolling, interaction, focus, and dirty-aware updates. The examples below provide a complete starting point without leaving this API reference. The plugin retains one native layout tree keyed by ECS entity ID. `ChildOf` is the layout hierarchy as well as the transform hierarchy. The layout system writes `Layout` only when the box changes and writes `RelativeTransform2D` only when layout or scrolling changes its result. Query membership observers retain structural invalidation, the ECS dirty-archetype set gates value updates, and the native tree computes only dirty roots before returning a compact list of changed boxes. `px`, `%`, and `auto` dimension strings are parsed only when a `Style` enters or dirties the retained tree. Unchanged frames skip layout computation, layout export, clip reconstruction, and hit-rectangle reconstruction. A screen root installed through `plugin(app)` follows the application's logical window size and pixel density automatically. The plugin observes viewport changes on the world's platform event bus, so unchanged frames do not poll the window. [`Intrinsic`](/modules/ui/#tecs.ui.Intrinsic) copies cached text, sprite-region, or custom leaf metrics into Taffy when the source component dirties. Wrapping text uses a bounded retained convergence pass: Taffy chooses its width, SDL_ttf measures that width exactly, and no native layout callback enters Lua. `Paint(true)` centers and stretches a drawing leaf over its box. Keep containers at unit scale and put their rectangle, circle, or image on an absolute child with `Paint`: transform scale composes through `ChildOf`, so a stretched container would also scale its descendants. ## Flex sizing and wrapping The Flex demo scene comparing a fixed-width child with flex-grow children and a wrapped grid The first row compares a fixed `88px` child with `flexGrow` weights one and two. The cards below use `flexWrap = "wrap"`, so the screenshot shows both remaining-space distribution and line wrapping against one retained parent. ## Absolute positioning and depth The Overlay demo scene with four absolutely positioned corner cards and a centered higher-depth circle Absolute children anchor to their retained parent without consuming flex space. Renderer depth remains a separate concern, which is why the center circle and its label can sit above the four anchored cards. ## Scrolling and clipping [`Scroll`](/modules/ui/#tecs.ui.Scroll) turns its entity's layout box into a viewport. Its `x` and `y` move descendants, not the viewport entity itself. Every nested viewport is intersected before the plugin assigns the existing `tecs.gfx.Clip` component, so each drawable still carries one clip index in the ordinary GPU instance. `firstClip` and `lastClip` reserve which of the renderer’s 255 clip rectangles the UI owns. Content extent includes negative, absolute, and nested descendants. Wheel distance that a deepest viewport cannot consume passes to its ancestors. `reveal` exposes a descendant programmatically, and `Scrollbar` derives a composed thumb transform without adding a renderer primitive. Snapshots retain scroll offsets and derive content extent again after load. The Scroll demo scene with a fixed clipped viewport, overflowing rows, and a composed block scrollbar Screen roots use logical UI coordinates and multiply clips by `pixelDensity`. World roots project viewport corners through the renderer camera. Clip rectangles stay axis-aligned; rotating a scroll viewport therefore does not rotate its clip. ## Interaction and navigation Passing the application to [`plugin`](/modules/ui/#tecs.ui.plugin) supplies its renderer and input, enabling clip-aware hit testing. Manual options remain available to tests and tools. [`Interaction`](/modules/ui/#tecs.ui.Interaction) opts in a layout box, and [`InteractionState`](/modules/ui/#tecs.ui.InteractionState) reports hover, per-pointer capture, drag, and focus without replacing application state. The plugin emits one bubbling [`Event`](/modules/ui/#tecs.ui.Event) at entity addresses. Mouse and touch identities capture independently; releasing over a captured target emits `"click"` and then `"activate"`, while draggable targets emit semantic drag events. Tab and Shift-Tab traverse visible focusable controls and repeat while held, and Return or Space emits `"activate"` at the focused control. Programmatic focus, focus reveal, and modal focus scopes use the same state. A `"wheel"` event begins at the deepest viewport under the pointer; setting `event.consumed = true` prevents both ancestor delivery and default scrolling. Hit priority uses the renderer's layer-depth calculation. A depth tie prefers the deeper UI node, explicit `Interaction.order`, and then stable retained insertion order. `Style.style.order` independently orders layout siblings. Give overlapping controls explicit orders and distinct renderer depth when both their input and drawing order are meaningful. ## Complete application Keep entity composition separate from plugin setup. This first block creates a full-window root, a centered panel, an ordinary GPU-rendered background, and intrinsically sized text: ```teal local tecs = require("tecs") local ui = tecs.ui local ChildOf = tecs.ecs.ChildOf local RelativeTransform2D = tecs.ecs.RelativeTransform2D local Tint = tecs.gfx.Tint local Material = tecs.gfx.Material local Renderable2D = tecs.gfx.Renderable2D local function spawnInterface( world: tecs.World, app: tecs.Application, font: tecs.gfx.Font ) local root = world:spawn( ui.Style({ width = "100%", height = "100%", justifyContent = "center", alignItems = "center", }), ui.Root("screen"), tecs.Transform2D(0, 0, 0, 16) ) local panel = world:spawn( ui.Style({ width = 420, height = 240, flexDirection = "column", padding = 20, gap = 12, }), RelativeTransform2D(), ChildOf(root) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.035, 0.055, 0.09, 0.96), Material(tecs.gfx.materials.id("rounded"), 0.05), Renderable2D(), ChildOf(panel) ) world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic("text", {wrap = true}), RelativeTransform2D(0, 0, 1), Tint(0.94, 0.97, 1.0, 1.0), tecs.gfx.Text.new({ text = "This text and panel are ordinary ECS entities.", font = font, size = 16, }), ChildOf(panel) ) end ``` The application plugin installs text and UI before a `Startup` system spawns the retained entities. Passing `app` to `ui.plugin` supplies the renderer, input, window size, and pixel density: ```teal local function gamePlugin(world: tecs.World, app: tecs.Application) tecs.gfx.layers.configure( 16, { sort = "z", screenSpace = true, unlit = true, } ) world:addPlugin(tecs.gfx.textPlugin({renderer = app.renderer})) world:addPlugin(ui.plugin(app)) world:addSystem({ name = "game.SpawnInterface", phase = tecs.ecs.phases.Startup, run = function() local font = tecs.gfx.newTTF({ source = "fonts/JetBrainsMono-ExtraBold.ttf", name = "game-ui-16", size = 16, raster = "alpha", }):wait().value spawnInterface(world, app, font) end, }) end return tecs.newApplication({plugin = gamePlugin}) ``` The standalone [`docs/examples/ui.tl`](https://github.com/tecs-dev/tecs/blob/main/docs/examples/ui.tl) application expands that setup into a scrollable, keyboard-navigable panel: The standalone retained UI example with a centered panel and scrollable controls ## Common recipes ### Stretch an ordinary rectangle over a layout box Put the drawing component on an absolute child. `Paint(true)` copies the computed width and height into its relative transform without scaling the container's descendants: ```teal local card = world:spawn( ui.Style({width = 280, height = 120}), RelativeTransform2D(), ChildOf(panel) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.10, 0.17, 0.27, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.08), Renderable2D(), ChildOf(card) ) ``` ### Load an image and preserve its aspect ratio The asset loader decodes pixels asynchronously. The renderer registers them and returns the ordinary sprite consumed by `Intrinsic("image")`: ```teal tecs.assets.loadImage( tecs.io.files.assetPath("images/portrait.png") ):map(function(image: tecs.assets.Image): tecs.gfx.Sprite return app.renderer.sprites:registerImage(image) end):onSettle(function(loaded: tecs.Future) if loaded.status == "ready" then world:spawn( ui.Style({height = 64}), ui.Intrinsic("image"), ui.Paint(true), RelativeTransform2D(0, 0, 2), loaded.value, Tint(1, 1, 1, 1), Renderable2D(), ChildOf(panel) ) end end) ``` ### Create a clipped scrolling list `Scroll` belongs on the fixed viewport. Its larger child creates overflow, and the scrollbar is an ordinary composed drawing entity rather than a Taffy or renderer primitive: ```teal local viewport = world:spawn( ui.Style({width = "100%", height = 240}), ui.Scroll(), RelativeTransform2D(), ChildOf(panel) ) local content = world:spawn( ui.Style({ width = "100%", height = 600, flexDirection = "column", gap = 8, }), RelativeTransform2D(), ChildOf(viewport) ) world:spawn( ui.Scrollbar("vertical", 12, 4, 28), ui.Interaction({focusable = false, draggable = true, order = 100}), RelativeTransform2D(0, 0, 10), Tint(0.32, 0.82, 1.0, 1.0), Material(tecs.gfx.materials.id("rounded"), 0), Renderable2D(), ChildOf(viewport) ) ``` ### Observe pointer and keyboard activation `Interaction` makes the computed box selectable but draws nothing. Events bubble through `ChildOf`, and Return or Space produces the same `activate` event as a click: ```teal local type uiTypes = require("tecs.ui") local type UiEvent = uiTypes.Event local button = world:spawn( ui.Style({width = 180, height = 44}), ui.Interaction({tabIndex = 1}), RelativeTransform2D(), ChildOf(content) ) world:observe( button, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("activated via", event.source) end end ) ``` ### Change a retained style Use `getMut` only when a property actually changes. This dirties the retained style once; unchanged frames do not parse the string or recompute the tree: ```teal local style = world:getMut(panel, ui.Style) style.style.width = "100%" style.style.maxWidth = "480px" ``` ### Focus and reveal a control Programmatic focus uses the same state and navigation order as Tab. Focusing a clipped descendant reveals it through every ancestor viewport: ```teal ui.focus(world, button) ui.reveal(world, button, "center") local focused = ui.focused(world) if focused == button then ui.blur(world) end ``` ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Event`](/modules/ui/#tecs.ui.Event) | record | Event reports one semantic interaction at an entity address. | | [`EventKind`](/modules/ui/#tecs.ui.EventKind) | enum | Identifies one semantic UI interaction. | | [`FocusScope`](/modules/ui/#tecs.ui.FocusScope) | record | FocusScope marks an entity that may become the active navigation boundary. | | [`Interaction`](/modules/ui/#tecs.ui.Interaction) | record | Interaction makes one computed layout box a UI input target. | | [`InteractionSource`](/modules/ui/#tecs.ui.InteractionSource) | enum | Identifies what initiated a semantic UI event. | | [`InteractionState`](/modules/ui/#tecs.ui.InteractionState) | record | InteractionState reports transient state derived by the UI plugin. | | [`Intrinsic`](/modules/ui/#tecs.ui.Intrinsic) | record | Intrinsic supplies cached leaf metrics to layout without a Lua callback. | | [`IntrinsicSource`](/modules/ui/#tecs.ui.IntrinsicSource) | enum | Selects which leaf component owns natural dimensions. | | [`Layout`](/modules/ui/#tecs.ui.Layout) | record | Layout reports the last box computed by retained layout. | | [`Options`](/modules/ui/#tecs.ui.Options) | record | Options configures rendering, input, scrolling, and the owned clip-index range. | | [`Overrides`](/modules/ui/#tecs.ui.Overrides) | record | Overrides changes scrolling and clip allocation without repeating an application's renderer and input. | | [`Paint`](/modules/ui/#tecs.ui.Paint) | record | Paint controls how a drawing component consumes its layout box. | | [`PointerType`](/modules/ui/#tecs.ui.PointerType) | enum | Identifies the pointer device associated with an event. | | [`RevealAlign`](/modules/ui/#tecs.ui.RevealAlign) | enum | Selects where reveal places a descendant in each viewport. | | [`Root`](/modules/ui/#tecs.ui.Root) | record | Root supplies the available space and coordinate mapping for one tree. | | [`RootSizing`](/modules/ui/#tecs.ui.RootSizing) | enum | Selects where a root receives its available dimensions. | | [`RootSpace`](/modules/ui/#tecs.ui.RootSpace) | enum | Selects whether a root uses screen or world coordinates. | | [`Scroll`](/modules/ui/#tecs.ui.Scroll) | record | Scroll offsets descendants and clips them to this entity's layout box. | | [`ScrollAxis`](/modules/ui/#tecs.ui.ScrollAxis) | enum | Selects the dimension controlled by a scrollbar thumb. | | [`Scrollbar`](/modules/ui/#tecs.ui.Scrollbar) | record | Scrollbar derives one composed thumb transform from an ancestor viewport. | | [`Style`](/modules/ui/#tecs.ui.Style) | record | Style stores the layout properties that place one UI entity. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`blur`](/modules/ui/#tecs.ui.blur) | Static | Clears the world's focused interaction. | | [`focus`](/modules/ui/#tecs.ui.focus) | Static | Moves focus to one enabled, focusable interaction. | | [`focused`](/modules/ui/#tecs.ui.focused) | Static | Returns the world's focused interaction. | | [`plugin`](/modules/ui/#tecs.ui.plugin) | Static | Returns a plugin that derives layout, transforms, clipping, and optional interaction from retained layout nodes. | | [`popFocusScope`](/modules/ui/#tecs.ui.popFocusScope) | Static | Pops the active navigation boundary and restores its saved focus. | | [`pushFocusScope`](/modules/ui/#tecs.ui.pushFocusScope) | Static | Pushes one modal navigation boundary and remembers the current focus. | | [`reveal`](/modules/ui/#tecs.ui.reveal) | Static | Scrolls every ancestor viewport enough to expose one descendant. | ### Values | Value | Type | Description | | --- | --- | --- | | [`Node`](/modules/ui/#tecs.ui.Node) | [`Component`](/modules/ecs/#tecs.ecs.Component) | Read-only. Exposes the tag that marks an entity as participating in UI layout. | ## Types ### tecs.ui.Event record `Event` reports one semantic interaction at an entity address. Read-only. Exposes the event type delivered to an interaction target and its `ChildOf` ancestors. ```teal record tecs.ui.Event is events.Event kind: EventKind target: integer currentTarget: integer x: number y: number button: integer pointerId: string pointerType: PointerType deltaX: number deltaY: number source: InteractionSource consumed: boolean init: function( event: Event, kind: EventKind, target: integer, x: number, y: number, button: integer, pointerId: string, pointerType: PointerType, deltaX: number, deltaY: number, source: InteractionSource ) end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### Examples Handles pointer and keyboard activation through the same observer. ```teal local tecs = require("tecs") local ui = tecs.ui local type uiTypes = require("tecs.ui") local type UiEvent = uiTypes.Event local world = tecs.ecs.newWorld() local button = world:spawn(ui.Interaction({tabIndex = 1})) world:observe( button, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("activated via", event.source) end end ) ``` #### tecs.ui.Event.kind field Read-only. Reports `"pointerEnter"`, `"pointerLeave"`, `"pointerDown"`, `"pointerMove"`, `"pointerUp"`, `"click"`, `"wheel"`, `"focus"`, `"blur"`, `"activate"`, `"dragStart"`, `"dragMove"`, `"dragEnd"`, or `"dragCancel"`. ```teal tecs.ui.Event.kind: EventKind ``` #### tecs.ui.Event.target field Read-only. Identifies the entity where dispatch began. ```teal tecs.ui.Event.target: integer ``` #### tecs.ui.Event.currentTarget field Read-only. Identifies the entity whose observers are currently receiving this bubbling event. ```teal tecs.ui.Event.currentTarget: integer ``` #### tecs.ui.Event.x field Read-only. Reports the pointer's horizontal window coordinate. ```teal tecs.ui.Event.x: number ``` #### tecs.ui.Event.y field Read-only. Reports the pointer's vertical window coordinate. ```teal tecs.ui.Event.y: number ``` #### tecs.ui.Event.button field Read-only. Reports the mouse button, or zero for touch and events that have no button. ```teal tecs.ui.Event.button: integer ``` #### tecs.ui.Event.pointerId field Read-only. Identifies `"mouse"` or one opaque touch identity. Keyboard, controller, and programmatic events use the empty string. ```teal tecs.ui.Event.pointerId: string ``` #### tecs.ui.Event.pointerType field Read-only. Reports `"mouse"`, `"touch"`, `"pen"`, or `"none"`. ```teal tecs.ui.Event.pointerType: PointerType ``` #### tecs.ui.Event.deltaX field Read-only. Reports horizontal movement for pointer-move, drag, and wheel events, or zero for other events. ```teal tecs.ui.Event.deltaX: number ``` #### tecs.ui.Event.deltaY field Read-only. Reports vertical movement for pointer-move, drag, and wheel events, or zero for other events. ```teal tecs.ui.Event.deltaY: number ``` #### tecs.ui.Event.source field Read-only. Reports `"pointer"`, `"keyboard"`, `"controller"`, or `"programmatic"` as the activation source. ```teal tecs.ui.Event.source: InteractionSource ``` #### tecs.ui.Event.consumed field Caller-writable. An observer may set this field to stop bubbling and suppress the default wheel scroll. ```teal tecs.ui.Event.consumed: boolean ``` #### tecs.ui.Event.init Static ```teal function tecs.ui.Event.init( event: Event, kind: EventKind, target: integer, x: number, y: number, button: integer, pointerId: string, pointerType: PointerType, deltaX: number, deltaY: number, source: InteractionSource ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | [`Event`](/modules/ui/#tecs.ui.Event) | | | `kind` | [`EventKind`](/modules/ui/#tecs.ui.EventKind) | | | `target` | `integer` | | | `x` | `number` | | | `y` | `number` | | | `button` | `integer` | | | `pointerId` | `string` | | | `pointerType` | [`PointerType`](/modules/ui/#tecs.ui.PointerType) | | | `deltaX` | `number` | | | `deltaY` | `number` | | | `source` | [`InteractionSource`](/modules/ui/#tecs.ui.InteractionSource) | | ##### Returns None. ### tecs.ui.EventKind enum Identifies one semantic UI interaction. ```teal enum tecs.ui.EventKind "activate" "blur" "click" "dragCancel" "dragEnd" "dragMove" "dragStart" "focus" "pointerDown" "pointerEnter" "pointerLeave" "pointerMove" "pointerUp" "wheel" end ``` ### tecs.ui.FocusScope record `FocusScope` marks an entity that may become the active navigation boundary. Read-only. Exposes the marker used by `pushFocusScope`. ```teal record tecs.ui.FocusScope is Component end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Marks a dialog as a boundary for modal pointer and keyboard navigation. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn(ui.Style({width = 1280, height = 720})) local dialog = world:spawn( ui.Style({width = 440, height = 260}), ui.FocusScope(), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(root) ) ``` ### tecs.ui.Interaction record `Interaction` makes one computed layout box a UI input target. Read-only. Exposes the component that admits a layout box to hit testing and keyboard navigation. ```teal record tecs.ui.Interaction is Component enabled: boolean focusable: boolean tabIndex: integer order: integer draggable: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a focusable control with an explicit Tab position. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 360})) local button = world:spawn( ui.Style({width = 180, height = 44}), ui.Interaction({tabIndex = 1, draggable = false}), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(panel) ) ``` #### tecs.ui.Interaction.enabled field Caller-writable. The caller controls whether hit testing and navigation can select this entity. The default is true. ```teal tecs.ui.Interaction.enabled: boolean ``` #### tecs.ui.Interaction.focusable field Caller-writable. The caller controls whether clicking or keyboard navigation can focus this entity. The default is true. ```teal tecs.ui.Interaction.focusable: boolean ``` #### tecs.ui.Interaction.tabIndex field Caller-writable. The caller orders keyboard focus. Positive values come first in ascending order, zero values follow in authorial order, and negative values skip keyboard navigation. ```teal tecs.ui.Interaction.tabIndex: integer ``` #### tecs.ui.Interaction.order field Caller-writable. The caller supplies an authorial tie-breaker for hit testing and focus when controls share renderer depth and tab index. Higher values hit later; lower values focus first. ```teal tecs.ui.Interaction.order: integer ``` #### tecs.ui.Interaction.draggable field Caller-writable. The caller enables semantic drag events after pointer movement crosses the plugin's drag threshold. ```teal tecs.ui.Interaction.draggable: boolean ``` ### tecs.ui.InteractionSource enum Identifies what initiated a semantic UI event. ```teal enum tecs.ui.InteractionSource "controller" "keyboard" "pointer" "programmatic" end ``` ### tecs.ui.InteractionState record `InteractionState` reports transient state derived by the UI plugin. Read-only. Exposes transient hover, press, and focus state. ```teal record tecs.ui.InteractionState is Component hovered: boolean pressed: boolean focused: boolean dragging: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Reads transient focus state without dirtying the retained control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local button = world:spawn(ui.Interaction({tabIndex = 1})) local state = world:get(button, ui.InteractionState) if state ~= nil and state.focused then print("button has keyboard focus") end ``` #### tecs.ui.InteractionState.hovered field Engine-owned. The engine reports whether the pointer is over the entity's visible, clipped box. Ordinary game code may read this field to choose its visual state and must not write it. ```teal tecs.ui.InteractionState.hovered: boolean ``` #### tecs.ui.InteractionState.pressed field Engine-owned. The engine reports whether at least one pointer was pressed on the entity and remains captured. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.pressed: boolean ``` #### tecs.ui.InteractionState.focused field Engine-owned. The engine reports whether keyboard activation currently targets the entity. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.focused: boolean ``` #### tecs.ui.InteractionState.dragging field Engine-owned. The engine reports whether at least one captured pointer is dragging this entity. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.dragging: boolean ``` ### tecs.ui.Intrinsic record `Intrinsic` supplies cached leaf metrics to layout without a Lua callback. Read-only. Exposes cached custom, text, or image leaf measurement. ```teal record tecs.ui.Intrinsic is Component source: IntrinsicSource width: number height: number minWidth: number scale: number wrap: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Selects natural text, image, or custom leaf measurements. ```teal local tecs = require("tecs") local ui = tecs.ui local wrappedText = ui.Intrinsic("text", {wrap = true}) local imageSize = ui.Intrinsic("image") local customSize = ui.Intrinsic( "custom", {width = 96, height = 28, minWidth = 48} ) ``` #### tecs.ui.Intrinsic.source field Caller-writable. The caller selects `"custom"`, `"text"`, or `"image"`. Text reads `tecs.gfx.text.Text`; image reads `tecs.components.Sprite`; custom reads the dimensions below. ```teal tecs.ui.Intrinsic.source: IntrinsicSource ``` #### tecs.ui.Intrinsic.width field Caller-writable. The caller supplies the preferred custom width in logical units. Text and image sources ignore this field. ```teal tecs.ui.Intrinsic.width: number ``` #### tecs.ui.Intrinsic.height field Caller-writable. The caller supplies the preferred custom height in logical units. Text and image sources ignore this field. ```teal tecs.ui.Intrinsic.height: number ``` #### tecs.ui.Intrinsic.minWidth field Caller-writable. The caller supplies the custom minimum-content width. Zero uses `width`. Text derives its longest unbroken run and image sources preserve their aspect ratio instead. ```teal tecs.ui.Intrinsic.minWidth: number ``` #### tecs.ui.Intrinsic.scale field Caller-writable. The caller scales text or source-image units into UI units. The default is one. ```teal tecs.ui.Intrinsic.scale: number ``` #### tecs.ui.Intrinsic.wrap field Caller-writable. The caller lets Taffy choose a text line width and lets the plugin write that result to `Text.wrapWidth`. Other sources ignore this field. ```teal tecs.ui.Intrinsic.wrap: boolean ``` ### tecs.ui.IntrinsicSource enum Selects which leaf component owns natural dimensions. ```teal enum tecs.ui.IntrinsicSource "custom" "image" "text" end ``` ### tecs.ui.Layout record `Layout` reports the last box computed by retained layout. Read-only. Exposes the component that reports a computed box without becoming a drawing primitive. ```teal record tecs.ui.Layout is Component x: number y: number width: number height: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Reads the final logical size after the UI layout phase. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 320})) local box = world:get(panel, ui.Layout) if box ~= nil then print(("%.0f x %.0f"):format(box.width, box.height)) end ``` #### tecs.ui.Layout.x field Read-only. The engine reports the box's X offset from its parent. ```teal tecs.ui.Layout.x: number ``` #### tecs.ui.Layout.y field Read-only. The engine reports the box's Y offset from its parent. ```teal tecs.ui.Layout.y: number ``` #### tecs.ui.Layout.width field Read-only. The engine reports the box width in logical units. ```teal tecs.ui.Layout.width: number ``` #### tecs.ui.Layout.height field Read-only. The engine reports the box height in logical units. ```teal tecs.ui.Layout.height: number ``` ### tecs.ui.Options record `Options` configures rendering, input, scrolling, and the owned clip-index range. ```teal record tecs.ui.Options renderer: Renderer firstClip: integer lastClip: integer input: any layer: any wheelStep: number dragThreshold: number end ``` #### tecs.ui.Options.renderer field Caller-writable. The caller supplies the renderer whose camera and GPU clip table the UI uses. ```teal tecs.ui.Options.renderer: Renderer ``` #### tecs.ui.Options.firstClip field Caller-writable. The caller reserves the first UI-owned clip index. The default is one. ```teal tecs.ui.Options.firstClip: integer ``` #### tecs.ui.Options.lastClip field Caller-writable. The caller reserves the last UI-owned clip index. The default is 255. ```teal tecs.ui.Options.lastClip: integer ``` #### tecs.ui.Options.input field Caller-writable. The caller supplies the application's folded input to enable hit testing, scrolling, focus, and activation. Omitting it installs layout and clipping only. ```teal tecs.ui.Options.input: any ``` #### tecs.ui.Options.layer field Caller-writable. The caller supplies the input layer the UI may read. Omitting it reads the base layer. ```teal tecs.ui.Options.layer: any ``` #### tecs.ui.Options.wheelStep field Caller-writable. The caller sets logical scroll units per wheel unit. The default is 40. ```teal tecs.ui.Options.wheelStep: number ``` #### tecs.ui.Options.dragThreshold field Caller-writable. The caller sets pointer movement in logical units before a draggable capture emits `"dragStart"`. The default is four. ```teal tecs.ui.Options.dragThreshold: number ``` ### tecs.ui.Overrides record `Overrides` changes scrolling and clip allocation without repeating an application's renderer and input. ```teal record tecs.ui.Overrides firstClip: integer lastClip: integer layer: any wheelStep: number dragThreshold: number end ``` #### tecs.ui.Overrides.firstClip field Caller-writable. The caller reserves the first UI-owned clip index. The default is one. ```teal tecs.ui.Overrides.firstClip: integer ``` #### tecs.ui.Overrides.lastClip field Caller-writable. The caller reserves the last UI-owned clip index. The default is 255. ```teal tecs.ui.Overrides.lastClip: integer ``` #### tecs.ui.Overrides.layer field Caller-writable. The caller supplies the input layer the UI may read. Omitting it reads the base layer. ```teal tecs.ui.Overrides.layer: any ``` #### tecs.ui.Overrides.wheelStep field Caller-writable. The caller sets logical scroll units per wheel unit. The default is 40. ```teal tecs.ui.Overrides.wheelStep: number ``` #### tecs.ui.Overrides.dragThreshold field Caller-writable. The caller sets pointer movement in logical units before a draggable capture emits `"dragStart"`. The default is four. ```teal tecs.ui.Overrides.dragThreshold: number ``` ### tecs.ui.Paint record `Paint` controls how a drawing component consumes its layout box. Read-only. Exposes the component that optionally stretches an existing primitive to its computed box. ```teal record tecs.ui.Paint is Component stretch: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Stretches an ordinary rounded rectangle over its retained layout box. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 320})) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), tecs.ecs.RelativeTransform2D(0, 0, 1), tecs.gfx.Tint(0.1, 0.2, 0.3, 1), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0.1), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(panel) ) ``` #### tecs.ui.Paint.stretch field Caller-writable. The caller enables `stretch` to copy the layout width and height into `RelativeTransform2D.scaleX` and `scaleY`. ```teal tecs.ui.Paint.stretch: boolean ``` ### tecs.ui.PointerType enum Identifies the pointer device associated with an event. ```teal enum tecs.ui.PointerType "mouse" "none" "pen" "touch" end ``` ### tecs.ui.RevealAlign enum Selects where `reveal` places a descendant in each viewport. ```teal enum tecs.ui.RevealAlign "center" "end" "nearest" "start" end ``` ### tecs.ui.Root record `Root` supplies the available space and coordinate mapping for one tree. Read-only. Exposes the component that gives a retained UI tree its available size and coordinate space. ```teal record tecs.ui.Root is Component space: RootSpace width: number height: number pixelDensity: number sizing: RootSizing end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a screen root that follows the application window automatically. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn( ui.Style({width = "100%", height = "100%"}), ui.Root("screen"), tecs.Transform2D(0, 0, 0, 16) ) ``` #### tecs.ui.Root.space field Caller-writable. The caller chooses `"screen"` for logical screen coordinates or `"world"` for coordinates projected by the camera. ```teal tecs.ui.Root.space: RootSpace ``` #### tecs.ui.Root.width field Caller-writable. The caller supplies the layout root width. An application-backed screen root with automatic sizing follows the logical window width; a camera-sized world root derives world units from the physical viewport and camera zoom. ```teal tecs.ui.Root.width: number ``` #### tecs.ui.Root.height field Caller-writable. The caller supplies the layout root height. An application-backed screen root with automatic sizing follows the logical window height; a camera-sized world root derives world units from the physical viewport and camera zoom. ```teal tecs.ui.Root.height: number ``` #### tecs.ui.Root.pixelDensity field Caller-writable. The caller supplies target pixels per logical point. The default is one. Automatic screen and camera-sized world roots follow the window's current pixel density. ```teal tecs.ui.Root.pixelDensity: number ``` #### tecs.ui.Root.sizing field Caller-writable. The caller selects `"auto"` for the standard behavior, `"manual"` to preserve every authored field, or `"camera"` on a world root to derive its extent and transform from the active camera. `"auto"` follows the window for screen roots and is manual for world roots. ```teal tecs.ui.Root.sizing: RootSizing ``` ### tecs.ui.RootSizing enum Selects where a root receives its available dimensions. ```teal enum tecs.ui.RootSizing "auto" "camera" "manual" end ``` ### tecs.ui.RootSpace enum Selects whether a root uses screen or world coordinates. ```teal enum tecs.ui.RootSpace "screen" "world" end ``` ### tecs.ui.Scroll record `Scroll` offsets descendants and clips them to this entity's layout box. Read-only. Exposes the component that offsets descendants and clips their existing GPU instances. ```teal record tecs.ui.Scroll is Component x: number y: number contentWidth: number contentHeight: number contentX: number contentY: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a fixed viewport and changes its retained vertical offset. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 360})) local viewport = world:spawn( ui.Style({width = "100%", height = 240}), ui.Scroll(), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(panel) ) local scroll = world:getMut(viewport, ui.Scroll) scroll.y = scroll.y + 80 ``` #### tecs.ui.Scroll.x field Caller-writable. The caller supplies the horizontal scroll offset in logical units. ```teal tecs.ui.Scroll.x: number ``` #### tecs.ui.Scroll.y field Caller-writable. The caller supplies the vertical scroll offset in logical units. ```teal tecs.ui.Scroll.y: number ``` #### tecs.ui.Scroll.contentWidth field Engine-owned. The engine reports the horizontal extent of retained content, including nested and absolute descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentWidth: number ``` #### tecs.ui.Scroll.contentHeight field Engine-owned. The engine reports the vertical extent of retained content, including nested and absolute descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentHeight: number ``` #### tecs.ui.Scroll.contentX field Engine-owned. The engine reports the smallest horizontal content coordinate, including negative-positioned descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentX: number ``` #### tecs.ui.Scroll.contentY field Engine-owned. The engine reports the smallest vertical content coordinate, including negative-positioned descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentY: number ``` ### tecs.ui.ScrollAxis enum Selects the dimension controlled by a scrollbar thumb. ```teal enum tecs.ui.ScrollAxis "horizontal" "vertical" end ``` ### tecs.ui.Scrollbar record `Scrollbar` derives one composed thumb transform from an ancestor viewport. Read-only. Exposes a composed scrollbar thumb driven by its parent viewport. ```teal record tecs.ui.Scrollbar is Component axis: ScrollAxis thickness: number inset: number minLength: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Composes a square vertical thumb from ordinary renderer components. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local viewport = world:spawn( ui.Style({width = 480, height = 240}), ui.Scroll() ) world:spawn( ui.Scrollbar("vertical", 12, 4, 28), ui.Interaction({focusable = false, draggable = true}), tecs.ecs.RelativeTransform2D(0, 0, 10), tecs.gfx.Tint(0.32, 0.82, 1, 1), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(viewport) ) ``` #### tecs.ui.Scrollbar.axis field Caller-writable. The caller selects `"horizontal"` or `"vertical"`. ```teal tecs.ui.Scrollbar.axis: ScrollAxis ``` #### tecs.ui.Scrollbar.thickness field Caller-writable. The caller sets the thumb thickness in logical units. ```teal tecs.ui.Scrollbar.thickness: number ``` #### tecs.ui.Scrollbar.inset field Caller-writable. The caller leaves this much room inside each end of the viewport track. ```teal tecs.ui.Scrollbar.inset: number ``` #### tecs.ui.Scrollbar.minLength field Caller-writable. The caller sets the smallest thumb length in logical units. ```teal tecs.ui.Scrollbar.minLength: number ``` ### tecs.ui.Style record `Style` stores the layout properties that place one UI entity. Read-only. Exposes the component that supplies retained layout properties and adds `Layout` and `tecs.gfx.Clip`. ```teal record tecs.ui.Style is Component style: {string: any} end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Builds a column whose width follows its parent up to 480 logical pixels. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn(ui.Style({width = 1280, height = 720})) local panel = world:spawn( ui.Style({ width = "100%", maxWidth = 480, flexDirection = "column", padding = 20, gap = 12, }), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(root) ) ``` #### tecs.ui.Style.style field Caller-writable. The caller supplies a table and marks `Style` dirty after mutating it. Numbers and `"24px"` strings use logical UI pixels, `"50%"` is parent-relative, and `"auto"` selects automatic sizing. Invalid strings raise when the plugin synchronizes the style. The older `{value=number, unit="percent"}` and `unit="points"` table forms remain supported. Supported keys are `display`, `position`, `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `alignContent`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `margin`, `padding`, `border`, `gap`, `rowGap`, `inset`, and the Tecs-only integer `order` used to retain authorial sibling order. Edge values accept one dimension or a table with `left`, `right`, `top`, and `bottom`. ```teal tecs.ui.Style.style: {string: any} ``` ## Functions ### tecs.ui.blur Static Clears the world's focused interaction. ```teal function tecs.ui.blur(world: World): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing. | #### Examples Clears the current keyboard focus when one exists. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() if ui.focused(world) ~= nil then ui.blur(world) end ``` ### tecs.ui.focus Static Moves focus to one enabled, focusable interaction. ```teal function tecs.ui.focus( world: World, entity: integer ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `entity` | `integer` | The caller supplies the interaction to focus. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing or the entity cannot receive focus. | #### Examples Moves keyboard focus to an enabled, focusable control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local saveButton = world:spawn(ui.Interaction({tabIndex = 1})) if not ui.focus(world, saveButton) then print("save button cannot receive focus") end ``` ### tecs.ui.focused Static Returns the world's focused interaction. ```teal function tecs.ui.focused(world: World): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with or without the UI plugin. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the focused entity, or nil when nothing is focused. | #### Examples Finds the interaction that currently owns keyboard focus. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local current = ui.focused(world) if current ~= nil then print("focused entity", current) end ``` ### tecs.ui.plugin Static Returns a plugin that derives layout, transforms, clipping, and optional interaction from retained layout nodes. The plugin owns every renderer clip index from `firstClip` through `lastClip`, inclusive. Reserve a smaller range when another system also calls `renderer.sprites:setClipRegion`. Exhausting the range raises rather than drawing unclipped content. Passing an [`Application`](/modules/Application/) supplies its renderer, input, and window. The plugin samples the viewport initially, then observes platform resize, pixel-size, display, and display-scale events at world address zero. It coalesces them and refreshes automatic roots before layout without polling the window on unchanged frames. The optional second argument overrides clip allocation, the input layer, and wheel distance. Passing `Options` directly preserves manual wiring for tests, tools, and worlds not owned by an application. When input is present, the plugin reads its configured layer in `PreUpdate`. It uses the hit geometry derived after the previous frame's layout and clipping, which is the geometry the player was shown when the input arrived. ```teal function tecs.ui.plugin( source: any, overrides: Overrides ): function(World) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | `any` | The caller supplies an application or manually wired options containing a renderer. | | `overrides` | [`Overrides`](/modules/ui/#tecs.ui.Overrides) | The caller may override source defaults without repeating its renderer and input. | #### Returns | Type | Description | | --- | --- | | `function(`[`World`](/modules/ecs/#tecs.World)`)` | Returns a plugin for `world:addPlugin`. Each world receives a separate retained tree and clip allocator. | #### Examples Installs UI with explicit dependencies in a test or manually constructed world. ```teal local tecs = require("tecs") local type Renderer = require("tecs.Renderer") local type inputTypes = require("tecs.input") local function installManualUi( world: tecs.World, renderer: Renderer, input: inputTypes.Input ) world:addPlugin(tecs.ui.plugin({ renderer = renderer, input = input, firstClip = 32, lastClip = 63, })) end ``` Installs UI from an application so roots follow its window and input. ```teal local tecs = require("tecs") local function gamePlugin(world: tecs.World, app: tecs.Application) world:addPlugin(tecs.ui.plugin( app, { wheelStep = 36, dragThreshold = 4, } )) end ``` ### tecs.ui.popFocusScope Static Pops the active navigation boundary and restores its saved focus. ```teal function tecs.ui.popFocusScope(world: World, scope: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `scope` | `integer` | The caller may require a particular active scope. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing or the requested scope is not active. | #### Examples Closes the active modal boundary and restores its saved focus. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local dialog = world:spawn(ui.FocusScope()) if not ui.popFocusScope(world, dialog) then print("dialog is not the active focus scope") end ``` ### tecs.ui.pushFocusScope Static Pushes one modal navigation boundary and remembers the current focus. ```teal function tecs.ui.pushFocusScope(world: World, scope: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `scope` | `integer` | The caller supplies an entity carrying `FocusScope`. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin or scope is missing. | #### Examples Activates a modal scope and focuses its first control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local dialog = world:spawn(ui.FocusScope()) local firstDialogControl = world:spawn( ui.Interaction({tabIndex = 1}), tecs.ecs.ChildOf(dialog) ) if ui.pushFocusScope(world, dialog) then ui.focus(world, firstDialogControl) end ``` ### tecs.ui.reveal Static Scrolls every ancestor viewport enough to expose one descendant. ```teal function tecs.ui.reveal( world: World, entity: integer, align: RevealAlign ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `entity` | `integer` | The caller supplies a retained UI descendant. | | `align` | [`RevealAlign`](/modules/ui/#tecs.ui.RevealAlign) | The caller selects `"nearest"`, `"start"`, `"center"`, or `"end"`; omission selects `"nearest"`. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin or layout is missing. | #### Examples Centers a selected row through every retained ancestor viewport. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local viewport = world:spawn( ui.Style({height = 240}), ui.Scroll() ) local selectedRow = world:spawn( ui.Style({height = 44}), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(viewport) ) if not ui.reveal(world, selectedRow, "center") then print("selected row has no retained layout") end ``` ## Values ### tecs.ui.Node variable Read-only. Exposes the tag that marks an entity as participating in UI layout. `Style`, `Root`, and `Scroll` add it automatically. ```teal tecs.ui.Node: Component ```