place the selection panel beside what it describes

This commit is contained in:
2026-08-09 13:45:04 +02:00
parent 7ae5f8c4dc
commit c0b009c548
25 changed files with 789 additions and 182 deletions

View File

@@ -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.
@@ -605,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.
@@ -618,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.

View File

@@ -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

View 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);
}

View 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);

View File

@@ -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
) )

View 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
{
};

View 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;
};

View File

@@ -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

View 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));
}

View File

@@ -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)

View File

@@ -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;
}; };

View File

@@ -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

View File

@@ -11,6 +11,9 @@
#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"
@@ -159,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)
@@ -174,7 +176,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
{ {
m_collapsed = !m_collapsed; m_collapsed = !m_collapsed;
m_scrollArea->setVisible(!m_collapsed); m_scrollArea->setVisible(!m_collapsed);
refit(); invalidateLayout();
} }
event->accept(); event->accept();
} }
@@ -241,7 +243,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
} }
m_scrollArea->setVisible(!m_collapsed); m_scrollArea->setVisible(!m_collapsed);
refit(); invalidateLayout();
} }
QString ControlsPanel::getHeadingText(const ControlContext& context) const QString ControlsPanel::getHeadingText(const ControlContext& context) const
@@ -277,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
@@ -290,7 +292,7 @@ void ControlsPanel::refit()
m_rowsLayout->invalidate(); m_rowsLayout->invalidate();
m_rowsLayout->activate(); m_rowsLayout->activate();
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx); const QRect band = viewRect.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 // Measured from the heading and the rows directly rather than from the panel's own
@@ -314,21 +316,17 @@ void ControlsPanel::refit()
// 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).
int widthPx = qMin(contentWidthPx + chromePx, band.width()); int widthPx = qMin(contentWidthPx + chromePx, band.width());
int heightPx = qMin(wantedHeightPx, 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);
const int availablePx = m_buildBarRect.top() - kMarginPx - band.top(); int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
heightPx = qMin(heightPx, qMax(0, availablePx)); int topPx = bottomPx - heightPx + 1;
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
}
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its // Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
// own width, or it would appear over the labels. // own width, or it would appear over the labels.

View File

@@ -7,6 +7,7 @@
#include <QWidget> #include <QWidget>
#include "ControlAction.h" #include "ControlAction.h"
#include "FloatingPanel.h"
class GameWorldView; class GameWorldView;
class QLabel; class QLabel;
@@ -29,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
@@ -42,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).
@@ -59,8 +61,8 @@ 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;
@@ -82,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
View 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;
};

View File

@@ -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();

View File

@@ -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);

View File

@@ -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)

View File

@@ -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
View 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(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(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(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();
}

50
src/ui/SelectionBounds.h Normal file
View File

@@ -0,0 +1,50 @@
#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. The admin is taken by
// non-const reference only because EntityAdmin::hasAll() is not const; nothing here
// writes to it.
std::optional<QRectF> getActorWidgetRect(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(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(Simulation& sim, const WorldCoordinates& coordinates,
const std::vector<BuildingId>& buildings,
const std::vector<entt::entity>& actors,
const std::vector<entt::entity>& debris);

View File

@@ -5,6 +5,9 @@
#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 +16,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'
@@ -94,10 +97,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)
@@ -113,6 +116,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)
{ {
@@ -178,7 +193,7 @@ void SelectionPanel::refreshContent()
} }
m_content->refresh(); m_content->refresh();
updateVisibility(); invalidateLayout();
} }
void SelectionPanel::rebuildContent() void SelectionPanel::rebuildContent()
@@ -208,36 +223,37 @@ void SelectionPanel::rebuildContent()
m_content->show(); 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);
{
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
// from contentsMargins() because the stylesheet box is what sets it, and asking the // from contentsMargins() because the stylesheet box is what sets it, and asking the
// 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;
} }
@@ -273,9 +289,35 @@ void SelectionPanel::refit()
// First at the cap, the most room the card can ever get, to learn how wide it // 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. // wants to be; then at that width for the height that follows from it.
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx); 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);
}
// 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)
{
return;
}
int contentHeightPx = measureAt(contentWidthPx).height(); int contentHeightPx = measureAt(contentWidthPx).height();
// A card taller than the band is capped there and scrolls // 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 // (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 -- // 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 // and where the cap does not allow that, the card is measured again at what is
@@ -301,11 +343,8 @@ void SelectionPanel::refit()
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);
} }
} }

View File

@@ -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;

View File

@@ -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"
@@ -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));

View File

@@ -139,11 +139,6 @@ 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 // Non-const only because EntityAdmin's component accessors are; the renderer