Skip to content

Editor UI Controls

ReGame editor UI is authored as RSX on UISurface so layout, input, accessibility, and automation can share one path.

Use ScrollView for small or custom child content. Use data-driven virtual controls when a panel can grow large enough that creating every row would hurt frame time.

Editor Context

Editor tools can read the current editor dimensions through the Editor singleton. It is available in RGScript, RSX render expressions, and RStyle expressions for registered editor UI surfaces.

Editor.surface is the content area of the surface currently being mounted. Use it for responsive tool layout inside a dock, panel, overlay, modal, or toolbar surface. Named regions describe other editor areas:

  • Editor.window
  • Editor.sceneTree
  • Editor.fileSystem
  • Editor.viewport
  • Editor.bottomDock
  • Editor.inspector
  • Editor.game
  • Editor.leftDock
  • Editor.rightDock
  • Editor.centerDock

Each region is an EditorSurfaceMetrics value with x, y, width, height, visible, and active. getWidth() and getHeight() return the same size values for RGScript code that prefers method calls.

rgscript
let viewportWidth = Editor.getViewportWidth()
let inspectorWidth = Editor.inspector.getWidth()
let dockHeight = Editor.bottomDock.height

Use Editor.surface for the current tool's own layout:

rstyle
assetGrid: {
  width: {Editor.surface.width},
  height: {Editor.surface.height - 42},
  overflow: "scroll"
}

Use named regions only when the tool needs another editor area's size. For example, a viewport overlay may align to Editor.viewport, and an inspector companion tool may choose a compact layout when Editor.inspector.width is narrow.

UIComponent surfaces can use ready for mount-time initialization. Runtime interaction should use control events such as onClick, onChange, and onScroll instead of polling layout every frame.

RSX Source Mode

Files ending in .rsx use the normal RGScript class parser with one extra UI member: render { ... }. The class must extend UIComponent to use render; ordinary .rg scripts and non-UIComponent classes reject render blocks.

Write fields, methods, imports, helper logic, and lifecycle code as RGScript outside render. This includes normal typed arrays, records, vectors, Math helpers, console output, indexed assignment, helper methods, and lifecycle state changes. Keep render declarative: use RSX markup and expressions that produce values or elements, such as {items.map(item => <Row item={item} />)} or {item.enabled ? "Enabled" : "Disabled"}. Render expressions cannot assign, compound-assign, increment, or decrement state.

Imported .rsx components are component types in RGScript. A parent can keep a typed handle, find a retained child component by semantic id, call its public methods, or write its public fields:

rsx
import CounterPanel from "./CounterPanel.rsx"

class Dashboard extends UIComponent {
  private counter: CounterPanel

  ready {
    counter = root.find<CounterPanel>("main-counter")
  }

  public addOne(): void {
    counter.increment()
  }

  public rename(nextTitle: string): void {
    counter.title = nextTitle
  }

  render {
    <View automationId="dashboard">
      <CounterPanel automationId="main-counter" title="Main Counter" />
    </View>
  }
}

Mounting RSX With InterfaceHost

Attach an InterfaceHost component to a GameObject to mount an .rsx source in a scene. One InterfaceHost can render an entire RSX tree, so a single host may contain a label, image, progress bar, and any other elements that belong to the same interface. A scene may also use multiple InterfaceHost GameObjects when interfaces need independent visibility, transforms, or lifecycles.

See the InterfaceHost guide for a complete Inspector walkthrough, floor purchase-pad recipe, moving overhead UI setup, property defaults, events, limitations, and troubleshooting.

The Space property chooses how the host is rendered:

  • screen mounts the UISurface in the normal screen overlay. This is the default.
  • world renders the UISurface as a transparent rectangle in the 3D scene.

Changing Space to world in the component inspector adds a Transform3D when the GameObject does not already have one. Transform3D is the only owner of world position, rotation, and scale; InterfaceHost does not add a second set of X/Y/Z fields. This keeps normal hierarchy behavior intact.

