Class Scene
One scene can serve a window for its whole life: to show another page in it, give the scene
another root with setRoot(limn.scene.Widget<?>).
Per-frame pipeline (deterministic, spec order): user input (dispatched
from the backend's input-batch hook, before the Ui.post drain) →
posted tasks → animation tick → layout if dirty → paint.
Coalescing: consecutive mouse-moves collapse to the newest, consecutive scrolls accumulate deltas, consecutive resizes collapse; clicks and keys are never dropped and never reordered.
Slow-handler instrumentation: every widget handler invocation is timed; exceeding the budget (default 8 ms) logs a warning naming the widget, the "StrictMode" that keeps the render thread honest.
V1 limitation: mouse capture tracks a single pressed widget/button, so chorded presses (two buttons held at once) synthesize a CLICK only for the most recent one.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceAnimation hook, called once per frame while registered. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final doubleLongest step any ticker is handed in one frame, whatever the wall clock says. -
Constructor Summary
ConstructorsConstructorDescriptionA scene overroot.Scene(Widget<?> root, LongSupplier clock) Injectable clock (slow-handler instrumentation and animation ticks), public so component tests can drive animations deterministically. -
Method Summary
Modifier and TypeMethodDescriptionvoidaddRealTimeTicker(Scene.Ticker ticker) Registers a ticker on wall time:setTimeScale(double)andsetPaused(boolean)do not reach it.addShortcutHandler(Predicate<KeyEvent> handler) Registers a scene-wide keyboard handler for chords the focused widget did not want: menu accelerators, and anything else that must work without being focused.voidaddTicker(Scene.Ticker ticker) Registers an animation ticker on scene time; frames keep coming while any is active.voidannounce(String text, Accessible.Politeness politeness) Says something out loud with a fixed string.voidannounce(I18nString text, Accessible.Politeness politeness) Says something out loud to whoever is using an assistive technology: a status that changed where nothing on screen says so, a background task that finished, an error that has no control to attach itself to.voidbind(NativeWindow window) Wires this scene into a window: input, frame rendering, invalidation, clipboard.booleancanEdit(EditCommand command) Whether an editing command can be done now by the widget that has the keyboard here, or by one of its ancestors.booleancanEdit(EditCommand command, @Nullable Widget<?> from) Whether an editing command can be done now byfromor by one of its ancestors: the questioncanEdit(EditCommand)asks of the focused widget, asked of a widget the caller remembered, as a menu does of the one that had the keyboard before it opened.clock()The clock this scene's animations tick on, for a scene built to stand beside it.@Nullable ControlSizevoidMarks a scene-space region as damaged and schedules a frame, for painting somewhere a widget'sWidget.invalidate()does not reach: its bounds grown by itspaintOutset(), which is where a shadow or a focus ring is declared.voiddisposeLater(@Nullable Runnable gpuCleanup) LikedisposeLater(GpuSurface)for any GPU cleanup that must run with this scene's GL context current, e.g.voiddisposeLater(@Nullable GpuSurface surface) Schedules aGpuSurfacefor disposal at the next frame, when this scene's window/GL context is current.booleanedit(EditCommand command) Does an editing command in the widget that has the keyboard here, or in the first of its ancestors that can, as the user's own act: what the command's chord does there.booleanedit(EditCommand command, @Nullable Widget<?> from) Does an editing command infrom, or in the first of its ancestors that can.voidfadeWindow(float target, double seconds, @Nullable Runnable onArrive) Animates this scene's native-window opacity towardtarget([0..1]) overseconds, runningonArriveonce it settles.voidfadeWindowIn(double seconds) Snaps the window opacity to0immediately, then eases it to1overseconds, the flash-free "appear" sequence.voidfadeWindowOut(double seconds, @Nullable Runnable onGone) Eases the window opacity to0, then runsonGone(typically a close), by the rules offadeWindow(float, double, java.lang.Runnable): never inside this call, and afterseconds(or on the next turn of the UI queue for none) when the fade goes straight there.@Nullable Widget<?> The first widget whose id isid, searching the root's tree in order, parents before children, then each overlay in the order it was pushed.<T> @Nullable Tfind(String), typed.@Nullable Widget<?> The widget holding keyboard focus, ornullwhen nothing does.voidfocusTraverse(boolean backward) Moves focus to the next/previous focusable widget in layout (DFS) order.voidgraftPopup(Scene popup) Publishes a popup that draws in a window of its own as part of this scene's accessible tree, under the widget that opened it, the way a native drop-down list is a child of its field: a combo's list, a date picker's calendar.voidinheritRenderingFlags(Scene owner) Copies the rendering flags (partial rendering, damage debug) from the scene that spawned this one: popup/dialog scenes live in their own native windows, so without this the flags would only ever affect the window they were toggled in.static voidinstallTooltipStyle(Function<ControlSize, TooltipStyle> supplier) Installs the themed tooltip appearance (the components layer calls this once).booleanWhether each frame's damage region is highlighted for inspection.booleanbooleanWhether only damaged regions are repainted rather than the whole window.booleanisPaused()booleanWhether the bound window holds OS input focus, as reported by the last focus event (falseuntil the first one arrives; windows shown without stealing focus, like popups, start unfocused).@Nullable LayoutDirectionvoidlayoutPass(float newWidth, float newHeight) Runs measure/layout when dirty or resized (public for headless tests/embedding).@Nullable Localelocale()metrics()intobserveChanges(ChangeObserver observer) Watches every change to every widget in this scene's tree, overlays included, from every origin.observePresses(Consumer<@Nullable Widget<?>> observer) Observes every mouse press with its hit-tested target (after normal dispatch), the hook for "click outside to dismiss" overlays and popups that must react to presses landing on non-focusable widgets.observeWindowBlur(Runnable observer) Observes the bound window losing OS focus, the cue that dismisses transient popups (dropdowns, menus) anchored to this window: a press in another window or application never reachesobservePresses(java.util.function.Consumer<limn.scene.Widget<?>>).observeWindowClosed(Runnable observer) Runsobserverwhen the bound window is destroyed, the hook for completion work driven by tickers/frames, which stop forever at that point (an abandoned fade would otherwise leak an uncompleted future).observeWindowFullscreen(Consumer<Boolean> observer) Observes the bound window entering or leaving fullscreen, the cue that closes a surface open in a window of its own over it (a menu, a drop-down list, a calendar), as the platforms dismiss their own menus on a change of their window's state: the change resizes and moves the window under a surface placed against its old geometry, and a window fullscreen at a named mode is minimized as soon as another window takes the focus.voidpack()Sizes the bound window to what this scene's content asks for: the root measured with no bound on either axis, rounded up to whole points, kept within the window's size limits and no larger than its display's work area.voidpushOverlay(Widget<?> overlay) Pushesoverlayas a full-scene modal layer painted on top of everything: it captures all input and confines focus (hit-testing and Tab traversal ignore the content and lower overlays).voidPushespopupaspushOverlay(limn.scene.Widget<?>)does — it captures all input and confines focus — for a surface that opens from a control and belongs to it: a combo's list, a menu, a date picker's calendar.voidrelayout()Re-measures and repaints this whole scene, root and overlays.voidremoveOverlay(Widget<?> overlay) Removes a modal overlay and restores focus to whatever it took it from.voidrenderFrame(Canvas canvas) Full frame: animation tick → layout (if dirty) → paint (root then overlays) → modal scrim.voidrenderFrame(Canvas canvas, boolean rePresent) voidrenderFrame(Canvas canvas, boolean rePresent, float gpuFrameMs, int bufferAge) voidrenderFrame(Canvas canvas, FrameInfo frame) Renders the frame a window's backend asked for, with everything the backend said about it: the re-present flag, the GPU-time sample and the back buffer's age.voidrequestFocus(@Nullable Widget<?> widget) Moves keyboard focus towidget, or clears it whennull.voidSchedules a frame and marks the whole scene damaged.Widget<?> root()The root widget of this scene's tree.voidsetBackground(@Nullable Color color) The colour cleared behind the tree each frame, chosen by the application and kept whatever the palette does: a scene that clears to transparent to let the desktop through, a video that wants black around it.voidsetClipboard(Clipboard newClipboard) Overrides the clipboard (tests inject mocks).voidsetControlSize(@Nullable ControlSize size) Sets this window's default step, the per-window root of the inheritance chain (a compact tool window beside a regular main window).voidsetDamageDebug(boolean enabled) Highlights each frame's fresh damage region (magenta wash + border), the tool for spotting under-invalidation bugs and seeing what partial rendering repaints.voidsetDragKey(int key, int modifiers) Sets the key that begins a drag from the keyboard on the focused widget.voidsetFrontPainter(@Nullable Consumer<Canvas> painter) Draws over the finished frame: after the root, after every overlay, after the modal scrim and after the tooltip.voidsetLayoutDirection(@Nullable LayoutDirection direction) Sets this window's default layout direction, the per-window root of the inheritance chain: the one line an application whose interface reads right to left writes.voidSets this window's default locale, the per-window root of the inheritance chain: what makes two windows in two languages expressible, which one process-wide locale could not say.voidsetPartialRendering(boolean enabled) Enables partial rendering: frames repaint only the damaged region (plus the previous frame's, for double buffering) instead of the whole window.voidsetPaused(boolean newPaused) Freezes or resumes scene time, keeping thetimeScale()the app chose (so a paused slow-motion scene resumes in slow motion).voidReplaces this scene's root withroot, which is how a window shows another page: one scene, bound once, given each page's tree in turn.voidsetSlowHandlerBudgetMillis(long millis) Budget above which an event handler is reported (default 8 ms).voidsetTextRuler(TextRuler ruler) Overrides the text measurer (tests use deterministic fake metrics).voidsetTimeScale(double scale) Speed of scene time, the clock everyaddTicker(limn.scene.Scene.Ticker)ticker integrates.intdouble@Nullable NativeWindowwindow()
-
Field Details
-
MAX_TICK_SECONDS
public static final double MAX_TICK_SECONDSLongest step any ticker is handed in one frame, whatever the wall clock says.The frame interval is not bounded by anything: a GC pause, a window drag, a breakpoint, a laptop lid or a monitor switch can leave seconds between two frames. Unclamped, that interval reaches every ticker at once: a 0.14 s focus fade jumps straight to its end (merely ugly), and anything integrating it (a game step, a physics tick, a sweep that wraps modulo 1) skips through states it was supposed to pass through, which is how tunnelling and NaN-poisoned simulations start. 0.25 s is the conventional guard: long enough that no healthy frame is ever clipped (it is 15 frames at 60 Hz), short enough that the worst a stall can do is one slow-motion step.
Clamping loses time on purpose: the toolkit does not bank the missing seconds and replay them. A stalled second is a second the app did not run, not a debt.
- See Also:
-
-
Constructor Details
-
Scene
A scene overroot. Subscribes to font, size-step and language changes.A widget belongs to one scene as its root or one of its overlays: one that is already another scene's root, or an overlay on another scene's stack, is refused, as
pushOverlay(Widget)and every container's add refuse it. That scene lets go of it when it is taken off the stack (removeOverlay(limn.scene.Widget<?>)), when it is given another root (setRoot(limn.scene.Widget<?>)), or when its window closes. A widget that has a parent is refused too: a root has none.- Parameters:
root- the widget tree to lay out, paint and dispatch to; never null- Throws:
IllegalArgumentException- ifroothas a parent, or is another scene's root or an overlay on another scene's stack
-
Scene
Injectable clock (slow-handler instrumentation and animation ticks), public so component tests can drive animations deterministically.- Parameters:
root- asScene(Widget)takes it, and refused as it refuses itclock- what this scene reads as now, in nanoseconds- Throws:
IllegalArgumentException- ifroothas a parent, or is another scene's root or an overlay on another scene's stack
-
-
Method Details
-
metrics
- Returns:
- live per-frame performance samples (FPS, frame time, event time)
-
clock
The clock this scene's animations tick on, for a scene built to stand beside it.A surface this scene opens in a window of its own — a combo's list, a menu's cascade, a date picker's calendar, a dialog's card — is the root of a second scene, and that scene owns the fade whose last frame destroys the window (
fadeWindowOut(double, java.lang.Runnable)). Built on the wall clock while its opener runs on an injected one, that fade advances by real microseconds however much time the opener's clock is told has passed, so the window never goes: a headless harness could open a popup and never close it, and did (the accessibility gallery's, 2026-09-15). So the four surfaces pass this along, and a popup's time is its opener's time. With no clock injected both areSystem.nanoTime(), which is every application.- Returns:
- what this scene reads as now
-
setTextRuler
Overrides the text measurer (tests use deterministic fake metrics). -
textRuler
- Returns:
- the scene's text measurer (defaults to the backend-installed one)
-
bind
Wires this scene into a window: input, frame rendering, invalidation, clipboard.A scene whose window closed may be bound again, to the same window's successor or to another: the tree the close let go of comes back to it. Not what another scene, or a container, took meanwhile: an overlay taken stays its taker's and is no longer on this scene's stack, and a root taken refuses the bind, since a scene has no tree without its root; take the root out of where it went first, or give this scene another one with
setRoot(limn.scene.Widget<?>).- Parameters:
window- the window this scene draws in and takes its input from- Throws:
IllegalStateException- if this scene's window closed and its root has been taken since: it has a parent, or it is in another scene
-
relayout
public void relayout()Re-measures and repaints this whole scene, root and overlays. Call it after changing an input to measurement that is not a widget property: the activeTheme, the UI font family, this scene's default control size.root().markNeedsLayout()is not enough: it dirties only the path to the root, so children return cached sizes under unchanged constraints, and it misses overlays entirely (open dialogs, open menus). The global path must therefore go throughScene, never through app code. -
pack
public void pack()Sizes the bound window to what this scene's content asks for: the root measured with no bound on either axis, rounded up to whole points, kept within the window's size limits and no larger than its display's work area. The call a dialog or a tool window makes once its content is in place, instead of guessing a size in its configuration; a window created hidden can be packed before it is shown, and appears at its size.What the content asks for is each widget's natural size: a label its text, a button its label and padding, a column the sum of its children. The width is settled first, and the height is then measured at that width, so a paragraph that wraps gets the lines it wraps into. A dialog or a form packs well. Content that fills whatever it is given does not ask for much: a flexible child asks for its floor (
Expanded.atLeast), a list or a table for little more than its header, and a widget with no natural size on an axis leaves the window's size there as it was. Text that wraps asks for its whole length on one line, which the work area then caps. Bounds on the content are what a packed window follows: a paragraph withWidget.setMaxWidth(float)packs to that width, and a list given a floor packs to that height. The layout is redone at the new size on the next frame. UI thread only.- Throws:
IllegalStateException- when the scene is bound to no window
-
window
- Returns:
- the bound window, or
nullwhen headless
-
setClipboard
Overrides the clipboard (tests inject mocks). -
clipboard
- Returns:
- the clipboard (window's, injected, or a local no-op; never null)
-
root
The root widget of this scene's tree. -
setRoot
Replaces this scene's root withroot, which is how a window shows another page: one scene, bound once, given each page's tree in turn. It works whether or not the scene is bound to a window, and on a scene whose window has closed, whose nextbind(limn.backend.NativeWindow)brings the new root in.The old root leaves the scene as a widget taken out of its container does: every widget in it is told
Widget.onDetached(), a popup, menu or popover opened from one of its widgets closes (a menu shown at a point of the scene, at no widget, stays open), the keyboard, the pointer's hover and a press held in it are taken out, a drag it started in the window is cancelled (its source told the drag ended with nothing), and an assistive technology is told the window's content changed. A drag already handed to the system, to leave the window, is the system's: it goes on, a drop on the window goes to the new root, and the source hears the system's answer when the system ends it. It then belongs to no scene: it may be added to a container, pushed as an overlay, made another scene's root, or given back to this one later. The new root joins as a new scene's root does, and the next frame lays out and paints the whole window. The overlays on the stack stay where they are.The keyboard goes to the new root's first focusable widget, arriving as
focusTraverse(boolean)puts it there, only when it was in the old root or in a popup that closes as the old root leaves (a popover, a combo box's list, a menu bar's menu drawn in the scene, where it had taken the keyboard); a widget the new root's content asks for withrequestFocus(limn.scene.Widget<?>)as it joins (from anWidget.onAttached()) is focused instead. An overlay opened while the keyboard was in the old root gives it to the new root's first focusable widget when it closes, in place of the widget that left.Asked again from code this call runs (an
onDetachedof the old root, anonAttachedof the new one, the handler of a popup closing, theonArriveof a fade or a motion that ends as its container leaves), the change waits for this one to be complete and is then made; when several are asked, the last one wins. A root that asks for another as it joins hands the window on, and so on: when, after the root it was given and 64 more asked for in turn, yet another is asked for, a chain that never settles (two pages whose guards disagree, each sending the window to the other as it joins), the call sets no more and throws anIllegalStateException, the scene showing the last root it set. What such code throws is thrown once the change is complete; with a chain that never settles, it is attached to thatIllegalStateExceptionas suppressed.Refused as a new scene refuses its root, before anything moves: a widget with a parent, another scene's root, an overlay on another scene's stack or on this one's, and the widget another call is making a scene's root. Setting the root this scene already has does nothing.
- Parameters:
root- the new root; never null- Throws:
IllegalArgumentException- ifroothas a parent, is another scene's root, or is an overlay on this or another scene's stackIllegalStateException- if the roots asked for while it sets one never settle, with what the code it ran threw, if anything, as suppressed
-
find
The first widget whose id isid, searching the root's tree in order, parents before children, then each overlay in the order it was pushed. A hidden widget is found like a shown one. UI thread only.- Parameters:
id- the id to look for- Returns:
- the widget, or
nullwhen none has that id
-
find
find(String), typed.- Type Parameters:
T- that class- Parameters:
id- the id to look fortype- the class the widget is expected to be- Returns:
- the widget, or
nullwhen none has that id - Throws:
ClassCastException- when the widget with that id is not atype
-
controlSize
- Returns:
- this scene's default step, or
nullto fall through to a hosted root's host link and then toControlSize.processDefault(). Nullable by design: a popup's own scene declares nothing, which is what lets it inherit from the widget that opened it.
-
setControlSize
Sets this window's default step, the per-window root of the inheritance chain (a compact tool window beside a regular main window). Widgets that declare their own step, and their subtrees, are unaffected.nullrestores fall-through. UI thread only. -
layoutDirection
- Returns:
- this scene's default layout direction, or
nullto fall through to a hosted root's host link and then toLayoutDirection.processDefault(). Nullable by design, for the reasoncontrolSize()is: a popup's own scene declares nothing, which is what lets it inherit from the widget that opened it.
-
setLayoutDirection
Sets this window's default layout direction, the per-window root of the inheritance chain: the one line an application whose interface reads right to left writes. Widgets that declare their own direction, and their subtrees, are unaffected — which is what lets a left-to-right code editor, log pane or URL bar sit inside it.nullrestores fall-through. UI thread only. -
locale
- Returns:
- this scene's default locale, or
nullto fall through to a hosted root's host link and then toI18n.processLocale(). Nullable by design, for the reasoncontrolSize()is: a popup's own scene declares nothing, which is what lets it inherit from the widget that opened it.
-
setLocale
Sets this window's default locale, the per-window root of the inheritance chain: what makes two windows in two languages expressible, which one process-wide locale could not say. Widgets that declare their own locale, and their subtrees, are unaffected.nullrestores fall-through. Retains the declared locale's bundle tables exactly asWidget.setLocale(java.util.Locale)does. UI thread only. -
pushOverlay
Pushesoverlayas a full-scene modal layer painted on top of everything: it captures all input and confines focus (hit-testing and Tab traversal ignore the content and lower overlays). This is how modal dialogs work: an in-scene overlay, no nested event loop. Focus moves to the overlay's first focusable widget, or stays on the one the overlay's content asked for withrequestFocus(limn.scene.Widget<?>)as it joined (from anWidget.onAttached()).A widget that is another scene's root, or an overlay on another scene's stack, is refused before anything moves, as a container's add and a new scene refuse it: the scene that holds it would go on naming it as its own and laying it out at its own window's size. That scene lets go of it when it is taken off the stack (
removeOverlay(limn.scene.Widget<?>)), when it is given another root (setRoot(limn.scene.Widget<?>)), or when its window closes. A widget that has a parent is refused the same way: an overlay is a root of its own, and as a container's child too it would be laid out, drawn and dispatched to twice. So are this scene's own root and an overlay already on its stack, which one push holds once.- Parameters:
overlay- the overlay's root- Throws:
IllegalArgumentException- ifoverlayhas a parent, is this or another scene's root, or is an overlay on this or another scene's stack
-
pushPopup
PushespopupaspushOverlay(limn.scene.Widget<?>)does — it captures all input and confines focus — for a surface that opens from a control and belongs to it: a combo's list, a menu, a date picker's calendar. What differs is what an assistive technology is told about the page beneath: under a modal overlay it is published not enabled, because a dialog blocks its owner as a native modal disables the window behind it; under a popup it stays enabled and merely offers no verb, because a native drop-down list does not disable the field it opened from. Closed withremoveOverlay(limn.scene.Widget<?>).- Parameters:
popup- the popup's root widget- Throws:
IllegalArgumentException- ifpopuphas a parent, is this or another scene's root, or is an overlay on this or another scene's stack, aspushOverlay(Widget)refuses it
-
graftPopup
Publishes a popup that draws in a window of its own as part of this scene's accessible tree, under the widget that opened it, the way a native drop-down list is a child of its field: a combo's list, a date picker's calendar. An assistive technology then never leaves this window's tree while the popup is open; the list's cursor is this tree's own, and the popup's window publishes nothing of its own.Call it before
popupis bound to its window, and only for a popup whose keyboard is its opener's: the field keeps it and drives the popup, so the focused node stays in this tree. A pointer click in the popup's window makes it the desktop's key window; such a popup hands every key that reaches that window on to its opener, and gives this window the keyboard back when it closes while holding it, as the combo box's list and the date picker's calendar do. The popup's root names its opener withWidget.setInheritanceHost(limn.scene.Widget<?>); its nodes are published under the opener's, in this window's coordinates, while its window is visible, and leave the tree when that window closes. What a reader performs on them is performed in the popup's scene.This scene may itself be a grafted popup, a panel of controls in a window of its own: a popup grafted into it is then published by the tree this one is published in, under its opener there, to any depth, and leaves that tree when its own window closes or this one's does.
- Parameters:
popup- the popup's scene, not yet bound- Throws:
IllegalArgumentException- if the popup's root names no openerIllegalStateException- if the popup is already bound, or grafted elsewhere
-
removeOverlay
Removes a modal overlay and restores focus to whatever it took it from. -
setBackground
The colour cleared behind the tree each frame, chosen by the application and kept whatever the palette does: a scene that clears to transparent to let the desktop through, a video that wants black around it. A translucent one only shows through where the window itself is translucent.A scene whose application chose none, which is every scene until this is called, clears to the background of the palette in effect and follows it when the palette switches, in every window, those opened after the switch included.
- Parameters:
color- the colour to clear to, ornullto follow the palette again
-
background
- Returns:
- the colour the next frame clears to: the one
setBackground(limn.graphics.Color)chose, or the background of the palette in effect when none was chosen
-
setSlowHandlerBudgetMillis
public void setSlowHandlerBudgetMillis(long millis) Budget above which an event handler is reported (default 8 ms). -
slowHandlerCount
public int slowHandlerCount()- Returns:
- handler-budget violations seen so far (debug/tests)
-
setPartialRendering
public void setPartialRendering(boolean enabled) Enables partial rendering: frames repaint only the damaged region (plus the previous frame's, for double buffering) instead of the whole window. A subtree that misses the pass region is skipped from the paint walk too (culledFromPaint), as one that misses the canvas clip is in every mode.Default on: the mode the toolkit is correct in is the one it runs, and every widget is held to it by
DamageContractTest.falseis the escape hatch for a scene that composites something the toolkit cannot see, and for a capture harness that wants whole frames on purpose; nothing else should need it. -
isPartialRendering
public boolean isPartialRendering()Whether only damaged regions are repainted rather than the whole window. -
setDamageDebug
public void setDamageDebug(boolean enabled) Highlights each frame's fresh damage region (magenta wash + border), the tool for spotting under-invalidation bugs and seeing what partial rendering repaints. Works with partial rendering on or off (off shows what would be repainted). Default off. -
isDamageDebug
public boolean isDamageDebug()Whether each frame's damage region is highlighted for inspection. -
inheritRenderingFlags
Copies the rendering flags (partial rendering, damage debug) from the scene that spawned this one: popup/dialog scenes live in their own native windows, so without this the flags would only ever affect the window they were toggled in. (A transparent-background popup still falls back to full frames; the debug overlay works regardless.) -
requestRender
public void requestRender()Schedules a frame and marks the whole scene damaged. -
announce
Says something out loud to whoever is using an assistive technology: a status that changed where nothing on screen says so, a background task that finished, an error that has no control to attach itself to.It is on the scene and not on a widget because it is a message to the user rather than a property of a box. Nothing is announced automatically: a status label that updates silently stays silent until an application adds this call.
It buys the frame that delivers it, and only while something is listening. The steady state while a screen reader is reading a quiet interface is a loop parked with no frame pending, and an announcement enqueued into that state would never be spoken — the application's one way of saying something out loud going silent exactly when the interface is quiet, which is when it is most likely to be used. With nothing listening the entry is still queued and still bounded, so an application's diagnostics do not depend on a reader being present, and no frame is spent on speech nobody will hear.
UI thread only.
- Parameters:
text- what to say; resolved under this scene's own language when it is drainedpoliteness- whether it waits for the assistive technology to finish, or interrupts it. Not a boolean, because all three platforms carry the distinction and it decides whether a user is cut off mid-sentence.- Throws:
NullPointerException- if either argument isnull
-
announce
Says something out loud with a fixed string. UI thread only.- Parameters:
text- what to saypoliteness- whether it waits, or interrupts- Throws:
NullPointerException- if either argument isnull
-
setFrontPainter
Draws over the finished frame: after the root, after every overlay, after the modal scrim and after the tooltip.nullremoves it.It is not in the tree and not in hit-testing. That is the whole difference from a widget, and it is what this exists for. A widget added last to the root paints under any overlay, so it disappears the moment a modal dialog opens; an overlay pushed on top paints correctly but becomes the topmost layer, and
hitAt(float, float)then routes every press to it alone, so a dialog underneath can never be driven. A front painter has neither problem because it takes no input at all.Meant for a recording or capture harness that has to draw something over the whole window: a mouse pointer into a filmed frame, a watermark, a debug readout. It is not a place to put user interface. Nothing here can be clicked, focused, or reached by a keyboard, and a control drawn from here would look live and be inert.
It draws on every repaint, including partial ones, and it is given the same canvas the frame used, so what it draws outside the damaged region will be clipped away. A caller that moves what it draws must therefore mark the scene damaged itself, which
requestRender()does; the capture harness renders whole frames and so does not have to.Painted before the damage-debug flashes, so those stay legible on top of it.
-
frontPainter
- Returns:
- the front painter, or
null.
-
damage
Marks a scene-space region as damaged and schedules a frame, for painting somewhere a widget'sWidget.invalidate()does not reach: its bounds grown by itspaintOutset(), which is where a shadow or a focus ring is declared. A caret drawn in an overlay is one.Widget.invalidate()is the common path. -
observePresses
Observes every mouse press with its hit-tested target (after normal dispatch), the hook for "click outside to dismiss" overlays and popups that must react to presses landing on non-focusable widgets.- Parameters:
observer- told about every press; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
isWindowFocused
public boolean isWindowFocused()Whether the bound window holds OS input focus, as reported by the last focus event (falseuntil the first one arrives; windows shown without stealing focus, like popups, start unfocused). -
observeWindowBlur
Observes the bound window losing OS focus, the cue that dismisses transient popups (dropdowns, menus) anchored to this window: a press in another window or application never reachesobservePresses(java.util.function.Consumer<limn.scene.Widget<?>>).- Parameters:
observer- told when the window loses focus; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
observeWindowFullscreen
Observes the bound window entering or leaving fullscreen, the cue that closes a surface open in a window of its own over it (a menu, a drop-down list, a calendar), as the platforms dismiss their own menus on a change of their window's state: the change resizes and moves the window under a surface placed against its old geometry, and a window fullscreen at a named mode is minimized as soon as another window takes the focus. The person can make the change on macOS with the green button or Control-Command-F while a list or a calendar is open, because neither takes the focus from the window. A modal dialog's window hears it too, and moves into the scene.Told once per change, at the end of the input batch that brought it, even while a modal blocks the window; see
WindowInput.windowFullscreenChanged(boolean).- Parameters:
observer- told whether the window is fullscreen now; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
modifiers
public int modifiers()- Returns:
- the modifier keys held right now (see
Keysbits)
-
observeChanges
Watches every change to every widget in this scene's tree, overlays included, from every origin. One registration serves a whole window, adds nothing per widget, and works on an unbound scene -- which is the trapScene's own metrics listener was moved into the constructor for, and which every component test, a combo box's popup and a dialog's modal scene before it binds all depend on.This is a channel over a tree, not over a set of widgets. It hears what the scene holds now, and goes quiet about a widget the scene no longer contains -- so a recycled list cell written to while unmounted reaches its own watchers and no scene's. A consumer that must follow one particular widget wherever it goes registers on the widget.
- Parameters:
observer- told about every change in this tree; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
focusedWidget
The widget holding keyboard focus, ornullwhen nothing does. -
requestFocus
Moves keyboard focus towidget, or clears it whennull. Ignored for a widget that is not focusable, for one that is hidden or disabled itself or through any ancestor (a control inside a hidden tab or a disabled form), for one inside a subtree sliding shut, for one outside the topmost modal overlay, and for one in another scene or in none. One merely scrolled out of its viewport is not refused: arriving reveals it. UI thread only.A widget still joining this scene is the exception: a container's
Widget.onAttached()runs before its children are attached, so a form that starts the keyboard on its first field from there asks for a widget that is not in the scene yet. The request is kept until the join is complete, and the field is focused then if it has joined and nothing above refuses it; a later request,nullincluded, replaces it. A join that anonAttachedcut short before reaching the field focuses nothing. Pushed as an overlay or a popup, the field asked for is the overlay's first focus: the push does not move on to its first stop.An application calling this is code moving the focus, so the two
FOCUSchanges it announces carryCODE. A gesture that moves focus does not come through here: a click and a Tab enter the funnel from the scene's own input paths, and the two components that move focus out of a gesture -- a tab strip's arrow key and a radio group's -- enter it throughWidget.requestFocus(Change.Origin). Reading the method rather than the entry point would report every keyboard-driven focus move in the toolkit as made by code. -
focusTraverse
public void focusTraverse(boolean backward) Moves focus to the next/previous focusable widget in layout (DFS) order.CODE, because this is the method an application calls; the Tab key reaches the same traversal through the scene's own key path and announcesUSER. -
addTicker
Registers an animation ticker on scene time; frames keep coming while any is active. Safe to call from inside another ticker'stick(e.g. one animation kicking off another); such a ticker joins on the next frame.Its
dtis clamped toMAX_TICK_SECONDSand multiplied bytimeScale(), sosetPaused(boolean)freezes it. That is the right default for anything the app animates. UseaddRealTimeTicker(limn.scene.Scene.Ticker)for an animation whose completion frees a resource or settles state; freezing one of those hangs it. -
addRealTimeTicker
Registers a ticker on wall time:setTimeScale(double)andsetPaused(boolean)do not reach it. Itsdtis still clamped toMAX_TICK_SECONDS: that guard is about a stalled clock, not about intent.For animations that are not content: the toolkit's own window / dialog / menu fades, widget transitions, scrollbar fades, progress sweeps. Two reasons they belong here. The hard one: several of them own a lifecycle; the fade-out is what destroys the popup window, removes the overlay that is capturing input, or completes the dialog's future, so a pause that froze them would leave a half-closed dialog on screen and a future nobody ever completes. The soft one: shell feedback that stops responding is read as a hang, not as a pause; a paused app should still highlight the button under the pointer.
-
disposeLater
Schedules aGpuSurfacefor disposal at the next frame, when this scene's window/GL context is current. Widgets that own GPU resources (e.g. a 3D viewport) call this fromWidget.onDetached()instead of disposing inline, becausedispose()would run without a current context. -
disposeLater
LikedisposeLater(GpuSurface)for any GPU cleanup that must run with this scene's GL context current, e.g. a retained 3D scene'sScene3D.dispose(). Runs once, at the top of the next frame. -
setTimeScale
public void setTimeScale(double scale) Speed of scene time, the clock everyaddTicker(limn.scene.Scene.Ticker)ticker integrates. 1 is real time, 0.5 slow motion, 2 double speed, 0 frozen. Never negative: nothing in the toolkit's animation model runs backwards, and a negative dt would driveTransitionpast its start with no way back.What it does not touch. Wall time keeps running underneath: input, layout,
Ui.postDelayed(java.lang.Runnable, long)(so the caret keeps blinking), the frame metrics, the tooltip and modal-scrim fades, and everyaddRealTimeTicker(limn.scene.Scene.Ticker)ticker, which is where the toolkit puts its own shell animation, so a paused app still opens dialogs, closes menus and fades windows normally. Scene time is for what the app animates, not for the chrome around it.- Parameters:
scale- a finite factor ≥ 0
-
timeScale
public double timeScale()- Returns:
- the current scene-time scale (see
setTimeScale(double))
-
setPaused
public void setPaused(boolean newPaused) Freezes or resumes scene time, keeping thetimeScale()the app chose (so a paused slow-motion scene resumes in slow motion). Equivalent in effect to a scale of 0.No time is banked while paused: the paused seconds are never replayed. A scene-time ticker resumes measuring from the last frame that actually ran, which is the resume itself when the pause stopped the frame pump (so
dt == 0), or the last frame a real-time ticker kept alive. Either way what it receives is the time since that frame, not the length of the pause. A ticker registered while paused still gets its owndt == 0first frame, whenever it finally runs. -
isPaused
public boolean isPaused()- Returns:
- whether scene time is frozen (see
setPaused(boolean))
-
addShortcutHandler
Registers a scene-wide keyboard handler for chords the focused widget did not want: menu accelerators, and anything else that must work without being focused.The ordering is the contract. A key event is offered, in this order, to:
- the focused widget, bubbling to the root, so
Ctrl+Cinside a text field with a selection is the field's copy and never a menu's; - every handler registered here, oldest first, until one returns
true; - Tab traversal, if the key is Tab and still nobody took it.
Returning
trueconsumes the event and stops both the remaining handlers and the Tab fallback. Handlers see presses, auto-repeats and releases alike: a shortcut whose trigger is a modifier let go of (a bare Alt reaching for the menu bar) has nothing else to key on.One system reverses the first two for a menu bar: where the bar is shown in the system's own, on macOS, the system runs a bar's accelerator before the key is sent to the window at all. See
MenuBar.Handlers are not consulted while a modal overlay owns the scene's keyboard: an open dialog or an in-scene menu has it, and a shortcut belonging to what it covers must not fire behind it. An overlay that gave the keyboard back and is fading out, or a list that leaves the keyboard in the field that opened it, does not own it.
A handler that throws is contained and read as did not handle, so the chord goes on to the next handler and to Tab traversal -- the policy
Work.deliverIfalready states for the same shape.Registering or unregistering from inside a handler is legal and takes effect on the next event, never on the one being dispatched.
- Parameters:
handler- consulted for chords nobody focused wanted; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
- the focused widget, bubbling to the root, so
-
canEdit
Whether an editing command can be done now by the widget that has the keyboard here, or by one of its ancestors. What a menu's item of an editing role is enabled by, and what a tool bar's button for the command may be.- Returns:
falsewith nothing focused
-
canEdit
Whether an editing command can be done now byfromor by one of its ancestors: the questioncanEdit(EditCommand)asks of the focused widget, asked of a widget the caller remembered, as a menu does of the one that had the keyboard before it opened.- Parameters:
from- a widget of this scene, ornull, which can do nothing
-
edit
Does an editing command in the widget that has the keyboard here, or in the first of its ancestors that can, as the user's own act: what the command's chord does there.- Returns:
- whether a widget did it
-
edit
Does an editing command infrom, or in the first of its ancestors that can.- Parameters:
from- a widget of this scene, ornull, which does nothing- Returns:
- whether a widget did it
-
setDragKey
public void setDragKey(int key, int modifiers) Sets the key that begins a drag from the keyboard on the focused widget. The default is Ctrl+Shift+D, and Cmd+Shift+D on macOS.- Parameters:
key- aKeysconstantmodifiers- theKeys.MOD_*bits held with it
-
isDragging
public boolean isDragging()- Returns:
- whether a drag is in flight in this scene
-
renderFrame
Full frame: animation tick → layout (if dirty) → paint (root then overlays) → modal scrim. -
renderFrame
- Parameters:
rePresent-truewhen re-drawing the same already-settled frame only to converge the double buffers (the backend's double-present anti-flicker): identical pixels, but excluded frommetrics()so FPS/frame-time count only frames rendered for content.
-
renderFrame
Renders the frame a window's backend asked for, with everything the backend said about it: the re-present flag, the GPU-time sample and the back buffer's age. A frame callback of an application's own, one that does something before or after the scene paints, forwards theFrameInfowhole through here.It replaced an overload that took the flag and the sample and assumed the age, which every such callback in the demo called: when the backend began reading the age from the driver, those callbacks went on dropping it, and on Wayland, where the age is three or four, the demo's window drew black but for the status bar that changes every frame (2026-09-25).
- Parameters:
canvas- the backend's canvas for this frameframe- what the backend passed to the frame callback
-
renderFrame
- Parameters:
gpuFrameMs- backend-measured GPU time of a recently completed frame in ms (Float.NaN= no new sample); recorded intometrics()alongside this frame's CPU numbersbufferAge- how many presents old the canvas's contents are (FrameInfo.bufferAge): 0 repaints the whole frame, 1 what changed since the last frame, 2 that plus the frame before, and so on. The overloads without it assume 2, ordinary double buffering, which is what rendering into a canvas of one's own has always been treated as
-
installTooltipStyle
Installs the themed tooltip appearance (the components layer calls this once). -
fadeWindowIn
public void fadeWindowIn(double seconds) Snaps the window opacity to0immediately, then eases it to1overseconds, the flash-free "appear" sequence. Call it right beforeNativeWindow.show()so the window is already transparent when it first maps. -
fadeWindowOut
Eases the window opacity to0, then runsonGone(typically a close), by the rules offadeWindow(float, double, java.lang.Runnable): never inside this call, and afterseconds(or on the next turn of the UI queue for none) when the fade goes straight there. -
fadeWindow
Animates this scene's native-window opacity towardtarget([0..1]) overseconds, runningonArriveonce it settles. A newer call supersedes an in-flight fade without stacking tickers, and itsonArrivenever runs. Zero duration, an already-current value, or a headless scene (no window) jumps straight totarget, andonArriveruns once, aftersecondswhen it is more than zero and on the next turn of the UI queue when it is not, unless a newer call supersedes it first. Never inside this call: a fade asked for again in its ownonArrivedoes not recurse, and one that asked for seconds keeps its pace while the loop sleeps between steps. A window closed before then runs it as it closes, as it runs a fade's in flight. Under Wayland, which gives a program no window opacity, nothing shows, and the fade keeps the same time and runsonArrivethe same way. UI thread only. -
observeWindowClosed
Runsobserverwhen the bound window is destroyed, the hook for completion work driven by tickers/frames, which stop forever at that point (an abandoned fade would otherwise leak an uncompleted future). Observers run once and are dropped; the returned handle unregisters earlier (call it when the normal path completed first).- Parameters:
observer- told once, when the window is destroyed; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
layoutPass
public void layoutPass(float newWidth, float newHeight) Runs measure/layout when dirty or resized (public for headless tests/embedding).What the new sizes take out of reach waits for the next frame: a transform that a pass turns edge-on, so that it draws its child as nothing, has the popups hung inside closed and the focus, the hover and a press inside taken out when
renderFrame(Canvas)runs, which lays out again before it paints, so that no handler edits a tree while it is being laid out. A caller that runs this alone and reads the scene before rendering a frame still finds such a popup open and the focus inside.- Parameters:
newWidth- the window's widthnewHeight- the window's height
-