Commit Graph

201 Commits

Author SHA1 Message Date
97c269576e scroll the controls panel's rows instead of clipping them
REQ-UI-CONTROLS-PANEL said the content scrolls once it outgrows the space
available; only the height cap was implemented, so the surplus rows were simply
cut off. Silent row loss is the one failure this panel must not have, and it
was reachable: the Selection context runs to fifteen rows, and a panel that has
also had to rise above the build button bar can be left with less room than
that.

The rows move into a scroll area. The heading stays outside it, so it remains
visible and remains the collapse control whatever the rows are doing.

The size can no longer come from the panel's own layout -- a scroll area's hint
describes a viewport, not its contents -- so the heading and the rows are
measured directly and the chrome added. Verified against a forced rebuild and
an artificially cramped view:

  roomy:    heading 56x13 + rows 124x188 -> 142x223, no scrollbar
  collapsed:                             ->  74x31, same bottom edge
  cramped:  wanted 223, band 124         -> 159x124, +17 for the scrollbar

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 20:39:06 +02:00
a5c51f08f8 put the controls panel in the corner and let it step around the build bar
It was confined to the band above the build button bar's strip, borrowing the
selection panel's rule. That rule exists because the selection panel is
full-height on the right, where the bar's strip is genuinely in the way. This
panel is short and in the corner, and the bar is centered and sized to its
buttons, so the corner is normally free -- reserving the whole strip pushed the
panel up for a collision that was not happening.

It now sits in the bottom-left corner and shares the view's bottom edge with
the bar, rising only when the panel's rectangle would actually intersect the
bar's, in which case it clears the bar's top by the usual margin and caps its
height at what is left. So it moves for a wide bar and a wide panel, and drops
back into the corner as soon as they no longer meet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 20:07:22 +02:00
3c4688bdb6 mark the exit row on its chips instead of its label
The label was painted palette(bright-text), which is not a destructive color at
all: Qt has no such role, and bright-text is white by design, meant for text
over dark highlights. On this panel's light chrome it was white on grey.

The chips carry the warning now and the label keeps the ordinary text color, so
the row stays legible whatever the palette and only the binding is marked --
which is what REQ-UI-CONTROLS-CARD asked for in the first place, and what the
mockup shows. The red is a literal because no palette role means it, chosen to
read on a light and a dark background alike, and it is widget chrome, so like
the rest of this stylesheet it is deliberately not a visuals.toml color.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 19:47:56 +02:00
d7c6734a2b show the controls panel's rows before measuring them
A widget created under an already-visible parent starts hidden, and a layout
counts a hidden item as empty -- it adds nothing to the size hint. Nothing
showed the freshly built rows until the event loop next ran, long after the
panel had measured itself, so every rebuild measured an empty card.

That one fact explains both symptoms. Originally refit() read a stale cached
hint, which described the previous context and so drew the card one rebuild
behind. Invalidating that cache to fix it replaced a plausible wrong answer
with the true one for a card whose rows were all still hidden, which is why the
panel then collapsed to its heading on every mode change.

Measured on a forced rebuild, with the rows shown and without:

  shown:   rowItems=10 hidden=0  rowsHint=188  needed=140x221
  hidden:  rowItems=10 hidden=10 rowsHint=6    needed=72x39

39px being heading plus margins -- the collapsed card exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 19:36:24 +02:00
1604dc02a2 measure the controls panel against its content, not its old geometry
The previous fix went too far: it activated the panel's own layout before
resizing, which lays the heading and the rows out inside the geometry left over
from the previous context. The panel then took that stale frame as its answer
and collapsed to almost nothing whenever the mode changed.

Only the rows layout is activated now -- that part was right, and is what makes
freshly added rows visible and so measurable. The size comes from
layout()->totalSizeHint(), which says how big the content needs to be without
reference to how big the panel currently is; setGeometry re-runs the outer
layout afterwards on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 19:28:15 +02:00
b346f3dfc1 size the controls panel to the rows it is actually showing
The card was drawn one rebuild behind: selecting something for the first time
sized it for the context before it, and the next selection sized it for that
one. refit() activated the panel's outer layout but never the rows layout
underneath it, so it read a size hint describing rows that were no longer
there, while the freshly added ones were not yet shown and so counted for
nothing. Both layouts are now invalidated and re-run innermost first, and the
rows are polished before being measured -- the badge chips carry border and
padding, which a label reports only once the stylesheet has reached it.

