Builtin Library
README.md
ui_observe
What the last UI frame painted, which widget owns a point, and why a widget is not on screen.
Reached through the ui namespace — ui.observe, ui.diagnose, ui.hitTest,
ui.pointerWidget, ui.paintOrder, ui.invisibilityReasons — and served to a
non-Luau reader at /zero/runtime/ui/<screen>/paint.json.
The reading
ui.observe() returns one document. Its widgets rows cover every widget any
registered screen's tree holds, each carrying:
| field | what it is |
|---|---|
parent | the nearest ancestor in its screen's tree carrying an id |
rect | the box layout gave the widget |
clip | the clip it painted under — every overflow: hidden/clip/scroll box above it, intersected down from the viewport |
visible | the part of rect that survived clip and the viewport; w/h are zero when nothing did |
viewport | the region the pass that recorded it paints into: the whole window for a screen on the window, the rect a Scene panel presents the camera's picture in for a screen in that picture |
paintIndex | order within the frame, so two identical rects still separate |
painted | whether the pass that recorded it is the one drawing the viewport |
opaqueFill | whether it filled its whole box at full alpha |
hovered | whether the pointer was over it on the recorded frame |
reason | its verdict, from the closed set below |
detail | a sentence naming what the verdict is about |
occludedBy | the widget covering it, when reason is occluded |
generation advances once per re-rendered frame. Two calls reporting the same
number describe the same frame.
Every rect, the document's own viewport (the window) and its pointer are in
logical points on the window, the space ui.getLayoutInfo and gui.bounds
answer in, wherever a widget's screen draws. A screen in the camera's picture
lays out on the camera's canvas (the size ui.screenSize() reports); while a
Scene panel presents that picture, its rects are mapped onto the window through
the panel. Times ui.pixelRatio() a point is in the window's physical pixels,
where Zin.test's mouse primitives and the inputSim toolbox's click /
mouseMove with target "ui" place a pointer.
The verdicts
ui.invisibilityReasons() lists them:
| reason | what the engine found |
|---|---|
painted | it reached the frame |
noSuchWidget | no registered screen's element tree carries the id |
hiddenByAuthor | props.visible = false on it or an ancestor kept it out of the render |
screenNotShown | its screen is registered and not shown |
neverLaidOut | its screen is shown and the frame laid out no box |
zeroSize | its own box has no area |
offViewport | its box sits entirely outside the viewport of the pass that drew it (the row's viewport) |
clippedByAncestor | an ancestor's clip removed the whole box |
occluded | a later widget covered it with an opaque fill |
screenNotOnViewport | its box is fine and the camera holding the viewport excludes its screen's render layer, so the frame measured it without drawing it |
The resolution reports the nearest cause among them — a widget both clipped
away and covered reports the clip — and painted when nothing kept the widget
out.
The camera's render-layer mask decides the screens in the camera's picture.
With no Scene panel presenting the game viewport, the window is that picture
too, and the mask decides every screen on it. While a Scene panel presents the
game viewport, the window around it is the editor's own picture: a screen
placed on the window (ui.setScreenSpace, and the default for a screen on
EditorUI) paints there whatever the camera's mask holds, and reads painted.
A screen placed in the camera's picture reads screenNotOnViewport under a
camera that excludes its layer, with or without a Scene panel.
The pointer
ui.hitTest(x, y) names the widget a pointer at that point reaches and the
stack beneath it, topmost first, in logical points on the window. Where
several boxes take the point in, the one that paints last is the one that gets
it, and the containers holding them follow — nearest first, by parent.
ui.pointerWidget() reports where the pointer sat, whether the UI claimed it,
and which widget the frame reported it over.