To make an overhead name, health bar, or order indicator follow a moving character, create a child GameObject under the character and attach InterfaceHost plus Transform3D to that child. Its local transform offsets the interface above the character, and the complete parent transform chain moves it with the character. The same approach works for a sign attached to a model or a label attached to another moving GameObject.

For an interface painted onto a floor or wall, use a world host and rotate its Transform3D to match the surface. For example, a floor pad normally uses an X rotation near -90 degrees and a small offset above the floor. depthBias can remove the remaining z-fighting without becoming a second position control.

World Surface Properties

  • surfaceSize is the RSX render-target resolution in pixels. Its default is 512 x 256.
  • pixelsPerUnit converts that pixel size into world units. At the default 100, a 512 x 256 surface occupies 5.12 x 2.56 world units before Transform3D scale is applied.
  • depthTest allows ordinary 3D geometry to occlude the interface and is on by default.
  • receiveShadows optionally lets directional scene shadows darken visible interface pixels. It is off by default.
  • doubleSided controls whether the interface is visible from both sides.
  • depthBias applies a small render-depth offset for interfaces placed directly against other geometry.

World surfaces remain otherwise unlit. When receiveShadows is enabled, the shared directional shadow map darkens visible surface pixels without changing transparent alpha. This means a worker or prop shadow can fall across floor-pad artwork. It does not make the InterfaceHost cast its own shadow onto the floor. Screen surfaces remain ordinary overlays and are never affected by scene lighting or shadows.

Moving a parent or changing a Transform3D updates the world rectangle's transform; it does not require the RSX content to be rerendered. A source or structural change can replace the UISurface. Ordinary retained state changes use the frame commit described below.

Retained Frame Commits

InterfaceHost mutation is frame-coalesced. Pointer handlers, UIComponent methods, and imperative retained-element setters may perform several writes during one frame; ReGame applies one InterfaceHost commit after frame processing.

The commit tracks four independent kinds of change:

  • structure changes replace only the affected surface and run its cancellation and mount lifecycle;
  • layout changes update dirty nodes in the existing Yoga tree;
  • paint changes regenerate cached render items for dirty elements while clean elements and clean hosts reuse their prior output;
  • semantic changes republish the accessibility tree without requiring a paint rebuild.

Setter return values and retained property reads are authoritative immediately. Geometry, render items, world render-target revisions, and accessibility publication reflect the completed frame commit. Do not force a source reload to display interaction state, and do not split frequently changing labels or controls into separate InterfaceHosts as a performance workaround.

Screen and World hosts share UISurface input. World hosts project the active Camera3D ray into surface pixels, then preserve normal per-pointer capture through Move, Up, or Cancel. Use an Area3D when a floor pad should react to player occupancy; use an RSX Button or Pressable when it should react to a pointer. The full routing order, typed payload hierarchy, Joystick API, and platform notes are documented in Pointer Input And Joystick.

World InterfaceHosts use the shared OpenGL runtime path on desktop and Android. Native iOS currently keeps screen InterfaceHosts on its 2D Metal path, but it does not yet render world InterfaceHosts because that renderer does not have the engine's 3D camera/depth pipeline. Native iOS world-host support requires that 3D Metal path rather than a special InterfaceHost fallback.

Built-In Fonts

RSX text uses ReGame's built-in Inter family when fontFamily is omitted. Regular, semibold, and light atlases are engine resources; a new data-only project does not need to copy them into src/assets/fonts.

Desktop editor bundles must contain the atlases under Resources/fonts. Android, iOS, and web exports stage the same built-in atlases into the exported project payload. If a fresh project displays the pixel/debug fallback font, check the Player log for Failed loading font atlas. That message means the engine or export bundle is missing its shared font resources; copying a font into one game only hides the packaging defect and does not fix other projects.

UI Builder Preview

