Compare commits
13 Commits
a5c51f08f8
...
ui_changes
| Author | SHA1 | Date | |
|---|---|---|---|
| aeab16757f | |||
| b97347329f | |||
| 352beda47c | |||
| 256c4b5492 | |||
| 997cbc65d3 | |||
| e66eb7a81f | |||
| 37899ea964 | |||
| a6152b9998 | |||
| 7d0f3e6daf | |||
| 44af3184d3 | |||
| 6fa3ba7f0c | |||
| f06d79e60d | |||
| 97c269576e |
@@ -124,13 +124,31 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
Three product targets plus tests:
|
Three product targets plus tests:
|
||||||
|
|
||||||
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
||||||
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar. Depends on `lib` and on Qt's OpenGL widgets module.
|
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar, controls panel. Depends on `lib` and on Qt's OpenGL widgets module.
|
||||||
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
||||||
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
||||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||||
|
|
||||||
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
||||||
|
|
||||||
|
## Player Input
|
||||||
|
|
||||||
|
Every player control is declared once, in `lib/core/ControlAction.h`, and read by three consumers that must never disagree about it:
|
||||||
|
|
||||||
|
* **`ControlsPanel`** asks which actions apply and draws a row per action (REQ-UI-CONTROLS-CONTENT).
|
||||||
|
* **`InputMapper`** resolves a key press to an action and fires the event that action stands for.
|
||||||
|
* **`GameWorldView`** resolves a mouse gesture to an action and runs the branch that carries it out.
|
||||||
|
|
||||||
|
The file declares; it never performs. It holds no simulation access, fires no events, and names nothing — display strings live in `ui/ControlActionText.h`, which renders each badge from the binding the resolver actually matches, so a chip cannot claim a key that does nothing. What an action *does* stays in the widget that always did it: the drag state machines, hit-testing, and command enqueuing were not moved.
|
||||||
|
|
||||||
|
Three invariants are easy to break here:
|
||||||
|
|
||||||
|
* **Do not add a shortcut straight to `InputMapper`'s switch or `mousePressEvent`'s branches.** Add the action and its binding to the table; the handler switches on the resolved action. A binding added directly is invisible to the panel, which is the drift the table exists to prevent. (Build hotkeys and `F3`/`F4` are deliberate exceptions, documented in the header and in REQ-UI-CONTROLS-ACCURACY.)
|
||||||
|
* **Availability and display are different questions.** An action can be live in a context the panel does not advertise it in — `Ctrl`+click with an empty selection is the standing example. `isControlActionAvailable` answers the first, the per-context row lists answer the second, and `ControlActionTest` asserts the pairing that matters: every row's bindings resolve back to that row's action.
|
||||||
|
* **Gesture state is shared, not owned by an action.** Whether a belt drag is in progress decides what the right mouse button means, so it lives on `BuildModeController` where the resolver can see it — as does the hovered-transfer flag. `m_boxSelecting` is likewise one gesture serving two actions (box select and deconstruct area).
|
||||||
|
|
||||||
|
`ControlContext` is a plain snapshot rather than references to the live controllers, which is what keeps the rules testable without a world and stops an action reaching into the simulation: if a rule needs a fact, the fact is named in the struct and the caller supplies it. When bindings become player-configurable, only the binding tables in `ControlAction.cpp` turn from hard-coded data into loaded data.
|
||||||
|
|
||||||
## Belt Subsystem
|
## Belt Subsystem
|
||||||
|
|
||||||
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
The screen is a single column: a header bar across the top and the game world view filling the whole area below it. There is no side panel. All three permanent UI widgets float over the game world — the build button bar (REQ-UI-BUILD-BAR) at its bottom center, the selection panel (REQ-UI-SELECTION-PANEL) at its right edge, centered vertically within the height left above the build button bar and shown only while something is selected, and the controls panel (REQ-UI-CONTROLS-PANEL) in its bottom-left corner, beside the build button bar and rising above it only when the two would overlap. Blueprints have no permanent screen real estate; they are reached through modal dialogs (REQ-UI-BLUEPRINT-DIALOG):
|
The screen is a single column: a header bar across the top and the game world view filling the whole area below it. There is no side panel. All three permanent UI widgets float over the game world — the build button bar (REQ-UI-BUILD-BAR) at its bottom center, the selection panel (REQ-UI-SELECTION-PANEL) beside whatever is currently selected, shown only while something is selected and holding its place on the screen until the next selection, and the controls panel (REQ-UI-CONTROLS-PANEL) in its bottom-left corner, beside the build button bar and rising above it only when the two would overlap. Blueprints have no permanent screen real estate; they are reached through modal dialogs (REQ-UI-BLUEPRINT-DIALOG):
|
||||||
|
|
||||||
```
|
```
|
||||||
+-----------------------------------------------------------+
|
+-----------------------------------------------------------+
|
||||||
@@ -456,11 +456,14 @@ The screen is a single column: a header bar across the top and the game world vi
|
|||||||
- REQ-UI-DECONSTRUCT-BORDER: While deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), a vignette border is drawn around the edges of the game world view to signal the mode, matching the geometry of the paused-state vignette (REQ-UI-PAUSE-BORDER): a 100-pixel thickness (capped at half the smaller viewport dimension on very small views) with the four sides meeting along mitred corner diagonals. It fades in the alpha channel from fully transparent at its inner (center-facing) edge to the deconstruct tint color at the viewport edge. The color — including its alpha, which sets the peak opacity at the viewport edge — is read from `visuals.toml [overlays].deconstruct_tint`, the same deconstruct-mode color used for the hover tint. The border is presentation-only and has no effect on the simulation. If the game is both paused and in deconstruct mode, both vignettes are drawn and compose over each other.
|
- REQ-UI-DECONSTRUCT-BORDER: While deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), a vignette border is drawn around the edges of the game world view to signal the mode, matching the geometry of the paused-state vignette (REQ-UI-PAUSE-BORDER): a 100-pixel thickness (capped at half the smaller viewport dimension on very small views) with the four sides meeting along mitred corner diagonals. It fades in the alpha channel from fully transparent at its inner (center-facing) edge to the deconstruct tint color at the viewport edge. The color — including its alpha, which sets the peak opacity at the viewport edge — is read from `visuals.toml [overlays].deconstruct_tint`, the same deconstruct-mode color used for the hover tint. The border is presentation-only and has no effect on the simulation. If the game is both paused and in deconstruct mode, both vignettes are drawn and compose over each other.
|
||||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x>` followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). When no icon file exists for `building_block`, the caption falls back to the `Expand: <x> Blocks` text. Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x>` followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). When no icon file exists for `building_block`, the caption falls back to the `Expand: <x> Blocks` text. Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||||
- REQ-UI-WORLD-SIZE: The game world view occupies the full width of the game window and the full height below the header bar. No widget insets it: the build button bar (REQ-UI-BUILD-BAR), the selection panel (REQ-UI-SELECTION-PANEL), and the controls panel (REQ-UI-CONTROLS-PANEL) float over it.
|
- REQ-UI-WORLD-SIZE: The game world view occupies the full width of the game window and the full height below the header bar. No widget insets it: the build button bar (REQ-UI-BUILD-BAR), the selection panel (REQ-UI-SELECTION-PANEL), and the controls panel (REQ-UI-CONTROLS-PANEL) float over it.
|
||||||
- REQ-UI-SELECTION-PANEL: The **selection panel** (the panel described under Selection Panel, REQ-UI-SINGLE-SELECTION and following) is a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), anchored to the view's right edge with a small margin. It is **sized to its content in both width and height**, so it grows and shrinks as the selection changes, staying right-anchored as its size changes.
|
- REQ-UI-SELECTION-PANEL: The **selection panel** (the panel described under Selection Panel, REQ-UI-SINGLE-SELECTION and following) is a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), placed **beside the objects it describes** rather than at a fixed corner of the view, so it appears where the player is already looking. It is **sized to its content in both width and height**, so it grows and shrinks as the content changes. It keeps the same small margin from the view's edges that it uses as its gap from the selection.
|
||||||
|
- **Anchor rectangle.** The panel is placed against the screen rectangle of the selection **at the moment that selection started**: the footprint of the single object selected (a building or construction site, an actor, or a piece of debris), or, when the selection started as a multi-selection (REQ-UI-MULTI-SELECT), the bounding box of all the objects it started with.
|
||||||
The panel's vertical placement and maximum height are defined against an **available band**: the height of the game world view, less the panel's top and bottom margins, less the height of the horizontal strip occupied by the build button bar at the bottom of the view (the bar's height plus its bottom margin, REQ-UI-BUILD-BAR) — the bar's band is subtracted over the full view width, regardless of how wide the bar currently is. The panel is **vertically centered within that available band**, not within the full view height, so it sits slightly above the view's vertical center. Should its content ever be taller than the band, the panel's height is capped at the band height and the content scrolls vertically within it.
|
- **Side.** The panel goes to the **right** of the anchor rectangle, separated from it by the panel's margin, whenever it fits within the view there. Otherwise it goes to the **left** of the anchor rectangle by that same margin. When it fits on neither side — a bounding box spanning most of the view, or an object too close to an edge — it is placed on whichever side leaves more room and then pushed inside the view. That is the one case in which the panel covers part of the selection.
|
||||||
|
- **Vertical placement.** The panel's **top edge is aligned with the anchor rectangle's top edge** and it extends downward. Its bottom is limited by the lowest of: the view's bottom edge less the panel's margin; and the top edge, less that margin, of the build button bar (REQ-UI-BUILD-BAR) or the controls panel (REQ-UI-CONTROLS-PANEL) — but each of those two only where the panel's own horizontal extent actually overlaps that widget's current rectangle, so a panel whose column misses them is not shortened by them. Should the panel not fit above that limit, it is shifted up, as far as the view's top margin and no further; if it still does not fit, its height is capped at the space available there and the content scrolls vertically within it.
|
||||||
|
- **Fixed for the life of the selection.** The anchor rectangle and the side are determined once, when the selection starts, and are not revisited while that selection lasts; the panel's own size is the only thing that may still move it (see **Resizing in place** below). The panel **keeps its place on the screen** when the player scrolls the view (REQ-UI-SCROLL) and when a selected object moves under it (a selected ship flying away), rather than following the object — which may leave it beside nothing, or beside an object that has left the view entirely. It likewise does not move when the selection is **expanded** by adding objects or reduced by removing them (REQ-UI-MULTI-SELECT), nor when a selected object is destroyed or deconstructed. Starting a **new** selection — clicking a different object, or a box drag that replaces the selection — places the panel anew against the new anchor rectangle.
|
||||||
|
- **Resizing in place.** Only the anchor rectangle and the chosen side are fixed for the life of the selection; the panel's geometry is **re-solved from them** whenever its content size changes (a section appearing or disappearing as the selection's state changes), the view is resized, or the build button bar's or controls panel's rectangle changes. Re-solving keeps the two edges the panel was placed by — its top edge, and the edge facing the anchor rectangle (its left edge when it sits to the right of the selection, its right edge when it sits to the left) — so the panel grows away from the selection rather than over it, and it never switches sides for as long as the selection lasts. What re-solving may change is the vertical result: growth that would take the panel outside the view or into either of those two widgets is resolved as in **Vertical placement** above, by shifting it up and capping its height, and a panel that shrinks again regains the room.
|
||||||
- **Visibility.** The panel is shown only while at least one object is selected. With an empty selection it is not shown at all (REQ-UI-EMPTY-SELECTION), leaving the full game world view visible.
|
- **Visibility.** The panel is shown only while at least one object is selected. With an empty selection it is not shown at all (REQ-UI-EMPTY-SELECTION), leaving the full game world view visible.
|
||||||
- **Overlay behavior.** As for the build button bar (REQ-UI-BUILD-BAR): the panel occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected. It is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full right-hand band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the panel. The panel and the build button bar never overlap, because the panel's available band excludes the bar's strip; the bar itself never moves on the panel's account (REQ-UI-BUILD-BAR).
|
- **Overlay behavior.** As for the build button bar (REQ-UI-BUILD-BAR): the panel occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected. It is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the panel. The panel never overlaps the build button bar or the controls panel, because it stays above both wherever their rectangles meet its own; neither of them ever moves on the panel's account (REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL).
|
||||||
- **Input.** Mouse events over the panel are consumed by the panel and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the panel does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
- **Input.** Mouse events over the panel are consumed by the panel and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the panel does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
||||||
- REQ-UI-MODAL-DIM: While a modal dialog, menu, or full-screen state screen is open on top of the game, a transparent black overlay (a dim/scrim) is drawn over the **entire game window** — the header bar, the game world view, and the widgets floating over it (the build button bar, REQ-UI-BUILD-BAR, the selection panel, REQ-UI-SELECTION-PANEL, and the controls panel, REQ-UI-CONTROLS-PANEL) — behind that modal, so the game reads as inactive while the modal holds focus. The overlay is shown for every modal that auto-pauses the simulation — the escape menu (REQ-UI-GAME-MENU), the recipe/schematic selection dialog (REQ-UI-SELECT-BUTTON), the layout configuration dialog (REQ-MOD-UI-DIALOG), the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP), the blueprint save dialog (REQ-UI-BLUEPRINT-CREATE), and the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG) — as well as the game-over screen (REQ-HQ-GAME-OVER) and the win screen (REQ-WIN-SCREEN), which end rather than pause the game. When modals are nested (for example the Create Blueprint name dialog (REQ-MOD-UI-BLUEPRINT-CREATE) opened from the layout configuration dialog), only a single dim is shown over the game window; nested modals do not stack additional overlays. The same applies when one modal hands directly off to another — the blueprint save dialog opening the blueprint selection dialog on confirm (REQ-UI-BLUEPRINT-CREATE): the dim persists across the handoff rather than flickering off and back on, and the simulation is not resumed in between. The dim color and opacity are read from `visuals.toml [overlays]` (a semi-transparent black modal-dim color), consistent with the other overlay colors. The overlay is presentation-only and has no effect on the simulation.
|
- REQ-UI-MODAL-DIM: While a modal dialog, menu, or full-screen state screen is open on top of the game, a transparent black overlay (a dim/scrim) is drawn over the **entire game window** — the header bar, the game world view, and the widgets floating over it (the build button bar, REQ-UI-BUILD-BAR, the selection panel, REQ-UI-SELECTION-PANEL, and the controls panel, REQ-UI-CONTROLS-PANEL) — behind that modal, so the game reads as inactive while the modal holds focus. The overlay is shown for every modal that auto-pauses the simulation — the escape menu (REQ-UI-GAME-MENU), the recipe/schematic selection dialog (REQ-UI-SELECT-BUTTON), the layout configuration dialog (REQ-MOD-UI-DIALOG), the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP), the blueprint save dialog (REQ-UI-BLUEPRINT-CREATE), and the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG) — as well as the game-over screen (REQ-HQ-GAME-OVER) and the win screen (REQ-WIN-SCREEN), which end rather than pause the game. When modals are nested (for example the Create Blueprint name dialog (REQ-MOD-UI-BLUEPRINT-CREATE) opened from the layout configuration dialog), only a single dim is shown over the game window; nested modals do not stack additional overlays. The same applies when one modal hands directly off to another — the blueprint save dialog opening the blueprint selection dialog on confirm (REQ-UI-BLUEPRINT-CREATE): the dim persists across the handoff rather than flickering off and back on, and the simulation is not resumed in between. The dim color and opacity are read from `visuals.toml [overlays]` (a semi-transparent black modal-dim color), consistent with the other overlay colors. The overlay is presentation-only and has no effect on the simulation.
|
||||||
|
|
||||||
@@ -537,7 +540,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
- REQ-UI-SELECTION-CARD: **Card structure.** Every panel content is a card with the same three parts, top to bottom:
|
- REQ-UI-SELECTION-CARD: **Card structure.** Every panel content is a card with the same three parts, top to bottom:
|
||||||
- **Header** — always shown. It holds the selection's identity symbol on the left — the building's icon glyph (REQ-UI-WORLD-ICON), a ship's schematic color swatch, or the kind symbol of a defence station or piece of debris — the selection's name beside it, and one optional **right slot**. The right slot holds a status indicator (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an object count — never more than one of them; which one applies is stated per content in REQ-UI-SELECTION-CONTENT.
|
- **Header** — always shown. It holds the selection's identity symbol on the left — the building's icon glyph (REQ-UI-WORLD-ICON), a ship's schematic color swatch, or the kind symbol of a defence station or piece of debris — the selection's name beside it, and one optional **right slot**. The right slot holds a status indicator (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an object count — never more than one of them; which one applies is stated per content in REQ-UI-SELECTION-CONTENT.
|
||||||
- **Configuration group** — the controls that change how the selected object is set up: the recipe/schematic selection control (REQ-UI-SELECT-BUTTON), a shipyard's layout preview and Configure button (REQ-MOD-UI-PREVIEW), and a splitter's output filters (REQ-BLD-SPLITTER). It is shown identically for an operational building and for a construction site of the same type (REQ-BLD-SITE-CONFIG).
|
- **Configuration group** — the controls that change how the selected object is set up: the recipe/schematic selection control (REQ-UI-SELECT-BUTTON), a shipyard's layout preview and Configure button (REQ-MOD-UI-PREVIEW), and a splitter's output filters (REQ-BLD-SPLITTER). It is shown identically for an operational building and for a construction site of the same type (REQ-BLD-SITE-CONFIG).
|
||||||
- **Runtime group** — what the object is currently doing: buffer contents, production progress, HP, remaining scrap, and the belt clear action (REQ-UI-BELT-CLEAR). For a **construction site** the entire runtime group is replaced by a captioned `Construction` section: a progress bar filled to the site's construction completion with that completion as an integer percentage beside the caption — the same value the world draws on the footprint (REQ-UI-CONSTRUCTION-PROGRESS) — followed by a note that buffers appear once the building is built, because a site has neither buffers nor a production cycle (REQ-BLD-SITE-CONFIG). The configuration group is unaffected and stays visible on a site.
|
- **Runtime group** — what the object is currently doing: buffer contents, production progress, HP, remaining scrap, and the belt clear action (REQ-UI-BELT-CLEAR). Where the object has **HP**, its bar is the first thing in this group, above everything else the card shows (REQ-UI-HQ-PANEL, REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL) — how close the thing is to dying outranks what it is holding. For a **construction site** the entire runtime group is replaced by a captioned `Construction` section: a progress bar filled to the site's construction completion with that completion as an integer percentage beside the caption — the same value the world draws on the footprint (REQ-UI-CONSTRUCTION-PROGRESS) — followed by a note that buffers appear once the building is built, because a site has neither buffers nor a production cycle (REQ-BLD-SITE-CONFIG). That section sits **directly below the header, above the configuration group**, so how far along the site is reads first; the configuration group is otherwise unaffected and stays visible on a site.
|
||||||
|
|
||||||
A group with nothing to show takes no space, so a content may consist of a header alone. Within a group, related parts form **sections** carrying a short caption above them (e.g. `Layout`, `Input buffers`, `Production`, `Output buffer`); a section and its caption are shown only while that section has content, so e.g. a Miner (which consumes nothing) shows no input buffer section.
|
A group with nothing to show takes no space, so a content may consist of a header alone. Within a group, related parts form **sections** carrying a short caption above them (e.g. `Layout`, `Input buffers`, `Production`, `Output buffer`); a section and its caption are shown only while that section has content, so e.g. a Miner (which consumes nothing) shows no input buffer section.
|
||||||
- REQ-UI-SELECTION-CONTENT: **Content catalog.** Which content the panel shows follows from the selection alone:
|
- REQ-UI-SELECTION-CONTENT: **Content catalog.** Which content the panel shows follows from the selection alone:
|
||||||
@@ -568,9 +571,13 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
|
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
|
||||||
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
|
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
|
||||||
|
|
||||||
Input and output chips form separately captioned sections, each shown only while it holds something (REQ-UI-SELECTION-CARD). For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
|
Input and output chips form separately captioned sections (REQ-UI-SELECTION-CARD). A section lists a chip for **every item the building's cycle involves**, and for an auto-recipe building every item it handles at all, whether or not the buffer currently holds any: an empty buffer reads `0` rather than its chip disappearing, so the card keeps one shape while the building runs. A section left with no chips at all is not shown. The production section (REQ-UI-PRODUCTION-PROGRESS) sits **between them**, so the card reads in the direction the materials flow: what goes in, what is being made of it, what has come out. For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
|
||||||
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production, and none while idle. A building with no recipe or schematic selected shows no summary.
|
|
||||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section**: a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
|
**Only unlocked items are listed.** A building's buffers may carry entries for items the player cannot make yet — an auto-recipe building's buffers are sized over *every* recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), including recipes that are still locked. Those entries are left out of both sections, consistent with the rest of the UI hiding what is not unlocked yet (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SPLITTER), so a Smelter shows the ores it can actually smelt rather than every ore in the game.
|
||||||
|
|
||||||
|
**An idle auto-recipe building still shows what it handles.** Having no selected recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), it would otherwise show empty sections whenever it happens to be between cycles. Its input and output sections instead list the unlocked items of every recipe of its type — the same union its buffers were sized over — with a count and no per-cycle denominator, since no one recipe is in force. While a cycle is running, that cycle's recipe supplies the denominators as for any other building.
|
||||||
|
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production and, while between cycles, of the one they ran last. They keep it rather than dropping it, because a summary that came and went with each cycle would resize the card in step with the building's status (REQ-UI-SELECTION-STATUS), which is the one thing the panel must not do while the player is reading it (REQ-UI-SELECTION-PANEL). Such a building shows no summary only until it has run its first cycle. A building with no recipe or schematic selected shows no summary.
|
||||||
|
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
|
||||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
||||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
|
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||||
@@ -580,7 +587,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
||||||
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon per REQ-UI-ITEM-ICON. When the item has no icon file, the button falls back to the recipe/item name caption. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions.
|
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon per REQ-UI-ITEM-ICON. When the item has no icon file, the button falls back to the recipe/item name caption. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions.
|
||||||
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a **"Clear stuck items"** button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why a selection of belts and tunnel ends aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE).
|
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a **"Clear stuck items"** button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why a selection of belts and tunnel ends aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE).
|
||||||
- REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon — and the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site.
|
- REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS) and, beneath it, the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon. The HP comes first, as it does on every card that has it (REQ-UI-SELECTION-CARD). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site.
|
||||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
|
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
|
||||||
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selection panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. Its header (REQ-UI-SELECTION-CARD) carries the schematic's color swatch and display name, with the ship's current behavior in the right slot (REQ-UI-SHIP-BEHAVIOR). The panel always shows all hull stats: HP (current / maximum) as a **bar** with the two values beside its caption, then max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed as label/value rows. In addition, capability module summaries are shown below the hull stats, each as its own outlined row, conditioned on which module types are installed and using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selection panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. Its header (REQ-UI-SELECTION-CARD) carries the schematic's color swatch and display name, with the ship's current behavior in the right slot (REQ-UI-SHIP-BEHAVIOR). The panel always shows all hull stats: HP (current / maximum) as a **bar** with the two values beside its caption, then max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed as label/value rows. In addition, capability module summaries are shown below the hull stats, each as its own outlined row, conditioned on which module types are installed and using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||||
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** in its header's right slot (REQ-UI-SELECTION-CARD) — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
|
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** in its header's right slot (REQ-UI-SELECTION-CARD) — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
|
||||||
@@ -601,7 +608,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
|
|
||||||
- REQ-UI-BUILD-BAR: All placeable building types are shown as a **single horizontal row** of buttons with no grouping and no wrapping, inside a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), horizontally centered and anchored at the bottom edge with a small margin. Tunnel Entry and Tunnel Exit share a single **Tunnel** button (REQ-BLD-TUNNEL-MODE) rather than one button each. The bar is sized to its buttons and re-centers whenever the set of shown buttons changes (REQ-LOCK-BUILDING) or the view is resized.
|
- REQ-UI-BUILD-BAR: All placeable building types are shown as a **single horizontal row** of buttons with no grouping and no wrapping, inside a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), horizontally centered and anchored at the bottom edge with a small margin. Tunnel Entry and Tunnel Exit share a single **Tunnel** button (REQ-BLD-TUNNEL-MODE) rather than one button each. The bar is sized to its buttons and re-centers whenever the set of shown buttons changes (REQ-LOCK-BUILDING) or the view is resized.
|
||||||
- **Overlay behavior.** The bar occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected (the world is not inset for the bar). The bar is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full 100-pixel bottom band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the bar.
|
- **Overlay behavior.** The bar occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected (the world is not inset for the bar). The bar is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full 100-pixel bottom band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the bar.
|
||||||
- **No overlap with the selection panel.** The bar and the selection panel (REQ-UI-SELECTION-PANEL) never overlap, and keeping them apart is entirely the panel's job: the bar's position depends only on its own button set and the view size, and it never moves, re-centers, or resizes because the panel appears, disappears, or changes size. The panel instead confines itself to the view height less the bar's strip (REQ-UI-SELECTION-PANEL).
|
- **No overlap with the selection panel.** The bar and the selection panel (REQ-UI-SELECTION-PANEL) never overlap, and keeping them apart is entirely the panel's job: the bar's position depends only on its own button set and the view size, and it never moves, re-centers, or resizes because the panel appears, disappears, or changes size. The panel instead steps around the bar's current rectangle wherever its own column would meet it (REQ-UI-SELECTION-PANEL).
|
||||||
- **Input.** Mouse events over the bar are consumed by the bar and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the bar does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
- **Input.** Mouse events over the bar are consumed by the bar and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the bar does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
||||||
- REQ-UI-BUILD-COST: Each button is **icon-only with a cost**, its face composed of three elements: the button's **hotkey badge** in the top-left corner, the building's icon (REQ-UI-BUILD-ICON) centered below it, and the building block cost centered under the icon, shown with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) to the right of the number in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its name as a text caption in the cost's place.
|
- REQ-UI-BUILD-COST: Each button is **icon-only with a cost**, its face composed of three elements: the button's **hotkey badge** in the top-left corner, the building's icon (REQ-UI-BUILD-ICON) centered below it, and the building block cost centered under the icon, shown with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) to the right of the number in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its name as a text caption in the cost's place.
|
||||||
- **Hotkey badge.** The badge names the build hotkey that activates the button (REQ-UI-HOTKEYS), so the player can learn the shortcuts from the bar itself. It is rendered dimmer than the cost so it reads as secondary, but at the same size and in bold, because a smaller badge is not legible. A plain-digit hotkey is shown as the bare digit (`1`, `2`, `3`); a Shift+digit hotkey is shown with an upwards arrow prefixed and no separator (`↑1` … `↑6`); the Deconstruct button shows `Q`. A button whose building type has no build hotkey shows no badge and keeps the same face size, so the row stays even.
|
- **Hotkey badge.** The badge names the build hotkey that activates the button (REQ-UI-HOTKEYS), so the player can learn the shortcuts from the bar itself. It is rendered dimmer than the cost so it reads as secondary, but at the same size and in bold, because a smaller badge is not legible. A plain-digit hotkey is shown as the bare digit (`1`, `2`, `3`); a Shift+digit hotkey is shown with an upwards arrow prefixed and no separator (`↑1` … `↑6`); the Deconstruct button shows `Q`. A button whose building type has no build hotkey shows no badge and keeps the same face size, so the row stays even.
|
||||||
@@ -614,8 +621,8 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
|
|
||||||
The controls panel tells the player which controls are available right now. It is context-sensitive: the game is always in exactly one **control context**, derived from the active build mode and the current selection, and the panel shows that context's rows and no others. Its position, size, and overlay behavior are defined in REQ-UI-CONTROLS-PANEL; its structure in REQ-UI-CONTROLS-CARD; and which rows each context shows in REQ-UI-CONTROLS-CONTENT. The panel never defines a binding: every row restates one already defined in REQ-UI-HOTKEYS or in the mouse gestures cited beside it.
|
The controls panel tells the player which controls are available right now. It is context-sensitive: the game is always in exactly one **control context**, derived from the active build mode and the current selection, and the panel shows that context's rows and no others. Its position, size, and overlay behavior are defined in REQ-UI-CONTROLS-PANEL; its structure in REQ-UI-CONTROLS-CARD; and which rows each context shows in REQ-UI-CONTROLS-CONTENT. The panel never defines a binding: every row restates one already defined in REQ-UI-HOTKEYS or in the mouse gestures cited beside it.
|
||||||
|
|
||||||
- REQ-UI-CONTROLS-PANEL: The **controls panel** is a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), anchored to the view's **bottom-left corner** with a small margin on both edges. It is **sized to its content in both width and height**, growing and shrinking upward from that corner as the context changes. Should its content ever be taller than the view, the panel's height is capped at the view height less its margins and the content scrolls vertically within it. The panel and the selection panel are on opposite sides of the view and never overlap.
|
- REQ-UI-CONTROLS-PANEL: The **controls panel** is a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), anchored to the view's **bottom-left corner** with a small margin on both edges. It is **sized to its content in both width and height**, growing and shrinking upward from that corner as the context changes. Should its content ever be taller than the view, the panel's height is capped at the view height less its margins and the content scrolls vertically within it. The selection panel is placed beside the current selection (REQ-UI-SELECTION-PANEL) and so can reach this corner; the two never overlap, and keeping them apart is entirely the selection panel's job — this panel's position depends only on its own content, the view size, and the build button bar, and it never moves or resizes because the selection panel appears, disappears, or changes size.
|
||||||
- **Stepping around the build button bar.** Unlike the selection panel (REQ-UI-SELECTION-PANEL), the panel does **not** confine itself to the band above the build button bar's strip: the bar is horizontally centered and sized to its buttons (REQ-UI-BUILD-BAR), so it normally leaves this corner free, and the panel shares the view's bottom edge with it. The panel **rises only to avoid an actual overlap**: whenever the panel at the bottom-left corner would intersect the bar's current rectangle, it is moved up so that its bottom edge clears the bar's top by the same margin it keeps from the view's edges, and its height is capped at the space that leaves. Whether it rises therefore depends on how wide the bar and the panel currently are, and it returns to the corner as soon as they no longer meet. The bar never moves on the panel's account (REQ-UI-BUILD-BAR).
|
- **Stepping around the build button bar.** Like the selection panel (REQ-UI-SELECTION-PANEL), the panel does **not** confine itself to the band above the build button bar's strip: the bar is horizontally centered and sized to its buttons (REQ-UI-BUILD-BAR), so it normally leaves this corner free, and the panel shares the view's bottom edge with it. The panel **rises only to avoid an actual overlap**: whenever the panel at the bottom-left corner would intersect the bar's current rectangle, it is moved up so that its bottom edge clears the bar's top by the same margin it keeps from the view's edges, and its height is capped at the space that leaves. Whether it rises therefore depends on how wide the bar and the panel currently are, and it returns to the corner as soon as they no longer meet. The bar never moves on the panel's account (REQ-UI-BUILD-BAR).
|
||||||
- **Visibility.** The panel is shown whenever the game is being played. Unlike the selection panel it has no empty state (REQ-UI-EMPTY-SELECTION): every context has rows, so there is never nothing to show.
|
- **Visibility.** The panel is shown whenever the game is being played. Unlike the selection panel it has no empty state (REQ-UI-EMPTY-SELECTION): every context has rows, so there is never nothing to show.
|
||||||
- **Collapsing.** Clicking anywhere on the panel's header (REQ-UI-CONTROLS-CARD) toggles the panel between **expanded** and **collapsed**. Collapsed, it shows its header alone, keeping the context name visible and the header clickable so the panel can be expanded again; expanded, it shows the header followed by every row of the current context. The panel starts **expanded**. Changing context does not change the collapsed state: a panel collapsed in one context stays collapsed in the next, and its header updates in place. The collapsed state is presentation-only — it is not a player command, never enters the replay stream, and has no effect on the simulation. It persists for as long as the application runs, including across a restart from the escape menu (REQ-UI-GAME-MENU), and is not saved to disk.
|
- **Collapsing.** Clicking anywhere on the panel's header (REQ-UI-CONTROLS-CARD) toggles the panel between **expanded** and **collapsed**. Collapsed, it shows its header alone, keeping the context name visible and the header clickable so the panel can be expanded again; expanded, it shows the header followed by every row of the current context. The panel starts **expanded**. Changing context does not change the collapsed state: a panel collapsed in one context stays collapsed in the next, and its header updates in place. The collapsed state is presentation-only — it is not a player command, never enters the replay stream, and has no effect on the simulation. It persists for as long as the application runs, including across a restart from the escape menu (REQ-UI-GAME-MENU), and is not saved to disk.
|
||||||
- **Overlay behavior.** As for the build button bar and the selection panel (REQ-UI-BUILD-BAR, REQ-UI-SELECTION-PANEL): the panel occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected. It is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full left-hand band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the panel.
|
- **Overlay behavior.** As for the build button bar and the selection panel (REQ-UI-BUILD-BAR, REQ-UI-SELECTION-PANEL): the panel occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected. It is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full left-hand band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the panel.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||||
@@ -32,6 +33,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public:
|
|||||||
void forEach(Func&& f) const;
|
void forEach(Func&& f) const;
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
bool hasAll(entt::entity entity);
|
bool hasAll(entt::entity entity) const;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
T& get(entt::entity entity);
|
T& get(entt::entity entity);
|
||||||
@@ -101,7 +101,7 @@ void EntityAdmin::forEach(Func&& f) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
bool EntityAdmin::hasAll(entt::entity entity)
|
bool EntityAdmin::hasAll(entt::entity entity) const
|
||||||
{
|
{
|
||||||
return m_registry.all_of<Ts...>(entity);
|
return m_registry.all_of<Ts...>(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx)
|
||||||
|
{
|
||||||
|
int bottomPx = band.bottom();
|
||||||
|
for (const QRect& occupied : occupiedRects)
|
||||||
|
{
|
||||||
|
if (occupied.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Only what is actually in the way counts: a widget entirely to one side of this
|
||||||
|
// span is not below it, however tall it is.
|
||||||
|
if (occupied.right() < leftPx || occupied.left() > rightPx)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bottomPx = std::min(bottomPx, occupied.top() - marginPx - 1);
|
||||||
|
}
|
||||||
|
return bottomPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
// What each side offers: the gap between the anchor and that edge of the band, less
|
||||||
|
// the margin the panel keeps from the anchor.
|
||||||
|
const int roomRightPx = band.right() - anchorRect.right() - marginPx;
|
||||||
|
const int roomLeftPx = anchorRect.left() - band.left() - marginPx;
|
||||||
|
|
||||||
|
if (roomRightPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Right;
|
||||||
|
}
|
||||||
|
if (roomLeftPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Left;
|
||||||
|
}
|
||||||
|
// Neither side can hold it without covering the selection, so it goes where it
|
||||||
|
// covers the least of it.
|
||||||
|
return (roomRightPx >= roomLeftPx) ? PanelSide::Right : PanelSide::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
const int widthPx = std::min(wantedSize.width(), band.width());
|
||||||
|
|
||||||
|
// Against the anchor on the chosen side, growing away from it: the edge facing the
|
||||||
|
// selection is the one that stays put as the panel's content resizes.
|
||||||
|
int leftPx = (side == PanelSide::Right) ? anchorRect.right() + marginPx + 1
|
||||||
|
: anchorRect.left() - marginPx - widthPx;
|
||||||
|
// A panel that does not fit there is pushed back inside the view rather than hanging
|
||||||
|
// off it, which is what puts it over the selection when neither side had room.
|
||||||
|
leftPx = std::min(leftPx, band.right() - widthPx + 1);
|
||||||
|
leftPx = std::max(leftPx, band.left());
|
||||||
|
|
||||||
|
// Only the widgets its own column meets can shorten it.
|
||||||
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, leftPx,
|
||||||
|
leftPx + widthPx - 1, marginPx);
|
||||||
|
const int heightPx =
|
||||||
|
std::min(wantedSize.height(), std::max(0, bottomPx - band.top() + 1));
|
||||||
|
|
||||||
|
// Top-aligned with the anchor, then lifted by however much of it hangs below what is
|
||||||
|
// free. Never above the band: a panel taller than the space left is capped instead,
|
||||||
|
// and scrolls.
|
||||||
|
int topPx = std::max(anchorRect.top(), band.top());
|
||||||
|
topPx = std::min(topPx, bottomPx - heightPx + 1);
|
||||||
|
topPx = std::max(topPx, band.top());
|
||||||
|
|
||||||
|
return QRect(leftPx, topPx, widthPx, heightPx);
|
||||||
|
}
|
||||||
43
src/lib/core/FloatingPanelPlacement.h
Normal file
43
src/lib/core/FloatingPanelPlacement.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
// Geometry for the widgets floating over the game world view (REQ-UI-WORLD-SIZE). Their
|
||||||
|
// owner places them in one ordered pass, each into the space the earlier ones left free,
|
||||||
|
// and these are the rules they place themselves by. Pure geometry -- no widget is
|
||||||
|
// involved, which is what lets the rules be tested without a display.
|
||||||
|
|
||||||
|
// The lowest bottom edge available to a widget occupying the horizontal span
|
||||||
|
// [leftPx, rightPx] inside band: the band's own bottom, or marginPx above the topmost
|
||||||
|
// occupied rectangle whose horizontal extent meets that span. A rectangle beside the
|
||||||
|
// span is not in the way and does not shorten it (REQ-UI-SELECTION-PANEL,
|
||||||
|
// REQ-UI-CONTROLS-PANEL). The result is inclusive, as QRect::bottom() is.
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx);
|
||||||
|
|
||||||
|
// Which side of the selection the panel stands on (REQ-UI-SELECTION-PANEL).
|
||||||
|
enum class PanelSide
|
||||||
|
{
|
||||||
|
Right,
|
||||||
|
Left
|
||||||
|
};
|
||||||
|
|
||||||
|
// The side a panel widthPx wide takes beside anchorRect: the right of it where it fits
|
||||||
|
// within band, otherwise the left, and where it fits on neither, whichever side leaves
|
||||||
|
// more room -- the one case in which the panel ends up over the selection
|
||||||
|
// (REQ-UI-SELECTION-PANEL). Decided once when the selection starts and kept for as long
|
||||||
|
// as it lasts, so a card that grows later never flips the panel across the object.
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx);
|
||||||
|
|
||||||
|
// Where a panel of wantedSize stands beside anchorRect on the given side: separated from
|
||||||
|
// it by marginPx and growing away from it, its top edge on the anchor's top edge, pushed
|
||||||
|
// inside band and above whatever occupies it. The returned height is short of
|
||||||
|
// wantedSize's when there was not enough room, which is the caller's cue to scroll its
|
||||||
|
// content (REQ-UI-SELECTION-PANEL).
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx);
|
||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionAnchorChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
||||||
@@ -43,6 +44,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingLayoutInvalidatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Asks the owner of the widgets floating over the game world view to re-run its placement
|
||||||
|
// pass (see ui/FloatingPanel.h). Published by a floating widget whose content or
|
||||||
|
// visibility changed: what space that widget may take depends on the ones placed before
|
||||||
|
// it, so it cannot re-place itself alone (REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL,
|
||||||
|
// REQ-UI-SELECTION-PANEL). Carries no payload -- the pass re-reads every widget.
|
||||||
|
class FloatingLayoutInvalidatedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Where on the screen the selection that is about to be made sits: the bounds of the one
|
||||||
|
// object selected, or of all of them when the selection starts as a multi-selection
|
||||||
|
// (REQ-UI-SELECTION-PANEL). The rectangle is in the game world view's own widget
|
||||||
|
// coordinates.
|
||||||
|
//
|
||||||
|
// Published only when a selection *starts* -- a plain click or drag, or an additive one
|
||||||
|
// onto an empty selection -- and always immediately before the selection itself. Adding
|
||||||
|
// to a selection publishes nothing, which is what leaves the selection panel where it
|
||||||
|
// is while the selection grows; and because the rectangle is screen space frozen at that
|
||||||
|
// moment, scrolling the view or a selected ship flying off does not move the panel
|
||||||
|
// either.
|
||||||
|
class SelectionAnchorChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SelectionAnchorChangedEvent(QRect rectPx)
|
||||||
|
: rectPx(rectPx) {}
|
||||||
|
|
||||||
|
const QRect rectPx;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ add_files(
|
|||||||
TunnelCompletionTest.cpp
|
TunnelCompletionTest.cpp
|
||||||
WorldCoordinatesTest.cpp
|
WorldCoordinatesTest.cpp
|
||||||
WorldCameraTest.cpp
|
WorldCameraTest.cpp
|
||||||
|
FloatingPanelPlacementTest.cpp
|
||||||
SelectionControllerTest.cpp
|
SelectionControllerTest.cpp
|
||||||
BuildModeControllerTest.cpp
|
BuildModeControllerTest.cpp
|
||||||
ControlActionTest.cpp
|
ControlActionTest.cpp
|
||||||
|
|||||||
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
|
|
||||||
|
// The band every case below places into: 1000x600, so a bottom edge of 599.
|
||||||
|
static QRect makeBand()
|
||||||
|
{
|
||||||
|
return QRect(0, 0, 1000, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int kMarginPx = 8;
|
||||||
|
|
||||||
|
TEST_CASE("With nothing in the way a widget may use the whole band", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: the band's own bottom is the limit when no other floating
|
||||||
|
// widget has been placed yet.
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), {}, 0, 999, kMarginPx) == 599);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget in the same column pushes the bottom above it", "[layout]")
|
||||||
|
{
|
||||||
|
// The build button bar sitting at the bottom center leaves the space above it, less
|
||||||
|
// the margin kept between the two (REQ-UI-BUILD-BAR).
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 350, 650, kMarginPx) == 511);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget beside the column does not shorten it", "[layout]")
|
||||||
|
{
|
||||||
|
// The controls panel in the bottom-left corner is not in the way of a panel standing
|
||||||
|
// at the right edge, however tall it is (REQ-UI-CONTROLS-PANEL).
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 100, 200, 499) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 700, 999, kMarginPx) == 599);
|
||||||
|
|
||||||
|
// Touching columns do count as meeting: the panel starts exactly where the widget
|
||||||
|
// ends, which is an overlap of one pixel.
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 199, 999, kMarginPx) == 91);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The topmost widget in the column decides", "[layout]")
|
||||||
|
{
|
||||||
|
// Several widgets meet the column: the one that reaches highest is the binding one,
|
||||||
|
// whatever order they are given in.
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72),
|
||||||
|
QRect(0, 300, 500, 299),
|
||||||
|
QRect(900, 560, 100, 40) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 450, 550, kMarginPx) == 291);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An empty rectangle occupies nothing", "[layout]")
|
||||||
|
{
|
||||||
|
// A floating widget that is hidden contributes a null rect rather than being left
|
||||||
|
// out of the pass.
|
||||||
|
const std::vector<QRect> occupied = { QRect(), QRect(400, 520, 200, 0) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == 599);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget filling the column leaves nothing", "[layout]")
|
||||||
|
{
|
||||||
|
// The caller is expected to notice that the space left is not positive rather than
|
||||||
|
// being handed a floor of its own.
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 0, 1000, 600) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == -9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Which side of the selection the panel takes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("The panel stands to the right of the selection where it fits", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: right of the anchor is the first choice.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 60, 60), 300, kMarginPx)
|
||||||
|
== PanelSide::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The panel goes left when the right cannot hold it", "[layout]")
|
||||||
|
{
|
||||||
|
// A selection near the right edge leaves 100 px there, not enough for a 300 px
|
||||||
|
// panel, and the left is wide open.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(880, 100, 20, 60), 300, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Fitting on neither side, the panel takes the roomier one", "[layout]")
|
||||||
|
{
|
||||||
|
// A bounding box spanning most of the view: 192 px free on the left, 92 on the
|
||||||
|
// right, and a 300 px panel fits in neither. It covers as little as it can.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(200, 100, 700, 200), 300, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 700, 200), 300, kMarginPx)
|
||||||
|
== PanelSide::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Where it then stands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("The panel sits beside the anchor with its top edges aligned", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: separated by the margin, growing away from the selection,
|
||||||
|
// top edge on the anchor's top edge.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 120, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 120, 300, 200));
|
||||||
|
|
||||||
|
const QRect placedLeft = placeBesideAnchor(makeBand(), QRect(500, 120, 60, 60),
|
||||||
|
PanelSide::Left, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placedLeft == QRect(192, 120, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel that would hang below the view is lifted", "[layout]")
|
||||||
|
{
|
||||||
|
// Top-aligning with a selection low in the view would put the panel's bottom past
|
||||||
|
// the band, so it rises until it fits rather than overrunning it.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 500, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 400, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel standing over another widget rises above it", "[layout]")
|
||||||
|
{
|
||||||
|
// The controls panel in the bottom-left is in the way of a panel placed to the left
|
||||||
|
// of a selection: it clears the top of it by the margin (REQ-UI-CONTROLS-PANEL).
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 300, 260, 300) };
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(500, 250, 60, 60),
|
||||||
|
PanelSide::Left, QSize(300, 200), occupied,
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(192, 92, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel taller than the space left is capped", "[layout]")
|
||||||
|
{
|
||||||
|
// Capping is the caller's cue to scroll: it asked for 700 and got what there was.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 700), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 0, 300, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel that fits on neither side is pushed inside the view", "[layout]")
|
||||||
|
{
|
||||||
|
// The one case where it covers part of the selection (REQ-UI-SELECTION-PANEL): it
|
||||||
|
// stands as far from the anchor as the band allows, not off the edge of it.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 700, 200),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(700, 100, 300, 200));
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "IconCaption.h"
|
#include "IconCaption.h"
|
||||||
#include "InputMapper.h"
|
#include "InputMapper.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
@@ -270,15 +271,25 @@ BuildButtonBar::~BuildButtonBar()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::anchorTo(const QRect& worldViewRect)
|
void BuildButtonBar::placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& /*occupiedRects*/)
|
||||||
{
|
{
|
||||||
m_viewRect = worldViewRect;
|
if (viewRect.isNull())
|
||||||
recenter();
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// The layout drops hidden buttons from its size hint, but only once it has been
|
||||||
|
// re-run: an unlock changes which buttons are shown, and Qt would not get around to
|
||||||
|
// it before the bar is measured here.
|
||||||
|
layout()->activate();
|
||||||
|
|
||||||
int BuildButtonBar::getStripHeightPx() const
|
const QSize barSize = sizeHint();
|
||||||
{
|
// Centered, except that a bar wider than the view stays flush with its left edge
|
||||||
return height() + kBottomMarginPx;
|
// rather than hanging off both sides.
|
||||||
|
const int x = qMax(viewRect.left(),
|
||||||
|
viewRect.left() + (viewRect.width() - barSize.width()) / 2);
|
||||||
|
const int y = viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
||||||
|
setGeometry(QRect(QPoint(x, y), barSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::clearActiveButton()
|
void BuildButtonBar::clearActiveButton()
|
||||||
@@ -328,29 +339,11 @@ void BuildButtonBar::updateVisibility()
|
|||||||
{
|
{
|
||||||
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
||||||
}
|
}
|
||||||
// A hidden button leaves the row, so the bar has to take up its new width and
|
// A hidden button leaves the row, so the bar takes up a new width and has to be
|
||||||
// re-center on it (REQ-UI-BUILD-BAR).
|
// re-centered on it -- and the widgets that keep clear of the bar have to be placed
|
||||||
recenter();
|
// against that new rect too, so the whole pass is re-run (REQ-UI-BUILD-BAR).
|
||||||
}
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
void BuildButtonBar::recenter()
|
|
||||||
{
|
|
||||||
if (m_viewRect.isNull())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The layout drops hidden buttons from its size hint, but only once it has been
|
|
||||||
// re-run: updateVisibility() calls this straight after setVisible(), before Qt
|
|
||||||
// would get around to it on its own.
|
|
||||||
layout()->activate();
|
|
||||||
|
|
||||||
const QSize barSize = sizeHint();
|
|
||||||
// Centered, except that a bar wider than the view stays flush with its left edge
|
|
||||||
// rather than hanging off both sides.
|
|
||||||
const int x = qMax(m_viewRect.left(),
|
|
||||||
m_viewRect.left() + (m_viewRect.width() - barSize.width()) / 2);
|
|
||||||
const int y = m_viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
|
||||||
setGeometry(QRect(QPoint(x, y), barSize));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::onBuildButton(int index)
|
void BuildButtonBar::onBuildButton(int index)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DeconstructModeChangedEvent.h"
|
#include "DeconstructModeChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "UnlockedBuildingsChangedEvent.h"
|
#include "UnlockedBuildingsChangedEvent.h"
|
||||||
|
|
||||||
@@ -24,10 +25,12 @@ class BuildingIconCache;
|
|||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
|
|
||||||
// The build menu: one horizontal row of build buttons floating over the game world
|
// The build menu: one horizontal row of build buttons floating over the game world
|
||||||
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons; its owner hands it the
|
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons and centers itself along the
|
||||||
// world view's rect through anchorTo() and it centers itself along that rect's
|
// bottom edge of the rect its owner places it in. It is placed first of the floating
|
||||||
// bottom edge.
|
// widgets and so takes the space it wants outright: nothing ever moves it aside, and the
|
||||||
|
// widgets placed after it keep out of its way instead.
|
||||||
class BuildButtonBar : public QWidget,
|
class BuildButtonBar : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
DeconstructModeChangedEvent,
|
DeconstructModeChangedEvent,
|
||||||
BuildHotkeyPressedEvent,
|
BuildHotkeyPressedEvent,
|
||||||
@@ -45,15 +48,11 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonBar() override;
|
~BuildButtonBar() override;
|
||||||
|
|
||||||
// Centers the bar along the bottom edge of the game world view's rect, given in
|
// Centers the bar along the bottom edge of the game world view's rect, given in the
|
||||||
// the bar's parent coordinates (REQ-UI-BUILD-BAR). The rect is remembered, so a
|
// bar's parent coordinates (REQ-UI-BUILD-BAR). Nothing is occupied yet when the bar
|
||||||
// re-center later driven by an unlock needs no second call from the owner.
|
// is placed, so it ignores what it is handed there.
|
||||||
void anchorTo(const QRect& worldViewRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
// Height of the strip the bar occupies along the bottom of the world view: its own
|
|
||||||
// height plus the margin below it. The selection panel keeps out of this strip, and
|
|
||||||
// the bar never moves for the panel in return (REQ-UI-BUILD-BAR).
|
|
||||||
int getStripHeightPx() const;
|
|
||||||
|
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
|
|
||||||
@@ -67,11 +66,6 @@ private:
|
|||||||
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
||||||
void updateVisibility();
|
void updateVisibility();
|
||||||
|
|
||||||
// Shrinks the bar to its currently shown buttons and re-centers it in the
|
|
||||||
// anchored rect (REQ-UI-BUILD-BAR). Does nothing until anchorTo() supplied that
|
|
||||||
// rect, so the construction-time call is harmless.
|
|
||||||
void recenter();
|
|
||||||
|
|
||||||
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
||||||
@@ -91,5 +85,4 @@ private:
|
|||||||
std::map<BuildingType, int> m_costs;
|
std::map<BuildingType, int> m_costs;
|
||||||
std::optional<std::size_t> m_activeIndex;
|
std::optional<std::size_t> m_activeIndex;
|
||||||
QPushButton* m_deconstructButton;
|
QPushButton* m_deconstructButton;
|
||||||
QRect m_viewRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
@@ -47,6 +49,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
|
|||||||
@@ -5,20 +5,30 @@
|
|||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QScrollBar>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ControlActionText.h"
|
#include "ControlActionText.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "selection/SelectionNames.h"
|
#include "selection/SelectionNames.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
const int kMarginPx = 8; // between the band's edge and the panel
|
const int kMarginPx = 8; // between the view's edge and the panel
|
||||||
const int kCardMarginPx = 8; // inside the panel, around its content
|
const int kCardMarginPx = 8; // inside the panel, around its content
|
||||||
|
const int kHeadingGapPx = 4; // between the heading and the rows
|
||||||
const int kRefreshMs = 50; // see the class comment on why this polls
|
const int kRefreshMs = 50; // see the class comment on why this polls
|
||||||
|
|
||||||
|
// The panel's border, from the stylesheet below. Spelled out because the stylesheet box
|
||||||
|
// is what sets it and asking the style for it before the first show is unreliable.
|
||||||
|
const int kBorderPx = 1;
|
||||||
|
|
||||||
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
|
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
|
||||||
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
|
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
|
||||||
|
|
||||||
@@ -117,7 +127,7 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||||
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
|
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
|
||||||
kCardMarginPx, kCardMarginPx);
|
kCardMarginPx, kCardMarginPx);
|
||||||
outerLayout->setSpacing(4);
|
outerLayout->setSpacing(kHeadingGapPx);
|
||||||
|
|
||||||
m_heading = new QLabel(this);
|
m_heading = new QLabel(this);
|
||||||
m_heading->setObjectName(QStringLiteral("controlHeading"));
|
m_heading->setObjectName(QStringLiteral("controlHeading"));
|
||||||
@@ -125,11 +135,24 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
|
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
|
||||||
outerLayout->addWidget(m_heading);
|
outerLayout->addWidget(m_heading);
|
||||||
|
|
||||||
|
// Rows taller than the space available scroll rather than being cut off
|
||||||
|
// (REQ-UI-CONTROLS-PANEL). The viewport is transparent so the panel's own rounded
|
||||||
|
// chrome shows through, and horizontal scrolling is off because the width always
|
||||||
|
// follows the content.
|
||||||
m_rows = new QWidget(this);
|
m_rows = new QWidget(this);
|
||||||
m_rowsLayout = new QVBoxLayout(m_rows);
|
m_rowsLayout = new QVBoxLayout(m_rows);
|
||||||
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
m_rowsLayout->setSpacing(0);
|
m_rowsLayout->setSpacing(0);
|
||||||
outerLayout->addWidget(m_rows);
|
m_rows->setAutoFillBackground(false);
|
||||||
|
|
||||||
|
m_scrollArea = new QScrollArea(this);
|
||||||
|
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
|
m_scrollArea->setWidgetResizable(true);
|
||||||
|
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_scrollArea->viewport()->setAutoFillBackground(false);
|
||||||
|
m_scrollArea->setWidget(m_rows);
|
||||||
|
outerLayout->addWidget(m_scrollArea);
|
||||||
|
|
||||||
// Polling rather than subscribing; see the class comment.
|
// Polling rather than subscribing; see the class comment.
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
@@ -139,11 +162,10 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::anchorTo(const QRect& worldRect, const QRect& buildBarRect)
|
void ControlsPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_worldRect = worldRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
m_buildBarRect = buildBarRect;
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
refit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||||
@@ -153,8 +175,8 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
|||||||
if (m_heading->geometry().contains(event->pos()))
|
if (m_heading->geometry().contains(event->pos()))
|
||||||
{
|
{
|
||||||
m_collapsed = !m_collapsed;
|
m_collapsed = !m_collapsed;
|
||||||
m_rows->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
@@ -220,8 +242,8 @@ void ControlsPanel::rebuild(const ControlContext& context)
|
|||||||
addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
|
addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_rows->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||||
@@ -257,9 +279,9 @@ QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
|||||||
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::refit()
|
void ControlsPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||||
{
|
{
|
||||||
if (m_worldRect.isNull()) { return; }
|
if (viewRect.isNull()) { return; }
|
||||||
|
|
||||||
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
||||||
// shown once that layout runs -- without this the new rows count for nothing and
|
// shown once that layout runs -- without this the new rows count for nothing and
|
||||||
@@ -270,33 +292,48 @@ void ControlsPanel::refit()
|
|||||||
m_rowsLayout->invalidate();
|
m_rowsLayout->invalidate();
|
||||||
m_rowsLayout->activate();
|
m_rowsLayout->activate();
|
||||||
|
|
||||||
// Deliberately not activating the panel's own layout here. That lays the heading
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
// and the rows out inside the geometry the panel still has from the previous
|
|
||||||
// context, which is the wrong frame of reference for choosing the new one.
|
|
||||||
// totalSizeHint answers "how big does this need to be" without reference to how big
|
|
||||||
// it currently is; setGeometry below then re-runs the outer layout on its own.
|
|
||||||
layout()->invalidate();
|
|
||||||
const QSize needed = layout()->totalSizeHint();
|
|
||||||
|
|
||||||
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
|
||||||
if (band.width() <= 0 || band.height() <= 0) { return; }
|
if (band.width() <= 0 || band.height() <= 0) { return; }
|
||||||
|
|
||||||
|
// Measured from the heading and the rows directly rather than from the panel's own
|
||||||
|
// layout: the rows now sit in a scroll area, whose size hint describes a viewport
|
||||||
|
// and says nothing about how tall its contents are. Deliberately not activating the
|
||||||
|
// panel's own layout either -- that lays its children out inside the geometry left
|
||||||
|
// over from the previous context, which is the wrong frame of reference for
|
||||||
|
// choosing the new one. setGeometry below re-runs it.
|
||||||
|
const int chromePx = 2 * (kCardMarginPx + kBorderPx);
|
||||||
|
const QSize headingHint = m_heading->sizeHint();
|
||||||
|
const QSize rowsHint = m_rows->sizeHint();
|
||||||
|
|
||||||
|
int contentWidthPx = headingHint.width();
|
||||||
|
int contentHeightPx = headingHint.height();
|
||||||
|
if (!m_collapsed)
|
||||||
|
{
|
||||||
|
contentWidthPx = qMax(contentWidthPx, rowsHint.width());
|
||||||
|
contentHeightPx += kHeadingGapPx + rowsHint.height();
|
||||||
|
}
|
||||||
|
const int wantedHeightPx = contentHeightPx + chromePx;
|
||||||
|
|
||||||
// The bottom-left corner of the view, growing upward as rows are added
|
// The bottom-left corner of the view, growing upward as rows are added
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
// (REQ-UI-CONTROLS-PANEL).
|
||||||
const int widthPx = qMin(needed.width(), band.width());
|
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||||
int heightPx = qMin(needed.height(), band.height());
|
|
||||||
int topPx = band.bottom() - heightPx + 1;
|
|
||||||
|
|
||||||
// The build button bar is centered and sized to its buttons, so it usually leaves
|
// The build button bar is centered and sized to its buttons, so it usually leaves
|
||||||
// this corner free and the panel can share the bottom edge with it. Only when the
|
// this corner free and the panel can share the bottom edge with it. Only where the
|
||||||
// two would actually overlap does the panel rise, clearing the bar's top by the
|
// two would actually meet does the panel rise, clearing the bar's top by the same
|
||||||
// same margin it keeps from the view's edges (REQ-UI-BUILD-BAR).
|
// margin it keeps from the view's edges (REQ-UI-BUILD-BAR). The bar is the only
|
||||||
const QRect wanted(band.left(), topPx, widthPx, heightPx);
|
// widget placed before this one, so it is the only rect that can be in the way.
|
||||||
if (!m_buildBarRect.isNull() && wanted.intersects(m_buildBarRect))
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, band.left(),
|
||||||
|
band.left() + widthPx - 1, kMarginPx);
|
||||||
|
int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
|
||||||
|
int topPx = bottomPx - heightPx + 1;
|
||||||
|
|
||||||
|
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
||||||
|
// own width, or it would appear over the labels.
|
||||||
|
if (heightPx < wantedHeightPx)
|
||||||
{
|
{
|
||||||
const int availablePx = m_buildBarRect.top() - kMarginPx - band.top();
|
widthPx = qMin(widthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
|
||||||
heightPx = qMin(heightPx, qMax(0, availablePx));
|
band.width());
|
||||||
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setGeometry(band.left(), topPx, widthPx, heightPx);
|
setGeometry(band.left(), topPx, widthPx, heightPx);
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ControlAction.h"
|
#include "ControlAction.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
|
||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QScrollArea;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ class QVBoxLayout;
|
|||||||
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
||||||
// skipped unless the resolved content actually differs, which is a vector of enums to
|
// skipped unless the resolved content actually differs, which is a vector of enums to
|
||||||
// compare.
|
// compare.
|
||||||
class ControlsPanel : public QWidget
|
class ControlsPanel : public QWidget, public FloatingPanel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -41,10 +43,11 @@ public:
|
|||||||
// Places the panel in the bottom-left corner of the game world view. It shares the
|
// Places the panel in the bottom-left corner of the game world view. It shares the
|
||||||
// bottom edge with the build button bar rather than clearing its strip, the bar
|
// bottom edge with the build button bar rather than clearing its strip, the bar
|
||||||
// being centered and sized to its buttons and so usually leaving the left free; it
|
// being centered and sized to its buttons and so usually leaving the left free; it
|
||||||
// rises above the bar only when the two would otherwise overlap. `buildBarRect` is
|
// rises above the bar only when the two would otherwise overlap
|
||||||
// the bar's current geometry in the same coordinates, or a null rect when there is
|
// (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR). The bar is the only thing placed before
|
||||||
// none to avoid (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR).
|
// it, so it is the only rect it ever has to rise above.
|
||||||
void anchorTo(const QRect& worldRect, const QRect& buildBarRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
||||||
@@ -58,12 +61,15 @@ private:
|
|||||||
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
||||||
// context has none.
|
// context has none.
|
||||||
QString getHeadingText(const ControlContext& context) const;
|
QString getHeadingText(const ControlContext& context) const;
|
||||||
// Re-fits the panel to its content within the anchored band.
|
// Asks for the placement pass to be re-run, the panel's own size having changed.
|
||||||
void refit();
|
void invalidateLayout();
|
||||||
|
|
||||||
const GameWorldView* m_view;
|
const GameWorldView* m_view;
|
||||||
|
|
||||||
QLabel* m_heading;
|
QLabel* m_heading;
|
||||||
|
// Scrolls the rows once they outgrow the space the panel has (REQ-UI-CONTROLS-PANEL).
|
||||||
|
// The heading is deliberately outside it, so it stays put and stays clickable.
|
||||||
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_rows;
|
QWidget* m_rows;
|
||||||
QVBoxLayout* m_rowsLayout;
|
QVBoxLayout* m_rowsLayout;
|
||||||
QTimer* m_refreshTimer;
|
QTimer* m_refreshTimer;
|
||||||
@@ -78,9 +84,4 @@ private:
|
|||||||
// Survives context changes and simulation restarts; presentation only, never a
|
// Survives context changes and simulation restarts; presentation only, never a
|
||||||
// command (REQ-UI-CONTROLS-PANEL).
|
// command (REQ-UI-CONTROLS-PANEL).
|
||||||
bool m_collapsed = false;
|
bool m_collapsed = false;
|
||||||
|
|
||||||
// The world view the panel sits in, and the build button bar it steps around, both
|
|
||||||
// in the coordinates of its parent. Null until the owner has anchored it.
|
|
||||||
QRect m_worldRect;
|
|
||||||
QRect m_buildBarRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
27
src/ui/FloatingPanel.h
Normal file
27
src/ui/FloatingPanel.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
// A widget floating over the game world view (REQ-UI-WORLD-SIZE). MainWindow places all
|
||||||
|
// of them in one ordered pass -- build button bar, then controls panel, then selection
|
||||||
|
// panel -- and each places itself into the space the earlier ones have not taken. The
|
||||||
|
// order is the priority the requirements state: the bar never moves for anyone
|
||||||
|
// (REQ-UI-BUILD-BAR), the controls panel steps around the bar (REQ-UI-CONTROLS-PANEL),
|
||||||
|
// and keeping clear of both is the selection panel's job (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// A widget never re-places itself, because what it may take depends on the widgets placed
|
||||||
|
// before it. It instead publishes FloatingLayoutInvalidatedEvent whenever its content or
|
||||||
|
// its visibility changed, and the owner re-runs the whole pass.
|
||||||
|
class FloatingPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FloatingPanel() = default;
|
||||||
|
|
||||||
|
// Sets this widget's own geometry within viewRect, keeping clear of occupiedRects --
|
||||||
|
// the geometry of every floating widget already placed in this pass, in the same
|
||||||
|
// coordinates. A widget with nothing to show hides itself and takes no space.
|
||||||
|
virtual void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) = 0;
|
||||||
|
};
|
||||||
@@ -47,6 +47,8 @@
|
|||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "DebrisSystem.h"
|
#include "DebrisSystem.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -1255,6 +1257,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
||||||
if (buildingHit.has_value())
|
if (buildingHit.has_value())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {*buildingHit}, {}, {});
|
||||||
m_selection.selectBuildings({*buildingHit}, mode);
|
m_selection.selectBuildings({*buildingHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1262,6 +1265,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (actorHit != entt::null)
|
if (actorHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {actorHit}, {});
|
||||||
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1269,6 +1273,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (debrisHit != entt::null)
|
if (debrisHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {}, {debrisHit});
|
||||||
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1289,6 +1294,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxIds.empty())
|
if (!boxIds.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, boxIds, {}, {});
|
||||||
m_selection.selectBuildings(boxIds, mode);
|
m_selection.selectBuildings(boxIds, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1305,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxActors.empty() || !boxDebris.empty())
|
if (!boxActors.empty() || !boxDebris.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, boxActors, boxDebris);
|
||||||
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1314,35 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
if (!additive) { m_selection.clearAll(); }
|
if (!additive) { m_selection.clearAll(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::publishSelectionAnchor(SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
// An additive gesture onto something already selected is growing that selection, not
|
||||||
|
// starting one, and the panel stays where it was put (REQ-UI-SELECTION-PANEL).
|
||||||
|
const bool startsSelection =
|
||||||
|
(mode == SelectionMode::Replace) || (m_selection.getSelectedBuildings().empty()
|
||||||
|
&& m_selection.getSelectedActors().empty()
|
||||||
|
&& m_selection.getSelectedDebris().empty());
|
||||||
|
if (!startsSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen space, frozen here: the panel is placed against where the selection is at
|
||||||
|
// this moment and stays there, however far the view scrolls or the objects move
|
||||||
|
// afterwards.
|
||||||
|
const QRect anchorRect = getSelectionWidgetRect(*m_sim, getCoordinates(),
|
||||||
|
buildings, actors, debris);
|
||||||
|
if (anchorRect.isNull())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionAnchorChangedEvent>(anchorRect));
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const WorldCoordinates coordinates = getCoordinates();
|
const WorldCoordinates coordinates = getCoordinates();
|
||||||
|
|||||||
@@ -226,6 +226,15 @@ private:
|
|||||||
// which is the only case that goes on to start a box drag.
|
// which is the only case that goes on to start a box drag.
|
||||||
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
||||||
void selectInBox(bool additive);
|
void selectInBox(bool additive);
|
||||||
|
// Publishes where on the screen the selection about to be made sits, so the
|
||||||
|
// selection panel can be placed beside it (REQ-UI-SELECTION-PANEL). Called with
|
||||||
|
// what is about to be selected, immediately before selecting it, and publishes
|
||||||
|
// nothing unless that selection is starting rather than growing.
|
||||||
|
void publishSelectionAnchor(
|
||||||
|
SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
|||||||
@@ -172,19 +172,50 @@ void MainWindow::layoutPanels()
|
|||||||
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
||||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
||||||
m_gameWorldView->setGeometry(worldRect);
|
m_gameWorldView->setGeometry(worldRect);
|
||||||
// Sizes itself to its buttons and centers along the bottom of the world view
|
|
||||||
// (REQ-UI-BUILD-BAR).
|
|
||||||
m_buildButtonBar->anchorTo(worldRect);
|
|
||||||
// The panel confines itself to what the bar leaves free, so the bar never has to
|
|
||||||
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
|
||||||
m_selectionPanel->anchorTo(
|
|
||||||
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
|
||||||
// The opposite corner: bottom-left of the view, sharing the bottom edge with the
|
|
||||||
// bar rather than clearing its whole strip, and rising only if the two would meet.
|
|
||||||
// Anchored after the bar so the geometry it steps around is the current one
|
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
|
||||||
m_controlsPanel->anchorTo(worldRect, m_buildButtonBar->geometry());
|
|
||||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||||
|
|
||||||
|
// The floating widgets are placed in one ordered pass, each into the space the
|
||||||
|
// earlier ones have not taken (FloatingPanel.h). The order is the priority the
|
||||||
|
// requirements state: the build button bar takes what it wants and never moves for
|
||||||
|
// anyone (REQ-UI-BUILD-BAR), the controls panel steps around the bar
|
||||||
|
// (REQ-UI-CONTROLS-PANEL), and the selection panel keeps clear of both
|
||||||
|
// (REQ-UI-SELECTION-PANEL). A widget with nothing to show hides itself in placeIn()
|
||||||
|
// and takes no space.
|
||||||
|
//
|
||||||
|
// Re-entry is refused rather than queued: setGeometry() on a widget in the pass can
|
||||||
|
// reach code that asks for another pass, and the one already running is about to
|
||||||
|
// produce the same answer.
|
||||||
|
if (m_layingOut)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_layingOut = true;
|
||||||
|
|
||||||
|
const std::vector<QWidget*> floatingWidgets = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
const std::vector<FloatingPanel*> floatingPanels = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
|
||||||
|
std::vector<QRect> occupiedRects;
|
||||||
|
for (std::size_t i = 0; i < floatingPanels.size(); ++i)
|
||||||
|
{
|
||||||
|
floatingPanels[i]->placeIn(worldRect, occupiedRects);
|
||||||
|
if (floatingWidgets[i]->isVisible())
|
||||||
|
{
|
||||||
|
occupiedRects.push_back(floatingWidgets[i]->geometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_layingOut = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(
|
||||||
|
std::shared_ptr<const FloatingLayoutInvalidatedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// One of the floating widgets changed size or visibility. What each of them may take
|
||||||
|
// depends on the ones placed before it, so the answer is the whole pass rather than
|
||||||
|
// that one widget re-placing itself.
|
||||||
|
layoutPanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "GameOverEvent.h"
|
#include "GameOverEvent.h"
|
||||||
#include "LayoutDialogRequestedEvent.h"
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
@@ -45,7 +46,8 @@ class MainWindow : public QWidget,
|
|||||||
LayoutDialogRequestedEvent,
|
LayoutDialogRequestedEvent,
|
||||||
RecipeSelectionRequestedEvent,
|
RecipeSelectionRequestedEvent,
|
||||||
BlueprintSaveRequestedEvent,
|
BlueprintSaveRequestedEvent,
|
||||||
BlueprintSelectionRequestedEvent>
|
BlueprintSelectionRequestedEvent,
|
||||||
|
FloatingLayoutInvalidatedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const FloatingLayoutInvalidatedEvent> event) override;
|
||||||
|
|
||||||
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
||||||
// by every restart path. On success the reloaded visuals are applied to this
|
// by every restart path. On success the reloaded visuals are applied to this
|
||||||
@@ -85,6 +88,8 @@ private:
|
|||||||
// both callers already hold theirs, which is what keeps the dim continuous when a
|
// both callers already hold theirs, which is what keeps the dim continuous when a
|
||||||
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||||
void showBlueprintSelectionDialog();
|
void showBlueprintSelectionDialog();
|
||||||
|
// Places the widgets floating over the game world view, in one ordered pass
|
||||||
|
// (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent.
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -109,4 +114,8 @@ private:
|
|||||||
|
|
||||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||||
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
||||||
|
|
||||||
|
// Set while the placement pass runs, so a widget placed in it cannot start a second
|
||||||
|
// pass from inside the first.
|
||||||
|
bool m_layingOut = false;
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/ui/SelectionBounds.cpp
Normal file
115
src/ui/SelectionBounds.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "SelectionBounds.h"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "DebrisComponent.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PositionComponent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "WorldCoordinates.h"
|
||||||
|
#include "WorldPrimitives.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The widget rectangle of a footprint anchored at a tile, both kinds of body being
|
||||||
|
// described the same way.
|
||||||
|
QRectF getFootprintWidgetRect(const WorldCoordinates& coordinates, QPoint anchor,
|
||||||
|
QSize footprint)
|
||||||
|
{
|
||||||
|
const QPointF topLeft = coordinates.tileToWidget(anchor);
|
||||||
|
return QRectF(topLeft.x(), topLeft.y(),
|
||||||
|
footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
|
||||||
|
footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, building->anchor, building->footprint);
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, site->anchor, site->footprint);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(actor))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (admin.hasAll<StationBodyComponent>(actor))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& body = admin.get<StationBodyComponent>(actor);
|
||||||
|
return getFootprintWidgetRect(coordinates, body.anchor, body.footprint);
|
||||||
|
}
|
||||||
|
if (admin.hasAll<PositionComponent>(actor))
|
||||||
|
{
|
||||||
|
// A ship is a triangle about its center; the square its longest extent fits in
|
||||||
|
// is what the panel is placed beside.
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(actor).value);
|
||||||
|
const qreal extent = static_cast<qreal>(getShipForwardExtentPx(coordinates));
|
||||||
|
return QRectF(center.x() - extent, center.y() - extent,
|
||||||
|
2.0 * extent, 2.0 * extent);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(debris) || !admin.hasAll<PositionComponent, DebrisComponent>(debris))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(debris).value);
|
||||||
|
const qreal radius = static_cast<qreal>(getDebrisRadiusPx(coordinates));
|
||||||
|
return QRectF(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
QRectF bounds;
|
||||||
|
// A null rect unites to nothing of its own, so the first object found sets the box
|
||||||
|
// and every later one grows it.
|
||||||
|
auto add = [&bounds](const std::optional<QRectF>& rect)
|
||||||
|
{
|
||||||
|
if (rect.has_value())
|
||||||
|
{
|
||||||
|
bounds = bounds.isNull() ? *rect : bounds.united(*rect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (BuildingId id : buildings)
|
||||||
|
{
|
||||||
|
add(getBuildingWidgetRect(sim.getFactoryState(), coordinates, id));
|
||||||
|
}
|
||||||
|
for (entt::entity actor : actors)
|
||||||
|
{
|
||||||
|
add(getActorWidgetRect(sim.getAdmin(), coordinates, actor));
|
||||||
|
}
|
||||||
|
for (entt::entity piece : debris)
|
||||||
|
{
|
||||||
|
add(getDebrisWidgetRect(sim.getAdmin(), coordinates, piece));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds.isNull() ? QRect() : bounds.toAlignedRect();
|
||||||
|
}
|
||||||
48
src/ui/SelectionBounds.h
Normal file
48
src/ui/SelectionBounds.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
class EntityAdmin;
|
||||||
|
class Simulation;
|
||||||
|
class WorldCoordinates;
|
||||||
|
struct FactoryState;
|
||||||
|
|
||||||
|
// Where selectable objects are on the screen. The world renderer draws every selection
|
||||||
|
// outline from these rectangles, and the selection panel is placed beside the one that
|
||||||
|
// covers the whole selection (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// All of them are in the game world view's own widget coordinates, and all of them are
|
||||||
|
// only true for the WorldCoordinates they were asked for: the transform is a value built
|
||||||
|
// per frame or per event, so a rectangle does not survive a scroll or a resize.
|
||||||
|
|
||||||
|
// The footprint of a building or of a construction site (REQ-UI-SELECTION-CARD). Null
|
||||||
|
// when the id names neither, which is what a deconstruction under the caller looks like.
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The body of a ship or of a defence station (REQ-UI-ENTITY-CLICK-SELECT): a station's
|
||||||
|
// footprint, or the square the ship's triangle is drawn in.
|
||||||
|
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor);
|
||||||
|
|
||||||
|
// The circle a piece of debris is drawn as (REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris);
|
||||||
|
|
||||||
|
// The rectangle covering a whole selection -- one object, or the bounding box of all of
|
||||||
|
// them (REQ-UI-SELECTION-PANEL). Objects that no longer resolve are skipped; the result
|
||||||
|
// is null when none of them does.
|
||||||
|
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
#include "SelectionPanel.h"
|
#include "SelectionPanel.h"
|
||||||
|
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QList>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BuildingIconCache.h"
|
#include "BuildingIconCache.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -13,8 +18,8 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// Distance kept between the panel and the edges of the band it is anchored to
|
// Distance kept between the panel and the edges of the game world view, and between it
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
// and the widgets it steps around (REQ-UI-SELECTION-PANEL).
|
||||||
const int kMarginPx = 8;
|
const int kMarginPx = 8;
|
||||||
|
|
||||||
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
||||||
@@ -62,7 +67,13 @@ SelectionPanel::SelectionPanel(Simulation* sim, const GameConfig* config,
|
|||||||
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
m_scrollArea->setWidgetResizable(true);
|
m_scrollArea->setWidgetResizable(true);
|
||||||
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
// The panel works out for itself whether the card fits the band, and sizes itself to
|
||||||
|
// leave room for the bar when it does not (REQ-UI-SELECTION-PANEL), so refit() sets
|
||||||
|
// this policy rather than leaving the scroll area to decide. Asked to decide, it
|
||||||
|
// shows a bar the moment the card is momentarily larger than the viewport -- which
|
||||||
|
// happens while the card is being measured -- and does not take it back when the
|
||||||
|
// range turns out to be empty.
|
||||||
|
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
m_scrollArea->viewport()->setAutoFillBackground(false);
|
m_scrollArea->viewport()->setAutoFillBackground(false);
|
||||||
m_body->setAutoFillBackground(false);
|
m_body->setAutoFillBackground(false);
|
||||||
m_scrollArea->setWidget(m_body);
|
m_scrollArea->setWidget(m_body);
|
||||||
@@ -88,10 +99,10 @@ SelectionPanel::~SelectionPanel()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::anchorTo(const QRect& bandRect)
|
void SelectionPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_bandRect = bandRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
updateVisibility();
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
@@ -107,6 +118,18 @@ void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> ev
|
|||||||
rebuildContent();
|
rebuildContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectionPanel::handleEvent(
|
||||||
|
std::shared_ptr<const SelectionAnchorChangedEvent> event)
|
||||||
|
{
|
||||||
|
// A new selection is starting. Both the anchor and the side are settled against it
|
||||||
|
// and then left alone for as long as it lasts (REQ-UI-SELECTION-PANEL); the side is
|
||||||
|
// only reset here, being resolved on the next placement once the card's width is
|
||||||
|
// known. The rect arrives in the world view's coordinates and is translated when the
|
||||||
|
// panel is placed, the two widgets being siblings in the same parent.
|
||||||
|
m_anchorRect = event->rectPx;
|
||||||
|
m_side.reset();
|
||||||
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(
|
void SelectionPanel::handleEvent(
|
||||||
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
@@ -172,7 +195,7 @@ void SelectionPanel::refreshContent()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_content->refresh();
|
m_content->refresh();
|
||||||
updateVisibility();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::rebuildContent()
|
void SelectionPanel::rebuildContent()
|
||||||
@@ -193,34 +216,38 @@ void SelectionPanel::rebuildContent()
|
|||||||
if (m_content)
|
if (m_content)
|
||||||
{
|
{
|
||||||
m_bodyLayout->addWidget(m_content);
|
m_bodyLayout->addWidget(m_content);
|
||||||
|
// The show is what makes the card count. A widget created under an
|
||||||
|
// already-visible parent starts hidden, and a layout treats a hidden item as
|
||||||
|
// empty -- it adds nothing to the size hint until something shows it, which
|
||||||
|
// otherwise does not happen until the event loop next runs, long after refit()
|
||||||
|
// has measured the panel. The panel then fits itself to an empty body and
|
||||||
|
// collapses to its scroll bar.
|
||||||
|
m_content->show();
|
||||||
m_content->refresh();
|
m_content->refresh();
|
||||||
}
|
}
|
||||||
updateVisibility();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::updateVisibility()
|
void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||||
{
|
{
|
||||||
// Nothing selected in either category means no panel at all rather than an empty one
|
// Nothing selected in either category means no panel at all rather than an empty one
|
||||||
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before the
|
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible.
|
||||||
// owner has anchored the panel there is nowhere to put it either, so it stays hidden
|
setVisible(m_content != nullptr);
|
||||||
// until then.
|
if (m_content == nullptr || viewRect.isNull())
|
||||||
const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
|
|
||||||
setVisible(shouldShow);
|
|
||||||
if (shouldShow)
|
|
||||||
{
|
{
|
||||||
refit();
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::refit()
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
{
|
|
||||||
// The layout drops hidden widgets from its size hint, but only once it has been
|
|
||||||
// re-run: a card hides and shows its parts as it refreshes, before Qt would get
|
|
||||||
// around to it on its own.
|
|
||||||
m_body->layout()->activate();
|
|
||||||
|
|
||||||
const QRect band =
|
// The anchor is published in the world view's coordinates and this panel is placed in
|
||||||
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
// its parent's; the two widgets are siblings, so the view's own origin is the whole
|
||||||
|
// difference. Without an anchor the panel falls back to the top-right corner, by
|
||||||
|
// standing beside a point just outside that corner -- in practice unreachable, every
|
||||||
|
// non-empty selection following a click or a drag that publishes one.
|
||||||
|
const QRect anchorRect =
|
||||||
|
m_anchorRect.isNull() ? QRect(band.right() + kMarginPx + 1, band.top(), 1, 1)
|
||||||
|
: m_anchorRect.translated(viewRect.topLeft());
|
||||||
|
|
||||||
// The panel's border is drawn around the scroll area rather than around the card, so
|
// The panel's border is drawn around the scroll area rather than around the card, so
|
||||||
// it is added to whatever the card asks for. Spelled out here instead of read back
|
// it is added to whatever the card asks for. Spelled out here instead of read back
|
||||||
@@ -228,40 +255,115 @@ void SelectionPanel::refit()
|
|||||||
// style for it before the first show is unreliable.
|
// style for it before the first show is unreliable.
|
||||||
const int borderPx = 1;
|
const int borderPx = 1;
|
||||||
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
||||||
const int maxHeightPx = band.height() - 2 * borderPx;
|
if (maxWidthPx <= 0 || band.height() <= 2 * borderPx)
|
||||||
if (maxWidthPx <= 0 || maxHeightPx <= 0)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int contentWidthPx = qMin(m_body->sizeHint().width(), maxWidthPx);
|
// What the card asks for at a given width. The width has to be applied before
|
||||||
// Word-wrapped labels only know their height once the width is fixed; the layout
|
// asking, because a card's height depends on the room it is given -- and so, once
|
||||||
// reports -1 when nothing in it wraps, in which case the plain hint is exact.
|
// laid out, does the width it reports. Measuring at whatever width the panel
|
||||||
int contentHeightPx = m_body->heightForWidth(contentWidthPx);
|
// happens to have carries the previous card's shape into this one.
|
||||||
if (contentHeightPx < 0)
|
//
|
||||||
|
// Each measurement re-runs the card's layouts -- every one of them, not just the
|
||||||
|
// body's own. Changing a label's text posts a LayoutRequest to the widget holding it
|
||||||
|
// and that event is only delivered when the event loop next runs, so a layout nested
|
||||||
|
// inside the card still reports the width of the text before the change: measuring
|
||||||
|
// here and re-measuring on the following refresh then gives two different answers,
|
||||||
|
// and the panel visibly resizes a frame after its content changed. Re-activating
|
||||||
|
// them all is what a delivered LayoutRequest would have done.
|
||||||
|
//
|
||||||
|
// Cards are built and discarded whole, so this is also what discards the previous
|
||||||
|
// card's cached hints, and what accounts for the parts a card hides and shows as it
|
||||||
|
// refreshes. The polish belongs to the same step: a freshly created chip reports an
|
||||||
|
// unstyled hint until the stylesheet has reached it, and the chips carry border and
|
||||||
|
// padding that change their size.
|
||||||
|
auto measureAt = [this](int widthPx) -> QSize
|
||||||
{
|
{
|
||||||
contentHeightPx = m_body->sizeHint().height();
|
m_body->resize(widthPx, m_body->height());
|
||||||
|
m_body->ensurePolished();
|
||||||
|
|
||||||
|
// Deepest first, so no layout is re-activated from children that are themselves
|
||||||
|
// still stale. findChildren walks parents before children, hence the reverse.
|
||||||
|
const QList<QLayout*> nested = m_body->findChildren<QLayout*>();
|
||||||
|
for (QList<QLayout*>::const_reverse_iterator it = nested.rbegin();
|
||||||
|
it != nested.rend(); ++it)
|
||||||
|
{
|
||||||
|
(*it)->invalidate();
|
||||||
|
(*it)->activate();
|
||||||
|
}
|
||||||
|
m_body->layout()->invalidate();
|
||||||
|
m_body->layout()->activate();
|
||||||
|
return m_body->sizeHint();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run twice. Parts of a card report an unstyled size until the style has actually
|
||||||
|
// reached them, which for a freshly built card happens during the first round of
|
||||||
|
// measuring; the second round then measures a card that is fully laid out and
|
||||||
|
// settles on the answer. Without it a card can end up a few pixels short of what it
|
||||||
|
// turns out to need, and the difference shows as a scroll bar over a card that
|
||||||
|
// looks like it fits.
|
||||||
|
for (int pass = 0; pass < 2; ++pass)
|
||||||
|
{
|
||||||
|
// First at the cap, the most room the card can ever get, to learn how wide it
|
||||||
|
// wants to be; then at that width for the height that follows from it.
|
||||||
|
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
||||||
|
|
||||||
|
// Which side of the selection the panel takes is settled on the first placement
|
||||||
|
// after a new anchor and kept for as long as that selection lasts, so a card that
|
||||||
|
// grows or shrinks never flips the panel across the object it describes
|
||||||
|
// (REQ-UI-SELECTION-PANEL). This is the first point at which its width is known.
|
||||||
|
if (!m_side.has_value())
|
||||||
|
{
|
||||||
|
m_side = chooseSide(band, anchorRect, contentWidthPx + 2 * borderPx,
|
||||||
|
kMarginPx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentHeightPx > maxHeightPx)
|
// How much height there is depends on where the panel ends up standing: of the
|
||||||
|
// widgets placed before it, only those whose rectangles meet its own column are
|
||||||
|
// in its way. That column follows from the width just measured, so this cannot be
|
||||||
|
// settled before it -- and where the scroll bar below widens the panel, the second
|
||||||
|
// pass settles it again against the wider column. Asking for the whole band's
|
||||||
|
// height is what makes the answer the most the panel could have there.
|
||||||
|
const int maxHeightPx =
|
||||||
|
placeBesideAnchor(band, anchorRect, *m_side,
|
||||||
|
QSize(contentWidthPx + 2 * borderPx, band.height()),
|
||||||
|
occupiedRects, kMarginPx).height() - 2 * borderPx;
|
||||||
|
if (maxHeightPx <= 0)
|
||||||
{
|
{
|
||||||
// A card taller than the band is capped there and scrolls
|
return;
|
||||||
// (REQ-UI-SELECTION-PANEL). The scroll bar is laid out beside the card, so the
|
|
||||||
// panel widens by its width to keep the card as wide as the height was computed
|
|
||||||
// for.
|
|
||||||
contentHeightPx = maxHeightPx;
|
|
||||||
contentWidthPx = qMin(
|
|
||||||
contentWidthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
|
|
||||||
maxWidthPx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int contentHeightPx = measureAt(contentWidthPx).height();
|
||||||
|
|
||||||
|
// A card taller than the space left is capped there and scrolls
|
||||||
|
// (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
|
||||||
|
// widens by its width to leave the card the width its height was measured for --
|
||||||
|
// and where the cap does not allow that, the card is measured again at what is
|
||||||
|
// left over.
|
||||||
|
const bool scrolls = (contentHeightPx > maxHeightPx);
|
||||||
|
int viewportWidthPx = contentWidthPx;
|
||||||
|
if (scrolls)
|
||||||
|
{
|
||||||
|
const int scrollBarWidthPx =
|
||||||
|
m_scrollArea->verticalScrollBar()->sizeHint().width();
|
||||||
|
contentWidthPx = qMin(contentWidthPx + scrollBarWidthPx, maxWidthPx);
|
||||||
|
contentHeightPx = maxHeightPx;
|
||||||
|
viewportWidthPx = contentWidthPx - scrollBarWidthPx;
|
||||||
|
measureAt(viewportWidthPx);
|
||||||
|
}
|
||||||
|
m_scrollArea->setVerticalScrollBarPolicy(
|
||||||
|
scrolls ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||||
|
|
||||||
|
// Left at the size the scroll area is about to give it, so the card is not
|
||||||
|
// briefly wider than its viewport.
|
||||||
|
m_body->resize(viewportWidthPx, m_body->sizeHint().height());
|
||||||
|
|
||||||
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
||||||
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
||||||
|
|
||||||
// Right-aligned in the band and centered on it vertically. The band excludes the
|
setGeometry(placeBesideAnchor(band, anchorRect, *m_side,
|
||||||
// build button bar's strip, so centering here never puts the panel over the bar
|
QSize(panelWidthPx, panelHeightPx),
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
occupiedRects, kMarginPx));
|
||||||
setGeometry(band.right() - panelWidthPx + 1,
|
}
|
||||||
band.top() + (band.height() - panelHeightPx) / 2,
|
|
||||||
panelWidthPx, panelHeightPx);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "DebrisSelectionChangedEvent.h"
|
#include "DebrisSelectionChangedEvent.h"
|
||||||
#include "DebugDrawToggledEvent.h"
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
#include "selection/SelectionContentFactory.h"
|
#include "selection/SelectionContentFactory.h"
|
||||||
@@ -28,14 +34,16 @@ class QVBoxLayout;
|
|||||||
// What each card looks like lives in src/ui/selection/.
|
// What each card looks like lives in src/ui/selection/.
|
||||||
//
|
//
|
||||||
// The panel floats over the game world view rather than occupying a column of its own
|
// The panel floats over the game world view rather than occupying a column of its own
|
||||||
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, anchors to the right edge of
|
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, places itself in what the build
|
||||||
// the band its owner hands it, and hides itself entirely while nothing is selected
|
// button bar and the controls panel have left free, and hides itself entirely while
|
||||||
// (REQ-UI-EMPTY-SELECTION).
|
// nothing is selected (REQ-UI-EMPTY-SELECTION).
|
||||||
class SelectionPanel : public QWidget,
|
class SelectionPanel : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectionChangedEvent,
|
EntitySelectionChangedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
|
SelectionAnchorChangedEvent,
|
||||||
DebrisSelectionChangedEvent,
|
DebrisSelectionChangedEvent,
|
||||||
DebugDrawToggledEvent>
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
@@ -49,14 +57,16 @@ public:
|
|||||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||||
~SelectionPanel() override;
|
~SelectionPanel() override;
|
||||||
|
|
||||||
// Confines the panel to the given band of the game world view: it right-aligns
|
// Sizes the panel to its card and places it beside the selection it describes, in
|
||||||
// within it and centers vertically in it. The band is the world view less the strip
|
// what the widgets placed before it have left free. Keeping clear of them is entirely
|
||||||
// the build button bar occupies, so the two never overlap and the bar never has to
|
// this panel's job; neither of them ever moves for it (REQ-UI-SELECTION-PANEL,
|
||||||
// move (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
// REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL).
|
||||||
void anchorTo(const QRect& bandRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionAnchorChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
@@ -69,11 +79,9 @@ private:
|
|||||||
void refreshContent();
|
void refreshContent();
|
||||||
// Replaces the card with the one the current selection calls for.
|
// Replaces the card with the one the current selection calls for.
|
||||||
void rebuildContent();
|
void rebuildContent();
|
||||||
// Shows the panel while a card exists and hides it otherwise
|
// Asks for the placement pass to be re-run, the card having changed size or the
|
||||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
// panel having gained or lost its reason to be shown at all.
|
||||||
void updateVisibility();
|
void invalidateLayout();
|
||||||
// Re-fits the panel to its card within the anchored band.
|
|
||||||
void refit();
|
|
||||||
|
|
||||||
SelectionContext m_context;
|
SelectionContext m_context;
|
||||||
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||||
@@ -84,12 +92,18 @@ private:
|
|||||||
ContentKey m_contentKey;
|
ContentKey m_contentKey;
|
||||||
SelectionContent* m_content = nullptr;
|
SelectionContent* m_content = nullptr;
|
||||||
|
|
||||||
// The band the panel confines itself to, in the coordinates of its parent; null
|
// Where the current selection was on the screen when it started, in the game world
|
||||||
// until the owner has anchored it for the first time.
|
// view's coordinates, and which side of it the panel took. Both are frozen for as
|
||||||
QRect m_bandRect;
|
// long as the selection lasts: the anchor because the panel does not chase a
|
||||||
|
// scrolling view or a moving ship, the side because a card that grows must not flip
|
||||||
|
// the panel across the object (REQ-UI-SELECTION-PANEL). The side is resolved on the
|
||||||
|
// first placement after a new anchor, being the first point at which the panel's
|
||||||
|
// width is known.
|
||||||
|
QRect m_anchorRect;
|
||||||
|
std::optional<PanelSide> m_side;
|
||||||
|
|
||||||
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
||||||
// child of m_body, not of the panel itself.
|
// The card is a child of m_body, not of the panel itself.
|
||||||
QScrollArea* m_scrollArea;
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_body;
|
QWidget* m_body;
|
||||||
QVBoxLayout* m_bodyLayout;
|
QVBoxLayout* m_bodyLayout;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
@@ -108,7 +109,7 @@ QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
WorldRenderer::WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
|
WorldRenderer::WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
|
||||||
ItemIconCache* itemIcons, const std::string& configDir)
|
ItemIconCache* itemIcons, const std::string& configDir)
|
||||||
: m_sim(sim)
|
: m_sim(sim)
|
||||||
, m_visuals(visuals)
|
, m_visuals(visuals)
|
||||||
@@ -451,30 +452,6 @@ void WorldRenderer::drawBuildings(QPainter& painter, const WorldCoordinates& coo
|
|||||||
drawSelectionHighlights(painter, coordinates, frame);
|
drawSelectionHighlights(painter, coordinates, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<QRectF> WorldRenderer::footprintWidgetRect(
|
|
||||||
const WorldCoordinates& coordinates, BuildingId id) const
|
|
||||||
{
|
|
||||||
std::optional<QPoint> anchor;
|
|
||||||
std::optional<QSize> footprint;
|
|
||||||
|
|
||||||
if (const Building* b = findBuilding(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = b->anchor;
|
|
||||||
footprint = b->footprint;
|
|
||||||
}
|
|
||||||
else if (const ConstructionSite* s = findSite(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = s->anchor;
|
|
||||||
footprint = s->footprint;
|
|
||||||
}
|
|
||||||
if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; }
|
|
||||||
|
|
||||||
const QPointF tl = coordinates.tileToWidget(*anchor);
|
|
||||||
return QRectF(tl.x(), tl.y(),
|
|
||||||
footprint->width() * static_cast<qreal>(coordinates.getTilePx()),
|
|
||||||
footprint->height() * static_cast<qreal>(coordinates.getTilePx()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
const WorldRenderFrame& frame)
|
const WorldRenderFrame& frame)
|
||||||
{
|
{
|
||||||
@@ -483,7 +460,8 @@ void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordi
|
|||||||
|
|
||||||
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
||||||
{
|
{
|
||||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, selId);
|
const std::optional<QRectF> rect =
|
||||||
|
getBuildingWidgetRect(m_sim.getFactoryState(), coordinates, selId);
|
||||||
if (!rect.has_value()) { continue; }
|
if (!rect.has_value()) { continue; }
|
||||||
// Outline sits 1px outside the footprint (into adjacent tiles).
|
// Outline sits 1px outside the footprint (into adjacent tiles).
|
||||||
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public:
|
|||||||
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
||||||
// owned, must outlive this renderer. `configDir` is used once, to load the
|
// owned, must outlive this renderer. `configDir` is used once, to load the
|
||||||
// per-building world icons.
|
// per-building world icons.
|
||||||
WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
|
WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
|
||||||
ItemIconCache* itemIcons, const std::string& configDir);
|
ItemIconCache* itemIcons, const std::string& configDir);
|
||||||
~WorldRenderer();
|
~WorldRenderer();
|
||||||
|
|
||||||
@@ -139,16 +139,10 @@ private:
|
|||||||
BuildingType type, const QRectF& box,
|
BuildingType type, const QRectF& box,
|
||||||
const QColor& fill) const;
|
const QColor& fill) const;
|
||||||
|
|
||||||
// Widget-space rectangle covering a building or construction site's footprint,
|
|
||||||
// or nullopt if the id resolves to neither. Used by the selection highlight.
|
|
||||||
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
|
|
||||||
BuildingId id) const;
|
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||||
|
|
||||||
// Non-const only because EntityAdmin's component accessors are; the renderer
|
// The renderer reads the simulation and never writes it.
|
||||||
// reads the simulation and never writes it.
|
const Simulation& m_sim;
|
||||||
Simulation& m_sim;
|
|
||||||
const VisualsConfig& m_visuals;
|
const VisualsConfig& m_visuals;
|
||||||
|
|
||||||
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by
|
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request,
|
const SelectionRequest& request,
|
||||||
@@ -19,13 +20,43 @@ BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
|||||||
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
|
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
|
||||||
// running cycle names a recipe, so while it is idle there is no cycle to describe.
|
// running cycle names a recipe, so while it is idle there is no cycle to describe.
|
||||||
info.runsProduction = true;
|
info.runsProduction = true;
|
||||||
if (!target.building || !target.building->production.has_value())
|
|
||||||
|
if (target.building)
|
||||||
|
{
|
||||||
|
// What the building handles at all, so its buffers are not blank whenever it
|
||||||
|
// happens to be between cycles (REQ-UI-SINGLE-SELECTION). This is the same union
|
||||||
|
// of every recipe of its type that the simulation sized the buffers over, and
|
||||||
|
// the locked ones among them are dropped when the card lists them.
|
||||||
|
for (const RecipeDef* recipe :
|
||||||
|
gatherCandidateRecipes(*getContext().config, *target.building))
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ingredient : recipe->inputs)
|
||||||
|
{
|
||||||
|
info.handledInputs.push_back(ingredient.item);
|
||||||
|
}
|
||||||
|
for (const RecipeOutput& output : recipe->outputs)
|
||||||
|
{
|
||||||
|
info.handledOutputs.push_back(output.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which recipe describes the cycle: the one running, or -- between cycles -- the one
|
||||||
|
// that ran last. Dropping it while idle would take the summary row and the chips'
|
||||||
|
// per-cycle amounts away and bring them back with every cycle, resizing the card in
|
||||||
|
// step with the building's status (REQ-UI-RECIPE-SUMMARY). Only a building that has
|
||||||
|
// never run has nothing to describe.
|
||||||
|
if (target.building && target.building->production.has_value())
|
||||||
|
{
|
||||||
|
m_lastRecipeId = target.building->production->recipeId;
|
||||||
|
}
|
||||||
|
if (!target.building || m_lastRecipeId.empty())
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
const RecipeDef* recipe =
|
||||||
target.building->production->recipeId, target.type);
|
getContext().config->recipes.findRecipeDef(m_lastRecipeId, target.type);
|
||||||
if (!recipe)
|
if (!recipe)
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "BufferedBuildingContent.h"
|
#include "BufferedBuildingContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
|
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
|
||||||
// auto-process whatever they receive and have no player-facing recipe selection
|
// auto-process whatever they receive and have no player-facing recipe selection
|
||||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
|
||||||
// all; its cycle is whichever recipe is currently in production.
|
// all; its cycle is whichever recipe is in production, or the last one that was.
|
||||||
class AutoProductionContent : public BufferedBuildingContent
|
class AutoProductionContent : public BufferedBuildingContent
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -17,4 +19,11 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The recipe last seen in production, which keeps describing the cycle while the
|
||||||
|
// building sits between cycles (REQ-UI-RECIPE-SUMMARY). Mutable because it is a
|
||||||
|
// record of what getCycleInfo() has observed rather than state of its own: the card
|
||||||
|
// shows the same thing whether or not it has been asked before.
|
||||||
|
mutable std::string m_lastRecipeId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,9 +50,14 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* /*event*/) override
|
void paintEvent(QPaintEvent* /*event*/) override
|
||||||
{
|
{
|
||||||
|
// The active group is asked for by name rather than taken from the current one,
|
||||||
|
// which follows the window's focus: the inactive group's highlight is close
|
||||||
|
// enough to the card's background that the bar reads as gone whenever the game
|
||||||
|
// is tabbed away from or a modal dialog holds focus -- exactly when a player
|
||||||
|
// watching a build finish is most likely to be looking at it.
|
||||||
const QColor fill = m_fillColor.isValid()
|
const QColor fill = m_fillColor.isValid()
|
||||||
? m_fillColor
|
? m_fillColor
|
||||||
: palette().color(QPalette::Highlight);
|
: palette().color(QPalette::Active, QPalette::Highlight);
|
||||||
|
|
||||||
QColor track = fill;
|
QColor track = fill;
|
||||||
track.setAlpha(kTrackAlpha);
|
track.setAlpha(kTrackAlpha);
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
#include "BufferSection.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "ItemChipRow.h"
|
|
||||||
#include "SectionBox.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
int findPerCycle(const std::map<std::string, int>& perCycle, const std::string& itemId)
|
|
||||||
{
|
|
||||||
const std::map<std::string, int>::const_iterator it = perCycle.find(itemId);
|
|
||||||
return (it != perCycle.end()) ? it->second : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
|
|
||||||
BufferSection::BufferSection(const SelectionContext& context, QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
layout->setSpacing(6);
|
|
||||||
|
|
||||||
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
|
||||||
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
|
|
||||||
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
|
||||||
|
|
||||||
m_outputSection = new SectionBox(tr("Output buffer"), this);
|
|
||||||
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
|
|
||||||
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
|
||||||
|
|
||||||
layout->addWidget(m_inputSection);
|
|
||||||
layout->addWidget(m_outputSection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BufferSection::setBuffers(const Building& building,
|
|
||||||
const std::map<std::string, int>& perCycleInputs,
|
|
||||||
const std::map<std::string, int>& perCycleOutputs)
|
|
||||||
{
|
|
||||||
std::vector<ItemChipRow::Entry> inputs;
|
|
||||||
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
|
||||||
{
|
|
||||||
ItemChipRow::Entry chip;
|
|
||||||
chip.itemId = entry.first.id;
|
|
||||||
chip.countText = QString::number(entry.second);
|
|
||||||
|
|
||||||
const int perCycle = findPerCycle(perCycleInputs, entry.first.id);
|
|
||||||
if (perCycle > 0)
|
|
||||||
{
|
|
||||||
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
|
|
||||||
}
|
|
||||||
inputs.push_back(chip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output-side items are the buffered ones plus those still emerging onto the output
|
|
||||||
// belts: an emerging item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE),
|
|
||||||
// so leaving it out would make it vanish from the panel while it animates.
|
|
||||||
std::map<std::string, int> outputCounts;
|
|
||||||
for (const Item& item : building.outputBuffer.items)
|
|
||||||
{
|
|
||||||
outputCounts[item.type.id]++;
|
|
||||||
}
|
|
||||||
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
|
|
||||||
{
|
|
||||||
for (const BeltItemSlot& slot : lane)
|
|
||||||
{
|
|
||||||
outputCounts[slot.item.type.id]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A configured building lists everything its cycle produces, so an output the player
|
|
||||||
// is waiting for reads as 0 rather than being absent.
|
|
||||||
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
|
|
||||||
{
|
|
||||||
outputCounts.emplace(entry.first, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<ItemChipRow::Entry> outputs;
|
|
||||||
for (const std::pair<const std::string, int>& entry : outputCounts)
|
|
||||||
{
|
|
||||||
ItemChipRow::Entry chip;
|
|
||||||
chip.itemId = entry.first;
|
|
||||||
// Counted against the buffer's capacity, which is what production stops at
|
|
||||||
// (REQ-MAT-OUTPUT-BUFFER).
|
|
||||||
chip.countText = building.outputBuffer.capacity > 0
|
|
||||||
? tr("%1 / %2").arg(entry.second).arg(building.outputBuffer.capacity)
|
|
||||||
: QString::number(entry.second);
|
|
||||||
chip.subLine = QString::fromStdString(toDisplayName(entry.first));
|
|
||||||
outputs.push_back(chip);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_inputChips->setEntries(inputs);
|
|
||||||
m_outputChips->setEntries(outputs);
|
|
||||||
m_inputSection->setVisible(!inputs.empty());
|
|
||||||
m_outputSection->setVisible(!outputs.empty());
|
|
||||||
setVisible(!inputs.empty() || !outputs.empty());
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "SelectionContext.h"
|
|
||||||
|
|
||||||
struct Building;
|
|
||||||
class ItemChipRow;
|
|
||||||
class SectionBox;
|
|
||||||
|
|
||||||
// The input and output buffer contents of one building, each a captioned section of item
|
|
||||||
// chips (REQ-UI-SINGLE-SELECTION).
|
|
||||||
//
|
|
||||||
// Counting what is in the buffers is the same for every building type, so it happens
|
|
||||||
// here; what a cycle consumes and produces is not, so the owning content supplies those
|
|
||||||
// per-cycle amounts. An item with no entry in the maps is shown without a denominator,
|
|
||||||
// and a section holding nothing is not shown at all.
|
|
||||||
class BufferSection : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BufferSection(const SelectionContext& context, QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
// perCycleInputs and perCycleOutputs map an item id to the amount one production
|
|
||||||
// cycle consumes or produces. Both may be empty, for a building that runs no cycle.
|
|
||||||
void setBuffers(const Building& building,
|
|
||||||
const std::map<std::string, int>& perCycleInputs,
|
|
||||||
const std::map<std::string, int>& perCycleOutputs);
|
|
||||||
|
|
||||||
private:
|
|
||||||
SectionBox* m_inputSection;
|
|
||||||
ItemChipRow* m_inputChips;
|
|
||||||
SectionBox* m_outputSection;
|
|
||||||
ItemChipRow* m_outputChips;
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
#include "BufferedBuildingContent.h"
|
#include "BufferedBuildingContent.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BufferSection.h"
|
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "ProductionSection.h"
|
#include "ProductionSection.h"
|
||||||
#include "RecipeSummaryRow.h"
|
#include "RecipeSummaryRow.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
#include "SelectionNames.h"
|
#include "SelectionNames.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
@@ -27,6 +29,33 @@ std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>
|
|||||||
return amounts;
|
return amounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every item one side of the card should list: what the buffer holds, what a cycle
|
||||||
|
// moves, and what the building handles at all. A building's buffers can carry items it
|
||||||
|
// is not currently making anything of, and an auto-recipe building between cycles names
|
||||||
|
// nothing at all, so the three sources are unioned rather than one being picked.
|
||||||
|
std::set<std::string> collectItemIds(const std::map<std::string, int>& buffered,
|
||||||
|
const std::map<std::string, int>& perCycle,
|
||||||
|
const std::vector<std::string>& handled)
|
||||||
|
{
|
||||||
|
std::set<std::string> itemIds;
|
||||||
|
for (const std::pair<const std::string, int>& entry : buffered)
|
||||||
|
{
|
||||||
|
itemIds.insert(entry.first);
|
||||||
|
}
|
||||||
|
for (const std::pair<const std::string, int>& entry : perCycle)
|
||||||
|
{
|
||||||
|
itemIds.insert(entry.first);
|
||||||
|
}
|
||||||
|
itemIds.insert(handled.begin(), handled.end());
|
||||||
|
return itemIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lookUp(const std::map<std::string, int>& map, const std::string& key)
|
||||||
|
{
|
||||||
|
const std::map<std::string, int>::const_iterator it = map.find(key);
|
||||||
|
return (it != map.end()) ? it->second : 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -40,10 +69,21 @@ BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context
|
|||||||
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
|
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
|
||||||
getConfigurationLayout()->addWidget(m_recipeSummary);
|
getConfigurationLayout()->addWidget(m_recipeSummary);
|
||||||
|
|
||||||
m_buffers = new BufferSection(context, this);
|
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
||||||
|
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
|
||||||
|
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
||||||
|
|
||||||
m_production = new ProductionSection(this);
|
m_production = new ProductionSection(this);
|
||||||
getRuntimeLayout()->addWidget(m_buffers);
|
|
||||||
|
m_outputSection = new SectionBox(tr("Output buffer"), this);
|
||||||
|
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
|
||||||
|
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
||||||
|
|
||||||
|
// In the direction the materials flow: what goes in, what is being made of it, what
|
||||||
|
// has come out (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS).
|
||||||
|
getRuntimeLayout()->addWidget(m_inputSection);
|
||||||
getRuntimeLayout()->addWidget(m_production);
|
getRuntimeLayout()->addWidget(m_production);
|
||||||
|
getRuntimeLayout()->addWidget(m_outputSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferedBuildingContent::refreshConfiguration()
|
void BufferedBuildingContent::refreshConfiguration()
|
||||||
@@ -77,8 +117,93 @@ void BufferedBuildingContent::refreshRuntime()
|
|||||||
setProductionStatusSlot(*target.building);
|
setProductionStatusSlot(*target.building);
|
||||||
|
|
||||||
const CycleInfo cycle = getCycleInfo(target);
|
const CycleInfo cycle = getCycleInfo(target);
|
||||||
m_buffers->setBuffers(*target.building, cycle.perCycleInputs, cycle.perCycleOutputs);
|
|
||||||
|
const std::vector<ItemChipRow::Entry> inputs =
|
||||||
|
buildInputEntries(*target.building, cycle);
|
||||||
|
const std::vector<ItemChipRow::Entry> outputs =
|
||||||
|
buildOutputEntries(*target.building, cycle);
|
||||||
|
|
||||||
|
m_inputChips->setEntries(inputs);
|
||||||
|
m_outputChips->setEntries(outputs);
|
||||||
|
m_inputSection->setVisible(!inputs.empty());
|
||||||
|
m_outputSection->setVisible(!outputs.empty());
|
||||||
|
|
||||||
m_production->setProduction(cycle.runsProduction, *target.building,
|
m_production->setProduction(cycle.runsProduction, *target.building,
|
||||||
cycle.durationSeconds,
|
cycle.durationSeconds,
|
||||||
getContext().sim->getCurrentTick());
|
getContext().sim->getCurrentTick());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildInputEntries(
|
||||||
|
const Building& building, const CycleInfo& cycle) const
|
||||||
|
{
|
||||||
|
std::map<std::string, int> buffered;
|
||||||
|
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
||||||
|
{
|
||||||
|
buffered[entry.first.id] = entry.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ItemChipRow::Entry> entries;
|
||||||
|
for (const std::string& itemId :
|
||||||
|
collectItemIds(buffered, cycle.perCycleInputs, cycle.handledInputs))
|
||||||
|
{
|
||||||
|
// An auto-recipe building's buffers are sized over every recipe of its type,
|
||||||
|
// including recipes still locked, so those entries are left out here
|
||||||
|
// (REQ-UI-SINGLE-SELECTION, REQ-LOCK-UI-RECIPE).
|
||||||
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
||||||
|
|
||||||
|
ItemChipRow::Entry chip;
|
||||||
|
chip.itemId = itemId;
|
||||||
|
chip.countText = QString::number(lookUp(buffered, itemId));
|
||||||
|
|
||||||
|
const int perCycle = lookUp(cycle.perCycleInputs, itemId);
|
||||||
|
if (perCycle > 0)
|
||||||
|
{
|
||||||
|
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chip.subLine = QString::fromStdString(toDisplayName(itemId));
|
||||||
|
}
|
||||||
|
entries.push_back(chip);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildOutputEntries(
|
||||||
|
const Building& building, const CycleInfo& cycle) const
|
||||||
|
{
|
||||||
|
// The buffered items plus those still emerging onto the output belts: an emerging
|
||||||
|
// item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE), so leaving it out
|
||||||
|
// would make it vanish from the panel while it animates.
|
||||||
|
std::map<std::string, int> buffered;
|
||||||
|
for (const Item& item : building.outputBuffer.items)
|
||||||
|
{
|
||||||
|
buffered[item.type.id]++;
|
||||||
|
}
|
||||||
|
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
|
||||||
|
{
|
||||||
|
for (const BeltItemSlot& slot : lane)
|
||||||
|
{
|
||||||
|
buffered[slot.item.type.id]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ItemChipRow::Entry> entries;
|
||||||
|
for (const std::string& itemId :
|
||||||
|
collectItemIds(buffered, cycle.perCycleOutputs, cycle.handledOutputs))
|
||||||
|
{
|
||||||
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
||||||
|
|
||||||
|
ItemChipRow::Entry chip;
|
||||||
|
chip.itemId = itemId;
|
||||||
|
// Counted against the buffer's capacity, which is what production stops at
|
||||||
|
// (REQ-MAT-OUTPUT-BUFFER).
|
||||||
|
chip.countText = building.outputBuffer.capacity > 0
|
||||||
|
? tr("%1 / %2").arg(lookUp(buffered, itemId))
|
||||||
|
.arg(building.outputBuffer.capacity)
|
||||||
|
: QString::number(lookUp(buffered, itemId));
|
||||||
|
chip.subLine = QString::fromStdString(toDisplayName(itemId));
|
||||||
|
entries.push_back(chip);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,20 +2,23 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
#include "ItemChipRow.h"
|
||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
|
|
||||||
|
struct Building;
|
||||||
struct BuildingTarget;
|
struct BuildingTarget;
|
||||||
class BufferSection;
|
|
||||||
class ProductionSection;
|
class ProductionSection;
|
||||||
class RecipeSummaryRow;
|
class RecipeSummaryRow;
|
||||||
|
class SectionBox;
|
||||||
|
|
||||||
// Shared body of the four cards that show one building with buffers -- the Miner and
|
// Shared body of the four cards that show one building with buffers -- the Miner and
|
||||||
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
|
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
|
||||||
// (REQ-UI-SELECTION-CONTENT). All four show the same header identity and status, recipe
|
// (REQ-UI-SELECTION-CONTENT). All four show the same header status, recipe summary,
|
||||||
// summary, buffer contents and production progress; they differ only in what one
|
// buffer contents and production progress; they differ only in what one production cycle
|
||||||
// production cycle costs and how long it takes, which is what the subclass supplies.
|
// costs and how long it takes, which is what the subclass supplies.
|
||||||
//
|
//
|
||||||
// This is implementation sharing, not a catalog entry: every concrete subclass is one
|
// This is implementation sharing, not a catalog entry: every concrete subclass is one
|
||||||
// row of the content catalog.
|
// row of the content catalog.
|
||||||
@@ -29,12 +32,21 @@ protected:
|
|||||||
{
|
{
|
||||||
std::map<std::string, int> perCycleInputs;
|
std::map<std::string, int> perCycleInputs;
|
||||||
std::map<std::string, int> perCycleOutputs;
|
std::map<std::string, int> perCycleOutputs;
|
||||||
|
|
||||||
|
// Items the card lists whether or not they are currently in the buffers, for a
|
||||||
|
// building whose recipe is implicit and so has nothing to name while it sits
|
||||||
|
// between cycles (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They carry no
|
||||||
|
// per-cycle denominator, since no one recipe is in force.
|
||||||
|
std::vector<std::string> handledInputs;
|
||||||
|
std::vector<std::string> handledOutputs;
|
||||||
|
|
||||||
// False when the building produces nothing at all (the Salvage Bay,
|
// False when the building produces nothing at all (the Salvage Bay,
|
||||||
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
|
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
|
||||||
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
|
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
bool runsProduction = false;
|
bool runsProduction = false;
|
||||||
// 0 while an auto-recipe building sits between cycles, when no single recipe
|
// 0 only when no recipe describes the cycle at all -- an auto-recipe building
|
||||||
// names a cycle time; the progress line then reads "idle".
|
// that has yet to run one (REQ-UI-RECIPE-SUMMARY). The progress line reads
|
||||||
|
// "idle" whenever no cycle is actually running, whatever this says.
|
||||||
double durationSeconds = 0.0;
|
double durationSeconds = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,8 +67,20 @@ private:
|
|||||||
void refreshConfiguration() override;
|
void refreshConfiguration() override;
|
||||||
void refreshRuntime() override;
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
std::vector<ItemChipRow::Entry> buildInputEntries(const Building& building,
|
||||||
|
const CycleInfo& cycle) const;
|
||||||
|
std::vector<ItemChipRow::Entry> buildOutputEntries(const Building& building,
|
||||||
|
const CycleInfo& cycle) const;
|
||||||
|
|
||||||
BuildingId m_id;
|
BuildingId m_id;
|
||||||
|
|
||||||
RecipeSummaryRow* m_recipeSummary;
|
RecipeSummaryRow* m_recipeSummary;
|
||||||
BufferSection* m_buffers;
|
|
||||||
|
// Input buffers, production progress, output buffer -- in that order, so the card
|
||||||
|
// reads the way the materials flow (REQ-UI-SINGLE-SELECTION).
|
||||||
|
SectionBox* m_inputSection;
|
||||||
|
ItemChipRow* m_inputChips;
|
||||||
ProductionSection* m_production;
|
ProductionSection* m_production;
|
||||||
|
SectionBox* m_outputSection;
|
||||||
|
ItemChipRow* m_outputChips;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
|
||||||
@@ -51,7 +50,6 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ HqContent::HqContent(const SelectionContext& context, const SelectionRequest& re
|
|||||||
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
|
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
|
||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
{
|
{
|
||||||
|
m_hpBar = new BarRow(tr("HP"), this);
|
||||||
|
|
||||||
m_stockSection = new SectionBox(tr("Building blocks"), this);
|
m_stockSection = new SectionBox(tr("Building blocks"), this);
|
||||||
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
|
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
|
||||||
m_stockSection->getContentLayout()->addWidget(m_stockChips);
|
m_stockSection->getContentLayout()->addWidget(m_stockChips);
|
||||||
|
|
||||||
m_hpBar = new BarRow(tr("HP"), this);
|
// HP first, as on every card that has it (REQ-UI-SELECTION-CARD, REQ-UI-HQ-PANEL).
|
||||||
|
|
||||||
getRuntimeLayout()->addWidget(m_stockSection);
|
|
||||||
getRuntimeLayout()->addWidget(m_hpBar);
|
getRuntimeLayout()->addWidget(m_hpBar);
|
||||||
|
getRuntimeLayout()->addWidget(m_stockSection);
|
||||||
|
|
||||||
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
|
|||||||
ItemChip* chip = new ItemChip(icon, this);
|
ItemChip* chip = new ItemChip(icon, this);
|
||||||
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
||||||
static_cast<int>(index) % kChipsPerRow);
|
static_cast<int>(index) % kChipsPerRow);
|
||||||
|
// Shown right away, because the panel measures itself as soon as this returns. A
|
||||||
|
// widget created under an already-visible parent starts hidden, and a layout
|
||||||
|
// treats a hidden item as empty, so an unshown chip would add nothing to the
|
||||||
|
// size hint and the panel would be fitted to a buffer section that looks empty.
|
||||||
|
chip->show();
|
||||||
m_chips.push_back(chip);
|
m_chips.push_back(chip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ namespace
|
|||||||
// Size the item icons are drawn at on the summary line, in device-independent pixels.
|
// Size the item icons are drawn at on the summary line, in device-independent pixels.
|
||||||
const int kSummaryIconSizePx = 14;
|
const int kSummaryIconSizePx = 14;
|
||||||
|
|
||||||
|
// Adds a freshly built label to the summary and shows it.
|
||||||
|
//
|
||||||
|
// The show is what makes it count: a widget created under an already-visible parent
|
||||||
|
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
|
||||||
|
// add nothing to the size hint the panel measures itself against as soon as this
|
||||||
|
// returns.
|
||||||
|
void addAndShow(QHBoxLayout* layout, QLabel* label)
|
||||||
|
{
|
||||||
|
layout->addWidget(label);
|
||||||
|
label->show();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -71,14 +83,14 @@ void RecipeSummaryRow::rebuild(const std::vector<Amount>& inputs,
|
|||||||
if (!inputs.empty())
|
if (!inputs.empty())
|
||||||
{
|
{
|
||||||
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||||
m_layout->addWidget(new QLabel(QString(rightArrow), this));
|
addAndShow(m_layout, new QLabel(QString(rightArrow), this));
|
||||||
}
|
}
|
||||||
addAmounts(outputs);
|
addAmounts(outputs);
|
||||||
|
|
||||||
if (durationSeconds > 0.0)
|
if (durationSeconds > 0.0)
|
||||||
{
|
{
|
||||||
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
||||||
m_layout->addWidget(new QLabel(
|
addAndShow(m_layout, new QLabel(
|
||||||
QStringLiteral("%1 %2").arg(middleDot).arg(
|
QStringLiteral("%1 %2").arg(middleDot).arg(
|
||||||
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
|
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
|
||||||
}
|
}
|
||||||
@@ -96,13 +108,13 @@ void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
|
|||||||
QLabel* iconLabel = new QLabel(this);
|
QLabel* iconLabel = new QLabel(this);
|
||||||
iconLabel->setPixmap(
|
iconLabel->setPixmap(
|
||||||
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
|
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
|
||||||
m_layout->addWidget(iconLabel);
|
addAndShow(m_layout, iconLabel);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_layout->addWidget(
|
addAndShow(m_layout,
|
||||||
new QLabel(QString::fromStdString(entry.itemId), this));
|
new QLabel(QString::fromStdString(entry.itemId), this));
|
||||||
}
|
}
|
||||||
m_layout->addWidget(new QLabel(QString::number(entry.amount), this));
|
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,38 @@ const int kSymbolSizePx = 20;
|
|||||||
const int kCardSpacingPx = 6;
|
const int kCardSpacingPx = 6;
|
||||||
const int kHeaderSpacingPx = 6;
|
const int kHeaderSpacingPx = 6;
|
||||||
|
|
||||||
|
// The Salvage Bay has no recipe and no cycle: its two states say whether it is holding
|
||||||
|
// scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY, REQ-UI-SELECTION-STATUS).
|
||||||
|
QString getStatusCaption(ProductionStatus status, bool isSalvageBay)
|
||||||
|
{
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case ProductionStatus::Unconfigured: return QObject::tr("no recipe");
|
||||||
|
case ProductionStatus::Producing:
|
||||||
|
return isSalvageBay ? QObject::tr("holding scrap")
|
||||||
|
: QObject::tr("producing");
|
||||||
|
case ProductionStatus::Starved:
|
||||||
|
return isSalvageBay ? QObject::tr("empty") : QObject::tr("missing input");
|
||||||
|
case ProductionStatus::Blocked: return QObject::tr("output full");
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every caption the status slot can end up showing for this building, so the header can
|
||||||
|
// keep its width as the state changes rather than the panel jumping with it.
|
||||||
|
QStringList getAllStatusCaptions(bool isSalvageBay)
|
||||||
|
{
|
||||||
|
QStringList captions;
|
||||||
|
for (ProductionStatus status : { ProductionStatus::Unconfigured,
|
||||||
|
ProductionStatus::Producing,
|
||||||
|
ProductionStatus::Starved,
|
||||||
|
ProductionStatus::Blocked })
|
||||||
|
{
|
||||||
|
captions << getStatusCaption(status, isSalvageBay);
|
||||||
|
}
|
||||||
|
return captions;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -96,7 +128,10 @@ SelectionContent::SelectionContent(const SelectionContext& context,
|
|||||||
m_constructionSection->getContentLayout()->addWidget(m_constructionBar);
|
m_constructionSection->getContentLayout()->addWidget(m_constructionBar);
|
||||||
m_constructionSection->getContentLayout()->addWidget(
|
m_constructionSection->getContentLayout()->addWidget(
|
||||||
new EmptyNote(tr("No buffers until built"), m_constructionSection));
|
new EmptyNote(tr("No buffers until built"), m_constructionSection));
|
||||||
cardLayout->addWidget(m_constructionSection);
|
// Directly below the header rather than where the runtime group sits, so how far
|
||||||
|
// along the site is reads before what it is configured to become
|
||||||
|
// (REQ-UI-SELECTION-CARD).
|
||||||
|
cardLayout->insertWidget(1, m_constructionSection);
|
||||||
|
|
||||||
setSlot(QColor(), tr("constructing"));
|
setSlot(QColor(), tr("constructing"));
|
||||||
}
|
}
|
||||||
@@ -165,25 +200,26 @@ void SelectionContent::setProductionStatusSlot(const Building& building)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
|
||||||
// The Salvage Bay has no recipe and no cycle: its two states say whether it is
|
|
||||||
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
|
|
||||||
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
||||||
|
// Room for every caption this building can show, so the panel keeps its width as the
|
||||||
|
// building's state changes rather than jumping with it.
|
||||||
|
reserveSlotFor(getAllStatusCaptions(isSalvageBay));
|
||||||
|
|
||||||
|
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
||||||
|
QColor dotColor;
|
||||||
switch (*status)
|
switch (*status)
|
||||||
{
|
{
|
||||||
case ProductionStatus::Unconfigured:
|
case ProductionStatus::Unconfigured: dotColor = colors.grey; break;
|
||||||
setSlot(colors.grey, tr("no recipe"));
|
case ProductionStatus::Producing: dotColor = colors.green; break;
|
||||||
break;
|
case ProductionStatus::Starved: dotColor = colors.red; break;
|
||||||
case ProductionStatus::Producing:
|
case ProductionStatus::Blocked: dotColor = colors.yellow; break;
|
||||||
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
|
|
||||||
break;
|
|
||||||
case ProductionStatus::Starved:
|
|
||||||
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
|
|
||||||
break;
|
|
||||||
case ProductionStatus::Blocked:
|
|
||||||
setSlot(colors.yellow, tr("output full"));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
setSlot(dotColor, getStatusCaption(*status, isSalvageBay));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::reserveSlotFor(const QStringList& captions)
|
||||||
|
{
|
||||||
|
m_statusPill->reserveFor(captions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionContent::refreshConstruction()
|
void SelectionContent::refreshConstruction()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
@@ -79,6 +80,11 @@ protected:
|
|||||||
void setCountSlot(int count);
|
void setCountSlot(int count);
|
||||||
void clearSlot();
|
void clearSlot();
|
||||||
|
|
||||||
|
// Reserves header room for the widest caption the slot will ever show. The panel is
|
||||||
|
// sized to its card (REQ-UI-SELECTION-PANEL), so without this it changes width every
|
||||||
|
// time the caption does -- and a card whose chips wrap changes height with it.
|
||||||
|
void reserveSlotFor(const QStringList& captions);
|
||||||
|
|
||||||
// Fills the right slot from the building's production status, mapped to the same
|
// Fills the right slot from the building's production status, mapped to the same
|
||||||
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
|
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
|
||||||
// the slot empty for a type that has no status light.
|
// the slot empty for a type that has no status light.
|
||||||
|
|||||||
@@ -33,3 +33,16 @@ QString getBehaviorLabel(BehaviorKind kind)
|
|||||||
}
|
}
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList getAllBehaviorLabels()
|
||||||
|
{
|
||||||
|
QStringList labels;
|
||||||
|
for (BehaviorKind kind : { BehaviorKind::Retreat, BehaviorKind::Attack,
|
||||||
|
BehaviorKind::SalvageScrap, BehaviorKind::Repair,
|
||||||
|
BehaviorKind::Rally, BehaviorKind::Standby,
|
||||||
|
BehaviorKind::Advance })
|
||||||
|
{
|
||||||
|
labels << getBehaviorLabel(kind);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
@@ -13,3 +14,8 @@ QString getBuildingTypeName(BuildingType type);
|
|||||||
// Name of the behavior currently governing a ship, for the ship card's header slot
|
// Name of the behavior currently governing a ship, for the ship card's header slot
|
||||||
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
||||||
QString getBehaviorLabel(BehaviorKind kind);
|
QString getBehaviorLabel(BehaviorKind kind);
|
||||||
|
|
||||||
|
// Every name getBehaviorLabel can return. A ship's behavior changes as it fights, so the
|
||||||
|
// header reserves room for the widest of these rather than letting the panel change
|
||||||
|
// width each time (REQ-UI-SELECTION-PANEL).
|
||||||
|
QStringList getAllBehaviorLabels();
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ ShipContent::ShipContent(const SelectionContext& context,
|
|||||||
m_statsPanel = new ShipStatsPanel(context.config, this);
|
m_statsPanel = new ShipStatsPanel(context.config, this);
|
||||||
getRuntimeLayout()->addWidget(m_statsPanel);
|
getRuntimeLayout()->addWidget(m_statsPanel);
|
||||||
|
|
||||||
|
// A ship's behavior changes as it fights, so the header holds room for the longest
|
||||||
|
// name rather than the panel resizing under the player each time it does.
|
||||||
|
reserveSlotFor(getAllBehaviorLabels());
|
||||||
|
|
||||||
EntityAdmin& admin = context.sim->getAdmin();
|
EntityAdmin& admin = context.sim->getAdmin();
|
||||||
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
|
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "StatusPill.h"
|
#include "StatusPill.h"
|
||||||
|
|
||||||
|
#include <QFontMetrics>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
@@ -49,6 +50,23 @@ StatusPill::StatusPill(QWidget* parent)
|
|||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StatusPill::reserveFor(const QStringList& captions)
|
||||||
|
{
|
||||||
|
if (captions == m_reservedFor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_reservedFor = captions;
|
||||||
|
|
||||||
|
const QFontMetrics metrics(m_captionLabel->font());
|
||||||
|
int widestPx = 0;
|
||||||
|
for (const QString& caption : captions)
|
||||||
|
{
|
||||||
|
widestPx = qMax(widestPx, metrics.horizontalAdvance(caption));
|
||||||
|
}
|
||||||
|
m_captionLabel->setMinimumWidth(widestPx);
|
||||||
|
}
|
||||||
|
|
||||||
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
const QString& caption)
|
const QString& caption)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@@ -23,7 +24,16 @@ public:
|
|||||||
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
const QString& caption);
|
const QString& caption);
|
||||||
|
|
||||||
|
// Reserves room for the widest of the captions this pill can show, so the header --
|
||||||
|
// and with it the panel, which is sized to its content (REQ-UI-SELECTION-PANEL) --
|
||||||
|
// keeps its width as the caption changes. Without it the whole panel jumps every
|
||||||
|
// time a building's status does, and a card whose chips wrap changes height with it.
|
||||||
|
void reserveFor(const QStringList& captions);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_dotLabel;
|
QLabel* m_dotLabel;
|
||||||
QLabel* m_captionLabel;
|
QLabel* m_captionLabel;
|
||||||
|
// What the width is currently reserved for, so re-reserving the same set on every
|
||||||
|
// refresh costs nothing.
|
||||||
|
QStringList m_reservedFor;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user