Covers: the two recursion seams, what an override of
drawdoes and does not carry to its children, where the screenGOutis built, and what a frame allocates. Split out of widgets.md, which keeps the tree and the create/place/destroy seams.
| What | Where |
|---|---|
| Tick root | UI.tick → dispatch(root, new Widget.TickEvent(delta)) → UI.dispatch → ev.dispatch(to) |
| Tick recursion point (the ONE seam) | Widget.Event.dispatch — handle(this) then propagate(w); TickEvent.dispatch overrides it to carry visible |
| Tick descent / leaf call | TickEvent.propagation walks from.child→next; shandle calls w.tick(ev) |
| Draw root | UI.draw → root.draw(g) (direct — not through a dispatch), then afterdraws |
| Draw recursion point (the ONE seam) | Widget.draw(GOut,boolean) child loop — xlate+reclip(l), CPUProfile.begin(wdg), Fonts.frame(wdg), wdg.draw(g2) |
gtick (render hand-off) |
UI.gtick → GTickEvent — a separate pass, inside the same utick phase |
| Frame phases around them | UILoop.Frame.tick (dsp/stick/utick), display (draw) |
| Per-widget cost probe (fork) | // addon: Widget.prof + profadd (Widget), the two seams above, and the root bracket in UI.draw |
| Per-widget decoration seam (fork) | // addon: Widget.addonovs (null on a widget carrying none) + the call in the child loop placed after wdg.draw(g2) and outside Fonts.frame(wdg), with the root’s twin in UI.draw after root.draw(g). The g2 it is handed is already translated and clipped to that child’s box |
Gotchas. Draw iterates child→next (bottom-first), hit-testing lchild→prev (topmost-first) — opposite,
both correct. draw skips !visible children; tick does not (TickEvent.visible goes false for the subtree).
The root is reached by neither seam (its “parent” is UI), so anything wrapped per widget misses it unless
UI.draw/UI.dispatch is handled separately. TickEvent.shandle tolerates a widget removing itself mid-tick —
hence the cached next — and a widget may equally rebuild its own child list in its tick: handle
(→shandle, the widget itself) runs before propagation, which then re-reads from.child fresh. That is
what makes Window.tick a legal place to chdeco (035).
Monitor gotcha — a tick is a callback with that tree’s monitor ALREADY HELD, and so is a draw.
UILoop.Frame.tick takes synchronized(ui) around ui.tick(), and UILoop.display takes it around
ui.draw(g) — so every Widget.tick and every Widget.draw in the tree runs inside it. A widget added to a
tree purely to get a per-frame callback is therefore a per-frame callback that starts out holding a lock,
which is invisible at the call site: nothing in tick(double)’s signature says so, and the traversal that
reaches it is three classes away. Anything such a callback does that takes a second UI monitor — and there
is more than one UI in this client (multi-session.md) — nests two, against a Loader
thread that holds them the other way round in Widget.add. Per-frame work that must reach another tree does
not belong on a widget at all; call it from Frame.tick, outside both blocks, where it holds none.
super.draw gotcha — a subclass decides whether its children are painted at all. Widget.draw(GOut) is
draw(g, true), which is the child loop and nothing else. So a class that overrides draw(GOut) without
calling super.draw(g) paints itself and then paints no child, and around forty of the overrides in
src/haven do exactly that — Buff, IMeter, MenuGrid, FlowerMenu and ChatUI among them. Nothing else
about such a widget shows it: its children are added, ticked, hit-tested and listed by children() as usual,
and only the picture is missing. Adding a child is therefore not a general way to draw over an arbitrary
widget. WItem is the exception in this tree: an item icon is the first widget anyone wants to put
something on top of, so it chains (// addon:, last, over the sprite and its info overlays) and a child built
into an icon is drawn.
And WItem is the only icon that chains — the other three do not. Makewindow.SpecWidget.draw is
drawbg + drawicon, SListWidget.IconText.draw (which BAttrWnd.Constipations.ItemIcon extends) is
drawicon + drawtext, and a .res widget drawing its own ItemSpec answers to nobody here: none of them
calls super.draw, so a child added to one is invisible. The decoration seam in the child loop above is
unaffected, because it is the parent that paints it after wdg.draw(g2) returns — so an overlay lands on
every one of those widgets, and only a child widget does not.
The style frame is per widget and nests. Fonts.frame(wdg) is opened around wdg.draw(g2) in the loop
above, so it covers that widget’s own text and its whole subtree while its siblings resolve under whatever
the level above them carries —
and Fonts.gen() therefore differs between draw sites within one frame, which is what makes it a key
component rather than a generation to compare against. The root’s own frame is UI.draw’s, since the loop
never reaches the widget it starts from.
| What | Where |
|---|---|
Where the screen GOut is built |
UILoop.display: basestate() (a BufPipe .prepped with wnd.fbstate() — the framebuffer state the toolkit window itself supplies) .prep blend + States.Viewport + Ortho2D + FrameInfo, buf.clear(...), then new GOut(buf, base, wnd.sz()) → ui.draw(g) under synchronized(ui). The 3D scene is inside that traversal (the MapView is a widget), so this ONE Render carries the whole frame in order — see world-3d.md for drawing a subtree into a texture instead |
| Clipping a child’s target | GOut.reclip(c, sz) (strict) / reclipl (loose) — the child loop picks by its own strict flag, and a strict box is what cuts a child’s drawing off at its own edge |
A parent’s chcolor reaches no child |
GOut(GOut), the copy every reclip is built on, copies def2d and never cur2d: a child’s GOut starts from the tree’s default 2D state, so a chcolor set on the parent’s GOut before super.draw(g) tints what the parent itself draws and nothing below it. chcolor() is cur2d.put(BaseColor.slot, …) — a reset to the default’s colour, not to the previous one — and defstate() copies the whole default back. Fork: GOut.tinted(Color) (// addon:) is a sub-view whose default carries a BaseColor, so the tint descends through every reclip beneath it; under it chcolor() comes back to the tint and chcolor(c) is c times it. BaseColor is a multiply in the fragment shader (u_color), one slot, so two tints never stack — the later chcolor replaces the earlier |
| 2D drawing itself | GOut — image/text/rect/line/prect/chcolor; the overloads and the 9-slice contract are in widgets.md |
A PView draws into whatever GOut it is handed |
PView.draw renders its scene into its OWN colour target (fragcol, a texture the id_fb basic state sizes from its FrameConfig) and then resolve(g) blits that texture into the given GOut (resolveout(g, …).image(…)), post-processors and the resampler applied on the way. So an Avaview, a MapView or any other PView composes correctly into an offscreen 2D target as it does into the screen’s — the mechanism a widget standing in the 3D world and a mirror of a widget both rest on. PView.frame() composes a Glob.FrameInfo from ui.sess, so the widget must still stand in a session’s tree. ⚠️ A PView scene renders ONCE per frame: PView.maindraw → GLDrawList.draw → settingbuf.get(0) (GLDoubleBuffer) waits for the previous submission to have run on the GL thread, so a second draw of the same view inside one frame’s Render blocks the UI thread for ever — holding synchronized(ui), which stops the whole client with no error. Fork: PView.draw (// addon:) keeps the Render it last rendered into (lastout, one per frame from UILoop) and a second draw into it only resolves |
gtick reaches a PView through the widget tree alone |
UI.gtick(Render) dispatches a Widget.GTickEvent over the tree, and PView.gtick runs its TickList (a Composited’s equipment sprites, a Skeleton.Debug); UILoop.Frame.tick sends it to the drawn session and to the addon layer, and Sessions.gtick sends a background member its Glob.gtick only — a PView in a session nobody is looking at is ticked (u.tick(), so Avaview.tick/Composited.tick pose it) but never gticked unless something dispatches the event to it (ui.dispatch(widget, new Widget.GTickEvent(out))) |
| Drawing a tree to an image, off screen | haven.rs.DrawBuffer(Acephal.instance().env(), sz) — a real GL target on the headless renderer (JOGLOffscreen, which needs no java.awt.headless and no window) whose draw(g -> …) runs the very same Widget.draw pass; g.getimage(img -> …) hands the pixels back. Window.main is the one-widget example. It is the only check that sees what the pass does to a widget’s geometry — Frame.xlate, a Label restyling on its first draw — where a read of child.c and child.sz does not: a layout question is settled there, not on a picture composed by hand from the rasters |
GOut per visible child per frame: Widget.draw reclip(l) → GOut.reclip2 new GOut(this) →
ctor def2d.copy() → BufPipe.copy. ≈ 0.5 kB/widget (a State[] of numslots(), ~60)
⇒ ~0.4 MB/frame at ~700 widgets.drawp builds Model+VertexArray+float[] per call,
image(BufferedImage) a whole TexI, atext is render→tex→blit→dispose per call (a Label dodges
it by holding its Text; immediate-mode cannot). True of every haven draw site; the fork’s own draw
path holds its rendered Text in a bounded LRU instead, and so never reaches atext.UILoop.framealloc reads ~11 MB — the bulk is not here. haven/render/gl
(BGL = one Command per GL call; GLDrawList = incremental) has no subsystem file: pay that toll first.