The InterfaceHost inspector exposes Space as the screen/world selector and keeps world placement in the normal Transform3D fields. In UI Builder, enable Scene (uiBuilder.viewport.showScene) to render the active scene behind the RSX preview. For a world host, this shows the interface at its actual 3D position, rotation, scale, and parent-relative placement instead of treating it as a flat screen overlay.

The UI Builder Components palette includes Image. Inserting it creates a normal RSX Image element with editable source and resizeMode properties, so an interface can combine authored images with Text, Button, and other retained elements in the same host.

Lifecycle Events

InterfaceHost exposes named lifecycle and geometry events through the shared GameObject event lane:

  • onMounted fires after the UISurface has been built, registered, and laid out.
  • onUnmounted fires before the retained surface is released.
  • onMountSpaceChanged reports a completed screen/world change.
  • onLayout reports completed UISurface layout.
  • onGeometryChanged reports changes to surface size or pixel-to-world geometry.
  • onRenderSettingsChanged reports changes to depth testing, shadow receiving, sidedness, or depth bias.
  • onRenderTargetReady reports that a world surface texture is available.
  • onRenderTargetInvalidated reports that the current world surface texture must be rebuilt.
  • onLoadFailed reports source, RSX build, or world render-target failures.

State is changed before these notifications are emitted, so listeners can read the new authoritative value. Keep the returned event controller when subscribing and cancel long-lived subscriptions during teardown; owner-bound listeners are also cleared when their owner or mounted surface is destroyed.

UIComponent List Rendering

Use map inside a render expression when a component needs to create repeated UI from an array. The callback returns RSX, and the item can be passed into a reusable row component through normal attributes.

rsx
import ComponentRow from "./ComponentRow.rsx"

class ComponentList extends UIComponent {
  public components: string[] = ["Camera", "Light", "Audio"]

  render {
    <View automationId="component.list">
      {components.map(component =>
        <ComponentRow automationId={"component.row." + component} item={component} />
      )}
    </View>
  }
}
rsx
class ComponentRow extends UIComponent {
  public item: string = ""

  render {
    <View>
      <Text>{item}</Text>
    </View>
  }
}

Render blocks should describe UI. Put normal RGScript logic in fields, methods, lifecycle blocks, or helpers outside render, then use render expressions for UI values. Event attributes take handler references such as onClick={increment} or handler-valued props such as onClick={props.onAdd}; render does not execute calls like onClick={increment()}. ReGame does not support raw foreach blocks, side-effectful .forEach(...) calls, console.log/warn/error calls, or assignment/update expressions such as {count = count + 1} and {count++} directly inside RSX render markup; use {items.map(item => <Row item={item} />)} for repeated children.

Conditional expressions use the normal RGScript condition ? valueWhenTrue : valueWhenFalse form and work in .rg scripts and .rsx render expressions. They are useful for row labels, state badges, and style selection when the expression stays side-effect free.

VirtualTree

VirtualTree renders hierarchy data with tree branch lines, disclosure state, icons, selection, and depth metadata. It is meant for large tree panels such as File System, Scene Tree, UI hierarchy, and similar editor surfaces.

Instead of creating every expanded row as a UISurface element, VirtualTree flattens the expanded tree, creates only the visible rows plus a small overscan buffer, and inserts spacer elements above and below those rows so the ScrollView keeps the correct total scroll height.

rsx
<ScrollView automationId="fileSystem.scroll" style={styles.scrollViewport} contentContainerStyle={styles.treeContent}>
  <VirtualTree rootLabel="Project Root" rootIcon="folder-open" rowHeight="30" itemStyle={styles.treeItem} />
</ScrollView>

Props

  • rowHeight: fixed pixel height used for visible-row math. Match this to the row style height.
  • rootLabel: optional synthetic root row, such as Project Root.
  • rootIcon: icon for the synthetic root row.
  • itemStyle: style applied to each visible tree row.

VirtualList And VirtualGrid