Also drops letter-spacing from the stylesheet. Qt has no such property and
warned once per widget it was applied to, which was most of the console. The
heading and the caption set it on their QFont, where it works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 19:21:28 +02:00
37d9166348 divide the always-available rows in the General context too
The General card was the one that ran its context rows and the global ones
together as a flat list. That made it the exception a player has to notice: the
same six rows sit under a caption everywhere else, so leaving them uncaptioned
here asks the reader to work out that they are the same six.

Requirement wording corrected with it -- it claimed the always-available rows
were the General context's entire content, which was never true. General has
Select, Select area and Deconstruct mode of its own above the divider, exactly
like the other contexts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 19:06:01 +02:00
fe69921b8e show the available controls in a panel over the world
The panel decides nothing about what the controls are: it asks the same
resolver the key handling and the mouse dispatch ask, and renders each badge
from the binding that resolver matches. A chip cannot claim a key that does
nothing, and a label cannot describe a click that does something else, because
neither is written here.

Display text is the one part that is not shared -- lib/core says what is
available and what triggers it, ControlActionText.cpp says what it is called.
That split is why the table can stay free of UI strings and still be the single
source of the pairing.

It refreshes on a timer rather than by subscribing. Two things that change a
row -- a belt drag starting, the ghost crossing a transfer target -- happen on
mouse movement and publish nothing, and they must still show while the game is
paused, so there is no event and no tick to hang it on. Resolving is comparing
two vectors of enums, and the rebuild is skipped unless they differ.

Left edge, bottom-aligned in the same band the selection panel is confined to,
so it clears the build bar's strip and never meets the panel on the opposite
edge. Clicking the heading collapses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 17:30:22 +02:00
3e79b09fec resolve mouse gestures through the action table too
The panel has to say what the left button does right now -- "Place" or "Apply
settings", "Select" or "Toggle deconstruct" -- and that answer lived in the
if-chain at the top of mousePressEvent, where the panel could not reach it.
So the chain becomes a switch on the resolved action, and the panel will read
the same resolver. Which branch runs is now decided in one place; what each
branch does is untouched, drag state machines and all.

Right-click gets the clearest win: cancel-the-drag versus leave-the-mode was
two nested conditions inspecting belt state, and is now the two actions the
table already distinguishes for the panel's sake.

Ctrl variants fall back to the plain gesture wherever nothing claims them,
which is what keeps Ctrl+click placing a building in builder mode and Ctrl+drag
deconstructing an area -- the modifier means something only where an action
says it does, rather than every handler re-deciding whether to ignore it.

Behaviour is unchanged. Full suite passes; the app runs clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 17:24:25 +02:00
77bbd58d02 resolve keyboard shortcuts through one action table instead of a switch
The controls panel needs to say what each key does right now, and a panel that
keeps its own list of that is a list that goes stale. So the list moves into
lib/core/ControlAction.h: which actions exist, what each is bound to, and when
each does something. InputMapper stops deciding that and switches on the
resolved action instead, so the panel and the key handling cannot disagree
about what Q means -- there is only one place that says.

The table declares; it never performs. It holds no simulation access, fires no
events, and names nothing: display strings live in the ui target, which formats
the bindings this hands it, so a badge is rendered from the real binding rather
than typed beside it. What an action *does* stays exactly where it was.

ControlContext is the snapshot the rules read, which is what keeps this
testable without a world. Two of its facts come from BlueprintLibrary, which is
built after the world view and so arrives by setter; one comes from the new
hovered-transfer flag on BuildModeController, resolved once on mouse-move
through the same classifier the click and the ghost colour already use.

Behaviour is unchanged, deliberately. Ctrl still separates the chords and every
other modifier is still ignored, so Shift+A pans as before; matching modifiers
exactly would have silently swallowed those presses. Build hotkeys and F3/F4
stay outside the table -- the first are advertised on the build buttons and
already derive their badges from the handler's own table, the second are
development controls the panel must never offer.

The tests are the point of putting this in lib: every row's bindings must
resolve back to that row's action in that same context, which fails the moment
a shown row and its handler part ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-07 17:20:44 +02:00
246cfc3935 give the selection cards their own parts instead of label blobs
The cards were assembled from plain labels carrying whole blocks of text.
Replace those with the widget vocabulary the requirements describe, so a part
means the same thing wherever it appears and a card is a list of parts rather
than a string builder.

The parts, all free of Simulation and GameConfig -- they take prepared values,
and the contents work out what those are:
- StatRow, BarRow, SectionBox: label/value line, captioned fill bar, captioned
  group. The bar is one part for three things: construction progress,
  production progress, and HP.
