Brodgar client

Panels and tabs

Where OptWnd keeps its pages and how one is swapped in, Tabs — the client’s one tab coordinator, which is not a widget at all — and PackCont, the one container that keeps itself packed. The frame around a window is chrome; the controls inside a panel are controls and lists; the tree itself is the widget system.

Tabs — a coordinator that never joins the tree

Tabs is a plain class, not a Widget. It holds the tab list and the current tab; the Tabs are widgets, and they are children of the parent the constructor was handed rather than of Tabs. So nothing addresses a Tabs: it has no place in the tree, no id and no role, and its c/sz are bookkeeping for the tabs it places.

What Where
Construction Tabs(Coord c, Coord sz, Widget parent) — c is where every tab body is placed in parent, sz the box each is born with
Minting a tab Tabs.add() → parent.add(new Tab(), c), reading c at that moment. A caller that wants the bodies under their buttons therefore sets Tabs.c and moves the tabs afterwards, because a TabButton needs its Tab and a Tab is placed where Tabs was already told
The first one wins Tab() makes itself curtab when there is none and hides itself otherwise, so whichever tab was minted first is the one showing
The button Tabs.TabButton(int w, String text, Tab tab) — an ordinary Button whose click() is showtab(tab), added by the caller wherever it wants it
The swap showtab(Tab) hides the old, shows the new, then calls changed(Tab from, Tab to) — the one overridable hook, empty by default
Sizing resize(Coord) folds over every tab · contentsz() is the union of them · pack() = resize(contentsz()), so every tab ends up the same box · indpack() packs each on its own instead
Teardown Tab.destroy() takes itself out of Tabs.tabs; curtab is not re-pointed, so a Tabs whose current tab was destroyed is showing nothing

Tabs.resize(Coord) is not a Widget.resize override, because there is no Widget here to override. Each Tab it resizes is one and reaches the base method as usual — what carries no seam is the coordinator, and nothing can address that anyway.

OptWnd — the panels one window swaps between

One window, and GameUI.opts, GameUI.kb_opt, GameUI’s private togglewnd and LoginScreen’s own instance all point at the same one. It is opened on OptWnd.main every time: show() is chpanel(main) before super.show(), so the panel the user left it on is never what it comes back up on.

What Where
The panels OptWnd.main — the game menu — and OptWnd.settings, the tabbed settings view. A third is built by a PButton on first press
The swap chpanel(Panel) hides current, shows the new one, writes the caption, and calls cresize
The caption Panel.cap is what the window is called while that panel shows, null for a panel that carries none. chpanel writes it through Window.chcap behind an equality test, chcap being the one caption seam
A panel is born hidden Panel() sets visible = false at Coord.z; whatever puts one on screen calls show() itself
Where a resize stops OptWnd.cresize(Widget) repacks the window and re-centres it on its old middle — only when the child is current, so a panel that is not showing may resize freely
The game menu OptWnd(boolean) fills main with one PButton per page, top to bottom, y carried from each button’s pos("bl"); the session entries below them only when the flag is set, which is LoginScreen’s OptWnd(false) leaving them out
The menu button OptWnd.PButton builds its panel on first press, chpanels it and keeps it. Its fresh flag destroys the previous build first, which is what a panel whose contents are read at build time needs
When a panel’s hide() runs from chpanel, and only there: on a PButton press, and on every OptWnd.show(), which is chpanel(main). Closing the window — the close box, Escape, reqclose() — is Widget.hide() on the window: current stays visible inside it until the next show(). A panel that acts on leaving the screen watches tvisible() from tick as well
Escape nothing under this window binds it, so it reaches Window.keydown and closes the window like any other

The settings view — a list on the left, a panel on the right

OptWnd.SettingsPanel is a Tabs strip over one list and one panel holder per tab. The list is the navigation: picking a row puts that row’s panel in the holder beside it, and no panel carries a button back to anywhere.