Use VirtualList or VirtualGrid for large flat data arrays where the item UI is still authored by the component. They work like a fixed-size FlatList: the virtualizer owns scroll range math and visible-window expansion, while the child template owns the row or tile contents.

rsx
class AssetGrid extends UIComponent {
  public items: record[] = []
  public hostWidth: number = 640
  public hostHeight: number = 320

  render {
    <VirtualGrid
      automationId="assets.grid"
      accessibilityLabel="Assets"
      data={items}
      itemWidth={104}
      itemHeight={132}
      viewportWidth={hostWidth}
      viewportHeight={hostHeight}
      gap={10}
      overscan={1}
      style={styles.gridViewport}
      onScroll={handleScroll}
    >
      <Pressable automationId={item.automationId} onClick={item.action} style={styles.tile}>
        <Text>{item.name}</Text>
      </Pressable>
    </VirtualGrid>
  }
}

Props

  • data: array expression to render.
  • itemHeight: fixed item height in pixels.
  • itemWidth: fixed item width in pixels. Required by VirtualGrid.
  • viewportHeight: visible viewport height used for range math.
  • viewportWidth: visible viewport width used for grid column math. Required by VirtualGrid.
  • gap, rowGap, columnGap: spacing used in the virtual range and item placement.
  • overscan: extra rows to render before and after the viewport.
  • scrollOffsetY: optional explicit vertical scroll offset. When omitted, retained editor and InterfaceHost surfaces preserve scroll offset by automationId.
  • itemName: optional template local name. Defaults to item.
  • indexName: optional template local name. Defaults to index.

onScroll receives (scrollOffsetY, scrollOffsetX, deltaY, deltaX) when the virtualized element is rendered from a UIComponent.

VirtualList and VirtualGrid require fixed item sizes. Variable-height measurement is a separate feature because it needs a measurement cache and reflow strategy. Put viewport sizing in RStyle or literal inline style for now; inline style object expressions such as style={{ width: hostWidth }} are not supported yet.

For editor tools, prefer Editor.surface.width and Editor.surface.height for viewportWidth and viewportHeight. This keeps bottom-dock and side-dock extensions responsive without hard-coded host dimensions.

rsx
<VirtualGrid
  automationId="assetBrowser.grid"
  data={items}
  itemWidth={112}
  itemHeight={132}
  viewportWidth={Editor.surface.width}
  viewportHeight={Editor.surface.height - 42}
  gap={10}
  overscan={1}
  onScroll={handleScroll}
/>

Only the visible rows plus overscan are expanded into UISurface elements. Keep item dimensions fixed and stable; changing item size during scroll invalidates the virtual range math and should be modeled as a separate layout mode.

Typed Drag And Drop

Retained RSX elements use one typed drag route across editor panels, inspectors, viewports, semantic automation, and gameplay InterfaceHost surfaces. A source declares dragPayloadType and dragPayloadText; a target declares the comma-separated payload types it accepts.

rsx
<Pressable
  automationId="assets.ship"
  dragPayloadType="MODEL_ASSET_FILE"
  dragPayloadText="file://models/ship.glb"
  onDragStart={beginShipDrag}
  onDragEnd={finishShipDrag}
/>

<View
  automationId="inspector.mesh"
  dropPayloadTypes="MODEL_ASSET_FILE"
  onDrop={assignMesh}
/>

Targets receive onDragEnter, onDragOver, onDragLeave, and onDrop. Sources receive onDragStart, followed by either onDragEnd or onDragCancel. Every callback receives a UIDragEvent containing payloadType, payloadText, bindingKey, coordinates, and semantic target ids. The source state is cleared before its terminal event is emitted.

Disabled sources cannot start a drag, disabled targets cannot accept one, and a shared popup or modal capture cancels the behind-the-overlay drag route. Give every source and target a stable automationId; semantic tools expose startDrag and dropPayload actions using the same event path as physical input.

ReGame engine documentation