- ItemChip / ItemChipRow: buffered items as icon, count and sub-line
  (REQ-UI-SINGLE-SELECTION). An input chip carries its per-cycle amount, an
  output chip its count against the buffer capacity. The chips are rebuilt only
  when the set of items changes, so a 30 Hz refresh moves numbers rather than
  widgets.
- RecipeSummaryRow: inputs, arrow, outputs, cycle time (REQ-UI-RECIPE-SUMMARY),
  which is now the panel's only display of the cycle time.
- CountRow, StatusPill, EmptyNote.

Behaviour that changed with them:
- The station card shows damage, range and fire rate as the requirement asks
  (REQ-UI-STATION-STATS-PANEL) rather than the combined DPS it showed before.
- A ship's behaviour moves from a stats row into the card header
  (REQ-UI-SHIP-BEHAVIOR). ShipStatsPanel keeps setBehavior for the balancing
  tool's inspect window, which has no header to put it in.
- A construction site's card shows a progress bar and the "no buffers until
  built" note (REQ-UI-SELECTION-CARD), and now also its recipe summary, since
  that is configuration and a site carries it (REQ-BLD-SITE-CONFIG). Costing a
  shipyard site's schematic needed computeShipyardRequiredMaterials to take a
  stored configuration as well as a live building -- one overload, so the
  module sum still exists once.

ShipStatsPanel is rebuilt on StatRow, BarRow and SectionBox, so the selection
card, the layout dialog's design preview and the balancing tool read alike.
Those three parts are compiled into the balancing target, which does not link
the ui library; keeping them sim-free is what makes that possible, and the
build enforces it.

Build clean, 541 tests pass, app and balancing tool both run with no Qt
warnings. Visual check pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-07 12:37:11 +02:00
89e984ec76 split the selection panel into one card per kind of selection
The panel rendered every selection out of a single pool of member widgets,
hidden and shown per branch, so each build path had to remember to hide the
other branches' widgets. That coupling produced the two defects fixed in
668ce0f, and the per-type detail the requirements now ask for would only add
more of it.

The pool is gone. SelectionPanel keeps the category arbitration, the float and
the hide-when-empty behaviour, and hosts exactly one SelectionContent at a
time; SelectionContentFactory picks which one from the selection alone. Each
card is one row of the catalog in REQ-UI-SELECTION-CONTENT and owns only its
own widgets.

The card structure (REQ-UI-SELECTION-CARD) lives in the base class: a header
with an identity symbol, a name and one right slot, then a configuration group
and a runtime group. A construction site keeps its configuration and has its
whole runtime group replaced by the construction progress, decided once there
rather than in every card (REQ-BLD-SITE-CONFIG).

Also implemented here:
- REQ-UI-SELECTION-STATUS: the header status dot, taken from the simulation's
  own getProductionStatus() so the panel and the world's status light cannot
  disagree.
- REQ-UI-SELECTION-AGGREGATE: belt-subsystem tiles and debris-only selections
  collapse into one card with a count instead of a count summary.
- REQ-UI-HQ-PANEL: the HQ shows the global block stock and its HP, neither of
  which is a buffer.
- BuildingIconCache, extracted from BuildButtonBar's file-local chip loading so
  the card headers and the build buttons rasterize the same SVGs once.

FieldSelectionPanel is deleted: ships, stations, debris and the field count
summary are four more cards in the same factory, so the two-panel arbitration
collapses into one decision.

The card parts are still today's labels and buttons; the item chips, bars,
recipe summary and stat rows follow.

Build clean, 541 tests pass, app runs with no Qt warnings. Visual check
pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-07 12:18:33 +02:00
668ce0fcb8 float the selection panel over the game world instead of a side column
Implements REQ-UI-SELECTION-PANEL and the full-width REQ-UI-WORLD-SIZE /
REQ-UI-HEADER: MainWindow drops the 25% side column and its 75/25 math, so
the header bar and world view span the window, and the panel joins the
build button bar as a widget floating over the world.