What Where
A row OptWnd.PanelEntry — the name the list draws, a Supplier<Panel>, and PButton’s own fresh flag, which is what the keybinding page needs
The list SettingsPanel.PanelList, an SListBox<PanelEntry, Widget> whose rows are SListWidget.ItemWidgets holding a Label — a Label rather than an SListWidget.TextItem because a navigation row is read back by name, and a TextItem’s caption is a private raster
Both selections funnel through one method PanelList.change(I) — a row click reaches it through ItemWidget.mousedown, and a selection written from anywhere else lands in the same place
No deselect PanelList.unselect(int) returns without clearing, where every other SListBox in the client answers a click on empty space with change(null)
The holder, and the swap one plain Widget per tab; SettingsPanel.Subject.show(PanelEntry) hides what was in it and adds or re-shows the one picked
The page box OptWnd.PAGE — the box every holder is built at and the height every list is built at. It is a declared constant, not a page’s own size, so a panel bigger or smaller than it changes what is drawn and never the window
Laying out once, at the end of the SettingsPanel constructor: Tabs.pack() then pack(). Nothing in the view has a size that moves afterwards, so no swap re-packs anything
A page with a port of its own BindingPanel builds its Scrollport at OptWnd.PAGE less the PointBind standing under it, so the page fills the box; the rows inside it spread to Scrollcont.sz.x, which is what Widget.addhl lays each caption and key button across. Fork: the Performance page builds its controls into a body of their own and, when that body is taller than OptWnd.PAGE, stands it in a Scrollport of exactly OptWnd.PAGE
A tab whose rows are a census one list is built once and one is re-read — in SettingsPanel.show(), and again from tick whenever a generation counter it watches moves while the view is on screen (tvisible()), because what belongs in it changes while the window sits there. The gate is the fork’s: the panel ticks hidden inside the OptWnd that GameUI builds hidden, its counters start out unequal, and the session’s first tick used to re-read the census, put the first addon’s page in the holder and hand its fill to the layer’s next step — 200–240 ms of Lua on the UI thread at login, for a page nobody had opened. What a :reload leaves stale in a view that is not showing is rebuilt by the show() that brings it back
A row’s identity across a re-read PanelEntry.key, defaulting to the row’s own name. The entries are minted fresh each time, so the selection cannot be remembered by identity
Swapping a whole list SettingsPanel.Subject.reset(List) — destroys every panel the old entries built, swaps the contents of entries, then re-picks the row with the same key, else the first. The SListBox needs no telling: update() re-reads items() every tick and diffs it by identity
Where a supplier runs inside the window’s tree: Subject.show is reached from PanelList.change in the input pass and from tick through reset, PButton.click from the press — the gotcha below is what follows

Gotchas

PackCont — the client’s one container that follows its content

The base of the client’s linear layouts, and the one place src/haven states the rule a box packed once stays packed: after the first pack(), every child entering, leaving or resizing re-packs the container. GobIcon’s settings window is its one user in src/haven (a VPack with packpar(true)); the wire type linpack mints one for the server.

What Where
The flag PackCont.packed, private — set by the first pack() and never cleared, so following is a one-way switch
The seams add(T), cdestroy, cresize → repack0(), which is repack() + pack() only while packed; before the first pack() a child entering is placed and nothing is measured
Up the tree packpar(true) makes pack() end in parent.pack(), so a window around a VPack refits on every change — the window’s own cresize stays the base no-op
The linear layout LinPack keeps its own order list beside the child list — last/insert/after/before(child, …, pad) place a child in it, cdestroy takes it out — and repack() walks order: VPack writes y and keeps each child’s x, HPack the mirror, margin between two. addchild’s spec is a char, 'l'/'i'/'a'/'b', for the same four

⚠️ A child moved or hidden reaches no seam. contentsz() skips a hidden child, but neither hide() nor move() calls anything on the parent, so a packed container keeps the box of the child’s old place until the next add, resize or destroy. GameUI.Hidepanel (GameUI’s own windows) has the same hole, and a container of your own that wants to follow those two has to be told from the writer’s side.

See also