| Universal action send |
Widget.wdgmsg → UI.wdgmsg → RemoteUI.rcvmsg → Session.queuemsg |
Which UI a send reaches: the widget’s own |
Widget.wdgmsg(sender, msg, args) recurses up parent and only the root (parent == null) calls ui.wdgmsg, so the UI is the one that widget’s tree hangs from — never an ambient “current” one. With several sessions live that is what makes a send driven off a widget land on that session: BuddyWnd.Buddy.chname/chgrp/endkin/forget and Speedget.set all reach the server through the login whose HUD holds them, drawn or not. UI.wdgmsg then runs the fork’s outbound action-stream dispatch before UI.rawWdgmsg, which resolves the id against that same UI’s rwidgets |
| Map clicks / move / itemact / place / sel |
MapView (Click.hit, drop, itemact, place, sel) |
⚠️ "click" is not the map’s — seven senders, and only one carries a place |
MapView.Click.hit {pc, mc.floor(posres), btn, modflags} is the only shape with a world coordinate in it. Beside it: Avaview.mousedown and GiveButton.mousedown send {btn}; ISBox.mousedown sends nothing (and "xfer" instead under shift); Img.mousedown sends {ev.c, btn, modflags}, where ev.c is widget-local device pixels and not a place at all; Fightview and Partyview send {gobid, btn}. So a tap keyed on the message NAME must read Widget.getClass() before it reads an index — arg 2 is a world Coord in one sender, a button in two, a Coord on a picture in another, and absent in a fifth |
| ⚠️ …and a minimap click arrives as the map view |
MiniMap.mvclick calls mv.wdgmsg("click", …) on the MapView, not on itself, so the sender is indistinguishable from a real map click. It fabricates pc from ui.mc when handed none, and builds mc from loc.tc against sessloc — the tile CENTRE (add(tilesz.div(2))), not a projected point. With a gob it appends Gob.GobClick’s tail, so the same name carries a 4-arg and a 9-arg shape from one method |
Click.hit is the one point where a click’s GOB is known |
Click.hit → clickhit resolves clickedgob(inf) and then sends; every fork tap hangs there (onGhostClick consumes and returns, the voice move intent for a ground click, the click token). ⚠️ It runs on the hit-test’s own thread, not the UI thread, and only for clicks that reach the map — an inventory or HUD click never passes here, which is exactly what makes “was this menu opened on a gob?” answerable |
| Attributing a later widget to an earlier click |
a time window alone is approximate by construction (the player clicks, nothing opens, the server puts a menu up much later). The exact form keys on UI.lcc instead — see widget-input.md |
| Gob click arg encoding |
Gob.GobClick.clickargs, Composited.CompositeClick |
| Menu action by path / id |
GameUI.act, MenuGrid.PagButton.use |
| Flower petal select |
FlowerMenu.choose — wdgmsg("cl", num, mods); cancel = "cl", -1. Not a close seam: BuddyWnd’s subclass overrides it, never calls super, and sends no cl — it calls uimsg by hand. choose(null) is the one door Esc (keydown) and a click away (mousedown) share, so it is what a programmatic cancel drives |
| A click picks from the ring’s first frame |
FlowerMenu.mousedown swallows the press while anims is non-empty, and the fork registers no anim on the ring — see radial-menu.md. Petal.mousedown → choose(this) is only reached after the parent lets the event through; choose itself has no guard |
Flower menu lifecycle (@RName("sm")) |
added grabs mouse and keys (so nothing can be typed while one is up) and organize(opts) lays the ring out inside it — the END of added is the moment the petal set is complete and placed. uimsg "act"/"cancel" are the commit points: each drops both grabs and then calls ui.destroy on the widget, so the ring leaves the tree the instant it stops holding input. ⚠️ That order is load-bearing: a ring that outlives its commit stands in the tree holding no input, a second sm lands beside it, and ui.root.children(FlowerMenu.class) answers two, the dead one first. BuddyWnd’s destroy() does call super |
| Item take/drop/transfer/iact |
WItem.mousedown — take {cc}; drop/transfer {cc, n}, where the modifier keys select n (shift = transfer 1, shift+ctrl = all, ctrl = drop 1, ctrl+meta = drop all), so those three carry no modifier field at all; iact {cc, modflags} |
⚠️ itemact names no held item — three senders, one meaning |
The subject is implicit: DTarget.Interact’s src is the ItemDrag, so the bytes mean whatever is on the cursor. WItem.iteminteract = {modflags} (1 arg, onto another item); MapView.iteminteract = {pc, mc.floor(posres), modflags} (3, bare ground), extended with inf.clickargs() when the hit resolves to an object (8). Arg count alone tells the three apart |
| Server → widget update |
Widget.uimsg, dispatched via UI.uimsg |
| ⚠️ The post-apply tap runs outside the monitor |
UI.UiMessage.run closes its synchronized(UI.this) before the fork’s post-apply // addon: line, and reaches it only when the update was actually applied (a swallowed one skips it). So anything hung there is on a Loader thread with no lock held: record the widget, read it on the tick |