The panel follows the bar's pattern -- an opaque sibling built after the
world view, so it sits above the vignettes, below the dim overlay, and
swallows the mouse events that would otherwise reach the world. It sizes
itself to its content within a band the window hands it (the world view
less the bar's strip, so the bar never has to move for it), right-aligned
and centered in that band, and scrolls once the content outgrows it. Its
content moved into a scroll area for that; the width is capped at 320 px
because the wrapped labels and the splitter filter lists have no natural
width of their own. With nothing selected the panel now hides entirely
rather than showing an empty box (REQ-UI-EMPTY-SELECTION).

Two defects that content sizing exposed: buildEmpty() left the selected
ids behind when the building vanished under the panel, which would have
held an empty panel on screen, and buildMulti() let a shipyard's layout
preview survive from a previous single selection (REQ-UI-MULTI-SELECTION).

Renames SelectedBuildingPanel to SelectionPanel throughout, matching the
requirements: the panel has long shown ships, stations, and debris too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-06 21:58:36 +02:00
3b37b0ecf8 target single-building transfers by hovering, not by footprint coincidence 2026-08-06 20:31:51 +02:00
08d8b0dd90 re-cover copy-settings through single-building blueprints 2026-08-06 19:44:55 +02:00
cd31af2611 remove the Shift copy-building-settings gesture 2026-08-06 19:09:11 +02:00
fd6a7c5815 rekey the temporary blueprint to C, add V to re-place it 2026-08-06 19:07:13 +02:00
18cfe238f6 move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs 2026-08-06 19:03:59 +02:00
c1af58d80c float the build buttons as a horizontal bar over the game world and show key bindings inside build buttons 2026-08-06 08:28:40 +02:00
f39fe9a118 move the debug stats panel out of the renderer 2026-08-05 22:14:55 +02:00
e5dcb9de5f extract WorldRenderer 2026-08-05 22:14:55 +02:00
dc83add5c6 move two placement queries out of the view into PlacementRules 2026-08-05 22:14:54 +02:00
3a1951559d share the world shapes both views (game and balancing) draw identically 2026-08-05 22:14:54 +02:00
26d7448492 extract BuildModeController, fixing a silent blueprint exit 2026-08-05 22:14:54 +02:00
202f583067 extract SelectionController 2026-08-05 22:14:54 +02:00
d5ab44b9bf move the remaining hotkeys into the InputMapper 2026-08-05 22:14:53 +02:00
bb1ffab8fc fix a bug where view continues to pan when window lost focus while panning 2026-08-05 22:14:53 +02:00
e1445fe508 move the already-event-driven hotkeys into the InputMapper 2026-08-05 22:14:53 +02:00
caa810f66d move pan input into an InputMapper 2026-08-05 22:14:53 +02:00
f6df95abb2 extract the scroll position into WorldCamera 2026-08-05 22:14:52 +02:00
fa9dbd62ad use WorldCoordinates in ArenaView too 2026-08-05 22:14:52 +02:00
2af09d9eb1 extract the world<->widget transform into WorldCoordinates 2026-08-05 22:14:52 +02:00
4c166bf47f depend on the registry instead of DebrisSystem in the AI path 2026-08-05 07:25:59 +02:00
114a43b205 make BuildingSystem stateless: FactoryState becomes a parameter 2026-08-05 06:50:11 +02:00
d87d063b10 move the placement rules and the config-dependent queries off BuildingSystem 2026-08-05 06:49:49 +02:00
9c3be0fbd0 extract the production rules as free functions over config and building 2026-08-05 06:49:15 +02:00
58b94223f7 migrate every factory query off BuildingSystem onto the free functions 2026-08-05 06:46:13 +02:00
3990351a16 share BuildingSystem's free functions instead of copying them 2026-08-04 18:24:38 +02:00
bd344e4fbe add FieldSelectionPanel for extracting the ships/stations/debris selection 2026-08-04 18:23:28 +02:00
906000b0e9 drop the dead payloads from the state-change events 2026-08-03 22:02:47 +02:00
d9ef6aa728 make HeaderBar read the tick, artifacts and boss wave from the simulation instead of event 2026-08-03 22:02:19 +02:00
b44a85685e make BlueprintPanel read the block stock from the simulation instead of event 2026-08-03 22:02:05 +02:00
edd1c31785 remove duplicate findModuleDef from ShipLayoutPreview 2026-08-03 22:00:25 +02:00
7017f8b4dc route the win-path restart through ResetCommand 2026-08-03 21:13:43 +02:00
d5ba72d554 add ModalPauseScope for the pause-around-modal idiom 2026-08-03 21:11:52 +02:00
83177729e9 extract MainWindow::reloadConfig 2026-08-03 21:11:02 +02:00
e02e323cb2 share a single ItemIconCache across the UI 2026-08-03 21:08:19 +02:00
594c3b93c5 make HeaderBar read block stock and expansion cost from the simulation 2026-08-03 21:04:43 +02:00
932b57720c dedupe tunnel lookup and key tunnel tiles by QPoint 2026-08-03 21:02:06 +02:00
553a7e0701 add findShipDef/findModuleDef/findRecipeDef to config structs and re-use them in the rest of the code base 2026-08-03 20:57:31 +02:00