Compare commits
24 Commits
ui_changes
...
37378e3c1b
| Author | SHA1 | Date | |
|---|---|---|---|
| 37378e3c1b | |||
| 37a7a7499a | |||
| 5794a51c9a | |||
| 09858f4b62 | |||
| ca49b9dbec | |||
| 79650ae211 | |||
| c0b009c548 | |||
| 7ae5f8c4dc | |||
| 9f574fa05e | |||
| e5d689c8d8 | |||
| 0b8f5eccd6 | |||
| 40415e663b | |||
| 7a098edf72 | |||
| 365641feb7 | |||
| 4b5e288632 | |||
| cab7244b00 | |||
| 980fad272f | |||
| 68a43a7d98 | |||
| 1f754de431 | |||
| f61f0bf761 | |||
| 075e44d295 | |||
| 2289277e12 | |||
| cc0ef856e3 | |||
| 603f8063e9 |
@@ -109,40 +109,40 @@ glyph = "E"
|
|||||||
# --- ores ---
|
# --- ores ---
|
||||||
|
|
||||||
[items.iron_ore]
|
[items.iron_ore]
|
||||||
fill = "#8a5a4a"
|
fill = "#47271c"
|
||||||
outline = "#201010"
|
outline = "#a65b42"
|
||||||
|
|
||||||
[items.copper_ore]
|
[items.copper_ore]
|
||||||
fill = "#c47a3a"
|
fill = "#deb592"
|
||||||
outline = "#3a1a0a"
|
outline = "#826a56"
|
||||||
|
|
||||||
[items.quartz]
|
[items.quartz]
|
||||||
fill = "#e0d4f0"
|
fill = "#4f3d66"
|
||||||
outline = "#40345a"
|
outline = "#9776c4"
|
||||||
|
|
||||||
# --- smelted basics ---
|
# --- smelted basics ---
|
||||||
|
|
||||||
[items.iron_ingot]
|
[items.iron_ingot]
|
||||||
fill = "#b0b0b8"
|
fill = "#535357"
|
||||||
outline = "#202028"
|
outline = "#97979e"
|
||||||
|
|
||||||
[items.copper_ingot]
|
[items.copper_ingot]
|
||||||
fill = "#d48a4a"
|
fill = "#663810"
|
||||||
outline = "#402010"
|
outline = "#b07746"
|
||||||
|
|
||||||
[items.silicon]
|
[items.silicon]
|
||||||
fill = "#33415e"
|
fill = "#737f99"
|
||||||
outline = "#0e1420"
|
outline = "#373f4f"
|
||||||
|
|
||||||
# --- salvage loop ---
|
# --- salvage loop ---
|
||||||
|
|
||||||
[items.scrap]
|
[items.scrap]
|
||||||
fill = "#7a7268"
|
fill = "#998e81"
|
||||||
outline = "#201a14"
|
outline = "#574b3d"
|
||||||
|
|
||||||
[items.voidsteel]
|
[items.voidsteel]
|
||||||
fill = "#4a3a6a"
|
fill = "#82759c"
|
||||||
outline = "#151020"
|
outline = "#3f374f"
|
||||||
|
|
||||||
# --- basic components ---
|
# --- basic components ---
|
||||||
|
|
||||||
@@ -151,106 +151,106 @@ fill = "#e09a50"
|
|||||||
outline = "#3a2008"
|
outline = "#3a2008"
|
||||||
|
|
||||||
[items.steel_plate]
|
[items.steel_plate]
|
||||||
fill = "#8a92a0"
|
fill = "#202836"
|
||||||
outline = "#22262c"
|
outline = "#54698c"
|
||||||
|
|
||||||
[items.copper_coil]
|
[items.copper_coil]
|
||||||
fill = "#d07030"
|
fill = "#cca287"
|
||||||
outline = "#381808"
|
outline = "#755d4d"
|
||||||
|
|
||||||
[items.building_block]
|
[items.building_block]
|
||||||
fill = "#c8b070"
|
fill = "#544724"
|
||||||
outline = "#302810"
|
outline = "#a18845"
|
||||||
|
|
||||||
# --- advanced components ---
|
# --- advanced components ---
|
||||||
|
|
||||||
[items.control_chip]
|
[items.control_chip]
|
||||||
fill = "#2ea35a"
|
fill = "#74b08b"
|
||||||
outline = "#0a2a14"
|
outline = "#42634e"
|
||||||
|
|
||||||
[items.capacitor_bank]
|
[items.capacitor_bank]
|
||||||
fill = "#d0a030"
|
fill = "#d0a030"
|
||||||
outline = "#302408"
|
outline = "#302408"
|
||||||
|
|
||||||
[items.hardened_steel]
|
[items.hardened_steel]
|
||||||
fill = "#6a7280"
|
fill = "#818999"
|
||||||
outline = "#181c22"
|
outline = "#3e4859"
|
||||||
|
|
||||||
[items.ceramic_plate]
|
[items.ceramic_plate]
|
||||||
fill = "#e0d8c8"
|
fill = "#e0d8c8"
|
||||||
outline = "#3a3428"
|
outline = "#3a3428"
|
||||||
|
|
||||||
[items.drive_unit]
|
[items.drive_unit]
|
||||||
fill = "#4a6ad0"
|
fill = "#92a4de"
|
||||||
outline = "#101a38"
|
outline = "#545f80"
|
||||||
|
|
||||||
# --- capital components ---
|
# --- capital components ---
|
||||||
|
|
||||||
[items.voidsteel_plate]
|
[items.voidsteel_plate]
|
||||||
fill = "#7a5aaa"
|
fill = "#9986b5"
|
||||||
outline = "#1c1038"
|
outline = "#544766"
|
||||||
|
|
||||||
[items.capital_core]
|
[items.capital_core]
|
||||||
fill = "#b040d0"
|
fill = "#420c52"
|
||||||
outline = "#280c30"
|
outline = "#9444ab"
|
||||||
|
|
||||||
# --- module items ---
|
# --- module items ---
|
||||||
|
|
||||||
[items.railgun_s_module]
|
[items.railgun_s_module]
|
||||||
fill = "#691313"
|
fill = "#bf7e7e"
|
||||||
outline = "#f3ff4f"
|
outline = "#664343"
|
||||||
|
|
||||||
[items.railgun_m_module]
|
[items.railgun_m_module]
|
||||||
fill = "#892020"
|
fill = "#b07474"
|
||||||
outline = "#f3ff4f"
|
outline = "#593b3b"
|
||||||
|
|
||||||
[items.railgun_l_module]
|
[items.railgun_l_module]
|
||||||
fill = "#a92d2d"
|
fill = "#b07474"
|
||||||
outline = "#f3ff4f"
|
outline = "#593b3b"
|
||||||
|
|
||||||
[items.salvager_module]
|
[items.salvager_module]
|
||||||
fill = "#b2cfdd"
|
fill = "#b2cfdd"
|
||||||
outline = "#236137"
|
outline = "#236137"
|
||||||
|
|
||||||
[items.repair_tool_module]
|
[items.repair_tool_module]
|
||||||
fill = "#2e9ba3"
|
fill = "#0b4347"
|
||||||
outline = "#689275"
|
outline = "#38868c"
|
||||||
|
|
||||||
[items.armor_plates_module]
|
[items.armor_plates_module]
|
||||||
fill = "#808080"
|
fill = "#999999"
|
||||||
outline = "#202020"
|
outline = "#545454"
|
||||||
|
|
||||||
[items.sensor_booster_module]
|
[items.sensor_booster_module]
|
||||||
fill = "#40a0ff"
|
fill = "#a0c9f2"
|
||||||
outline = "#102840"
|
outline = "#607991"
|
||||||
|
|
||||||
[items.maneuvering_thrusters_module]
|
[items.maneuvering_thrusters_module]
|
||||||
fill = "#5090e0"
|
fill = "#92b4de"
|
||||||
outline = "#142438"
|
outline = "#566982"
|
||||||
|
|
||||||
[items.afterburner_module]
|
[items.afterburner_module]
|
||||||
fill = "#6080c0"
|
fill = "#1d2e52"
|
||||||
outline = "#182030"
|
outline = "#486db5"
|
||||||
|
|
||||||
[items.weapon_upgrade_module]
|
[items.weapon_upgrade_module]
|
||||||
fill = "#ff4040"
|
fill = "#ff4040"
|
||||||
outline = "#401010"
|
outline = "#401010"
|
||||||
|
|
||||||
[items.weapon_primer_module]
|
[items.weapon_primer_module]
|
||||||
fill = "#e03838"
|
fill = "#e69797"
|
||||||
outline = "#380e0e"
|
outline = "#855858"
|
||||||
|
|
||||||
[items.weapon_stabilizer_module]
|
[items.weapon_stabilizer_module]
|
||||||
fill = "#c03030"
|
fill = "#c78383"
|
||||||
outline = "#300c0c"
|
outline = "#6e4848"
|
||||||
|
|
||||||
[items.drone_bay_module]
|
[items.drone_bay_module]
|
||||||
fill = "#cc66ff"
|
fill = "#cc66ff"
|
||||||
outline = "#331040"
|
outline = "#331040"
|
||||||
|
|
||||||
[items.drone_hangar_module]
|
[items.drone_hangar_module]
|
||||||
fill = "#9933cc"
|
fill = "#8c689e"
|
||||||
outline = "#260c33"
|
outline = "#42314a"
|
||||||
|
|
||||||
# --- ship hulls (outline matches the ship's fleet color in [ships.*]) ---
|
# --- ship hulls (outline matches the ship's fleet color in [ships.*]) ---
|
||||||
|
|
||||||
@@ -283,8 +283,8 @@ fill = "#1b1b1b"
|
|||||||
outline = "#ff5533"
|
outline = "#ff5533"
|
||||||
|
|
||||||
[items.carrier_hull]
|
[items.carrier_hull]
|
||||||
fill = "#1b1b1b"
|
fill = "#310f42"
|
||||||
outline = "#cc66ff"
|
outline = "#8542a6"
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Ships
|
# Ships
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 40
|
height_tiles = 30
|
||||||
refund_percentage = 100
|
refund_percentage = 100
|
||||||
deconstruction_time_seconds = 0.1
|
deconstruction_time_seconds = 0.1
|
||||||
starting_building_blocks = 200
|
starting_building_blocks = 200
|
||||||
@@ -15,7 +15,7 @@ building_blocks_tooltip = "Building blocks are the currency for construction. Sp
|
|||||||
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
|
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 60
|
asteroid_width_tiles = 40
|
||||||
player_buffer_width_tiles = 20
|
player_buffer_width_tiles = 20
|
||||||
contest_zone_width_tiles = 60
|
contest_zone_width_tiles = 60
|
||||||
enemy_buffer_width_tiles = 20
|
enemy_buffer_width_tiles = 20
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
## Game World
|
## Game World
|
||||||
|
|
||||||
- REQ-GW-COORDS: Tile coordinates are integer `(x, y)`. The origin `(0, 0)` is the first column of space — the tile immediately to the right of the asteroid's right edge at game start, at the top of the world. X grows right; Y grows down. All asteroid tiles have `x < 0`; asteroid left-expansions add tiles at increasingly negative X. The origin never shifts.
|
- REQ-GW-COORDS: Tile coordinates are integer `(x, y)`. The origin `(0, 0)` is the first column of space — the tile immediately to the right of the asteroid's right edge at game start, at the top of the world. X grows right; Y grows down. All asteroid tiles have `x < 0`; asteroid left-expansions add tiles at increasingly negative X. The origin never shifts.
|
||||||
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size (drawn as an item icon, or a colored square as the fallback — REQ-UI-ITEM-ICON); when multiple items occupy the same tile they are spaced quarter-tile apart along the direction of travel and overlap, rendered in ascending order of progress — the least-progressed item is drawn first (bottom) and the furthest-progressed item is drawn last (on top). Items emerging from a building's output port are rendered by these same rules on that port's output belt (REQ-MAT-OUTPUT-EMERGE).
|
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size (drawn as a colored square carrying the item's icon, or the square alone when the item has no icon — REQ-UI-ITEM-ICON); when multiple items occupy the same tile they are spaced quarter-tile apart along the direction of travel and overlap, rendered in ascending order of progress — the least-progressed item is drawn first (bottom) and the furthest-progressed item is drawn last (on top). Items emerging from a building's output port are rendered by these same rules on that port's output belt (REQ-MAT-OUTPUT-EMERGE).
|
||||||
- REQ-GW-BELT-CAPACITY: Belt tiles and tunnel entry/exit tiles each hold up to four items simultaneously, queued one behind the other in the direction of travel. Splitter tiles hold up to four items: two unassigned items (progress < 0.5, not yet routed to an output) and one item per output slot (progress ≥ 0.5, committed to a specific output direction). Output-slot items are rendered on top of unassigned items; when both output slots are occupied, their rendering order follows the clockwise port order starting from East.
|
- REQ-GW-BELT-CAPACITY: Belt tiles and tunnel entry/exit tiles each hold up to four items simultaneously, queued one behind the other in the direction of travel. Splitter tiles hold up to four items: two unassigned items (progress < 0.5, not yet routed to an output) and one item per output slot (progress ≥ 0.5, committed to a specific output direction). Output-slot items are rendered on top of unassigned items; when both output slots are occupied, their rendering order follows the clockwise port order starting from East.
|
||||||
- REQ-GW-BELT-SPEED: Items on belts move at `world.toml [world].belt_speed_tiles_per_second` tiles per second (default 2).
|
- REQ-GW-BELT-SPEED: Items on belts move at `world.toml [world].belt_speed_tiles_per_second` tiles per second (default 2).
|
||||||
- REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`.
|
- REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`.
|
||||||
@@ -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) 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):
|
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 — or wherever the player has dragged it by its header (REQ-UI-SELECTION-PANEL-DRAG) — 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):
|
||||||
|
|
||||||
```
|
```
|
||||||
+-----------------------------------------------------------+
|
+-----------------------------------------------------------+
|
||||||
@@ -460,11 +460,18 @@ The screen is a single column: a header bar across the top and the game world vi
|
|||||||
- **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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
- **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.
|
- **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 player may override the resulting position by dragging the panel's header (REQ-UI-SELECTION-PANEL-DRAG); the dragged position then takes the anchor rectangle's and the side's place for the rest of that selection. 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.
|
- **Resizing in place.** Only the anchor rectangle and the chosen side are fixed for the life of the selection (or, once the panel has been dragged, the dragged desired position — REQ-UI-SELECTION-PANEL-DRAG); 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 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).
|
- **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). Beside the controls its content offers, the panel's own chrome offers one gesture: the header drag that moves it (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
|
- REQ-UI-SELECTION-PANEL-DRAG: **Moving the panel by its header.** The player can move the selection panel by pressing the left mouse button on the panel's **header** (REQ-UI-SELECTION-CARD) and dragging: the panel follows the cursor for the duration of the drag and stays where it is dropped on release. The header is the whole drag handle, and no other part of the panel starts a drag.
|
||||||
|
- **Desired position, not resolved position.** A drag sets only the panel's **desired top-left corner** in view coordinates. Where the panel actually lands is resolved from that desired position by the rules of REQ-UI-SELECTION-PANEL, exactly as an anchor-derived position is: the panel keeps its margin from the view's edges; its bottom is limited by the top edge, less that margin, of the build button bar (REQ-UI-BUILD-BAR) and of the controls panel (REQ-UI-CONTROLS-PANEL), but each only where the panel's own horizontal extent actually overlaps that widget's current rectangle; and a panel that does not fit above that limit is shifted up as far as the view's top margin and, failing that, capped in height with its content scrolling. The player therefore cannot park the panel over either widget, and neither widget ever moves on the panel's account (REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL) — stepping around them stays entirely the panel's job.
|
||||||
|
- **The desired position survives the resolution.** Resolving does not overwrite what the player set: the desired position is retained as dropped, so a panel that had to be shifted up or shortened returns to it as soon as the obstruction stops overlapping it — its content shrinks, the bar's button set changes (REQ-LOCK-BUILDING), the controls panel's context changes, or the view is resized. A desired position that the current view cannot honour at all is likewise kept, so enlarging the window brings the panel back to it.
|
||||||
|
- **What the drag replaces.** From the first drag on, the desired position replaces the anchor rectangle and the side (REQ-UI-SELECTION-PANEL) for the rest of the current selection; the panel no longer has a side and never switches to one. Re-solving (the **Resizing in place** rule of REQ-UI-SELECTION-PANEL) then keeps the top and left edges of the desired position, in place of the top edge and the edge facing the anchor, so the panel still grows away from where the player put it rather than over it. The panel may be dragged repeatedly; each drag replaces the previous desired position.
|
||||||
|
- **Scope: the current selection.** The desired position lasts as long as the selection it was set in — across the panel's own resizing, view resizes, and view scrolling (REQ-UI-SELECTION-PANEL), and across the selection being expanded or reduced (REQ-UI-MULTI-SELECT). Starting a **new** selection discards it: the panel is placed anew against the new anchor rectangle (REQ-UI-SELECTION-PANEL), and the player drags it again if they want it elsewhere.
|
||||||
|
- **Input.** The drag consumes its mouse events like every other event over the panel (REQ-UI-SELECTION-PANEL): the press, the movement, and the release never reach the game world, so dragging the header neither box-selects (REQ-UI-MULTI-SELECT) nor places belts (REQ-BLD-BELT-DRAG). The drag continues while the cursor moves outside the panel or outside the view, and ends when the left button is released, wherever that happens. A press and release on the header without movement moves nothing and has no other effect.
|
||||||
|
- **Presentation only.** Moving the panel is not a player command: it never enters the replay stream and has no effect on the simulation, consistent with the controls panel's collapsed state (REQ-UI-CONTROLS-PANEL). The desired position is not saved to disk.
|
||||||
- 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.
|
||||||
|
|
||||||
### Game World
|
### Game World
|
||||||
@@ -477,7 +484,7 @@ The screen is a single column: a header bar across the top and the game world vi
|
|||||||
|
|
||||||
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
|
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
|
||||||
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
|
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
|
||||||
- REQ-UI-ITEM-ICON: In the game world, an item is drawn with its **item icon** in place of the colored square of REQ-GW-TILE-SIZE. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). It fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE, and applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file falls back to its `visuals.toml` colored square (`fill` + `outline`); a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
|
- REQ-UI-ITEM-ICON: In the game world, an item is drawn as its `visuals.toml` colored square (`fill` + `outline`, the square of REQ-GW-TILE-SIZE) carrying its **item icon** on top. The square is drawn for every item, with or without an icon: it is what gives the item contrast against the tile beneath it, and its outline is what separates neighbouring items where they overlap on a belt. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). The square fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE; the icon is drawn **inset** within that rect so a frame of the square's color stays visible all around it — required because each icon's viewBox is cropped tight to its artwork, so an icon drawn at the full rect would cover the square entirely. This applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file shows the colored square alone; a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. The colored square is a game-world treatment only: the item chips and selection dialogs of the UI panels (REQ-UI-SINGLE-SELECTION, REQ-UI-SELECT-BUTTON) show the icon without it. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
|
||||||
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
||||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||||
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
|
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
|
||||||
@@ -538,7 +545,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or piece of debris), the selection panel is not shown at all — it is hidden rather than shown empty, so the full game world view is visible (REQ-UI-SELECTION-PANEL). It reappears as soon as an object is selected.
|
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or piece of debris), the selection panel is not shown at all — it is hidden rather than shown empty, so the full game world view is visible (REQ-UI-SELECTION-PANEL). It reappears as soon as an object is selected.
|
||||||
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with debris). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus debris, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident piece of debris (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT).
|
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with debris). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus debris, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident piece of debris (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
- REQ-UI-SELECTION-CARD: **Card structure.** Every panel content is a card with the same three parts, top to bottom:
|
- REQ-UI-SELECTION-CARD: **Card structure.** Every panel content is a card with the same three parts, top to bottom:
|
||||||
- **Header** — always shown. It holds the selection's identity symbol on the left — the building's icon glyph (REQ-UI-WORLD-ICON), a ship's schematic color swatch, or the kind symbol of a defence station or piece of debris — the selection's name beside it, and one optional **right slot**. The right slot holds a status indicator (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an object count — never more than one of them; which one applies is stated per content in REQ-UI-SELECTION-CONTENT.
|
- **Header** — always shown. It holds the selection's identity symbol on the left — the building's icon glyph (REQ-UI-WORLD-ICON), a ship's schematic color swatch, or the kind symbol of a defence station or piece of debris — the selection's name beside it, and one optional **right slot**. The right slot holds a status indicator (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an object count — never more than one of them; which one applies is stated per content in REQ-UI-SELECTION-CONTENT. The header carries no control of its own, and doubles as the panel's drag handle (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
- **Configuration group** — the controls that change how the selected object is set up: the recipe/schematic selection control (REQ-UI-SELECT-BUTTON), a shipyard's layout preview and Configure button (REQ-MOD-UI-PREVIEW), and a splitter's output filters (REQ-BLD-SPLITTER). It is shown identically for an operational building and for a construction site of the same type (REQ-BLD-SITE-CONFIG).
|
- **Configuration group** — the controls that change how the selected object is set up: the recipe/schematic selection control (REQ-UI-SELECT-BUTTON), a shipyard's layout preview and Configure button (REQ-MOD-UI-PREVIEW), and a splitter's output filters (REQ-BLD-SPLITTER). It is shown identically for an operational building and for a construction site of the same type (REQ-BLD-SITE-CONFIG).
|
||||||
- **Runtime group** — what the object is currently doing: buffer contents, production progress, HP, remaining scrap, and the belt clear action (REQ-UI-BELT-CLEAR). Where the object has **HP**, its bar is the first thing in this group, above everything else the card shows (REQ-UI-HQ-PANEL, REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL) — how close the thing is to dying outranks what it is holding. For a **construction site** the entire runtime group is replaced by a captioned `Construction` section: a progress bar filled to the site's construction completion with that completion as an integer percentage beside the caption — the same value the world draws on the footprint (REQ-UI-CONSTRUCTION-PROGRESS) — followed by a note that buffers appear once the building is built, because a site has neither buffers nor a production cycle (REQ-BLD-SITE-CONFIG). That section sits **directly below the header, above the configuration group**, so how far along the site is reads first; the configuration group is otherwise unaffected and stays visible on a site.
|
- **Runtime group** — what the object is currently doing: buffer contents, production progress, HP, remaining scrap, and the belt clear action (REQ-UI-BELT-CLEAR). Where the object has **HP**, its bar is the first thing in this group, above everything else the card shows (REQ-UI-HQ-PANEL, REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL) — how close the thing is to dying outranks what it is holding. For a **construction site** the entire runtime group is replaced by a captioned `Construction` section: a progress bar filled to the site's construction completion with that completion as an integer percentage beside the caption — the same value the world draws on the footprint (REQ-UI-CONSTRUCTION-PROGRESS) — followed by a note that buffers appear once the building is built, because a site has neither buffers nor a production cycle (REQ-BLD-SITE-CONFIG). That section sits **directly below the header, above the configuration group**, so how far along the site is reads first; the configuration group is otherwise unaffected and stays visible on a site.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,39 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The part every placement shares, whatever put the panel where it wants to be: it is
|
||||||
|
// pushed inside band rather than hanging off it, shortened to what its own column has
|
||||||
|
// free, and lifted by however much of it hangs below that. wantedLeftPx and wantedTopPx
|
||||||
|
// are where the panel would stand if nothing were in the way.
|
||||||
|
QRect fitInBand(const QRect& band, int wantedLeftPx, int wantedTopPx, QSize wantedSize,
|
||||||
|
const std::vector<QRect>& occupiedRects, int marginPx)
|
||||||
|
{
|
||||||
|
const int widthPx = std::min(wantedSize.width(), band.width());
|
||||||
|
|
||||||
|
int leftPx = std::min(wantedLeftPx, 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));
|
||||||
|
|
||||||
|
// Never above the band: a panel taller than the space left is capped instead, and
|
||||||
|
// scrolls.
|
||||||
|
int topPx = std::max(wantedTopPx, band.top());
|
||||||
|
topPx = std::min(topPx, bottomPx - heightPx + 1);
|
||||||
|
topPx = std::max(topPx, band.top());
|
||||||
|
|
||||||
|
return QRect(leftPx, topPx, widthPx, heightPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
int leftPx, int rightPx, int marginPx)
|
int leftPx, int rightPx, int marginPx)
|
||||||
{
|
{
|
||||||
@@ -51,26 +84,27 @@ QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide si
|
|||||||
const int widthPx = std::min(wantedSize.width(), band.width());
|
const int widthPx = std::min(wantedSize.width(), band.width());
|
||||||
|
|
||||||
// Against the anchor on the chosen side, growing away from it: the edge facing the
|
// 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.
|
// selection is the one that stays put as the panel's content resizes. A panel that
|
||||||
int leftPx = (side == PanelSide::Right) ? anchorRect.right() + marginPx + 1
|
// 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.
|
||||||
|
const int wantedLeftPx = (side == PanelSide::Right)
|
||||||
|
? anchorRect.right() + marginPx + 1
|
||||||
: anchorRect.left() - marginPx - widthPx;
|
: 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
|
// 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,
|
// free.
|
||||||
// and scrolls.
|
return fitInBand(band, wantedLeftPx, anchorRect.top(), wantedSize, occupiedRects,
|
||||||
int topPx = std::max(anchorRect.top(), band.top());
|
marginPx);
|
||||||
topPx = std::min(topPx, bottomPx - heightPx + 1);
|
}
|
||||||
topPx = std::max(topPx, band.top());
|
|
||||||
|
QRect placeAtDesiredTopLeft(const QRect& band, const QPoint& desiredTopLeftPx,
|
||||||
return QRect(leftPx, topPx, widthPx, heightPx);
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
// Where the player dropped it, resolved by the same rules as any other placement
|
||||||
|
// (REQ-UI-SELECTION-PANEL-DRAG). Nothing here is written back to the desired point:
|
||||||
|
// a panel lifted above the build button bar returns to where it was dropped as soon
|
||||||
|
// as the bar stops meeting its column.
|
||||||
|
return fitInBand(band, desiredTopLeftPx.x(), desiredTopLeftPx.y(), wantedSize,
|
||||||
|
occupiedRects, marginPx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
|
|
||||||
@@ -41,3 +42,13 @@ PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
|||||||
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
int marginPx);
|
int marginPx);
|
||||||
|
|
||||||
|
// Where a panel of wantedSize stands once the player has dragged it to desiredTopLeftPx:
|
||||||
|
// at that point, by the same rules that place it beside a selection -- pushed inside band,
|
||||||
|
// lifted above whatever occupies its column, and capped in height where that leaves too
|
||||||
|
// little room (REQ-UI-SELECTION-PANEL-DRAG). The desired point is an input only: it is
|
||||||
|
// never corrected and handed back, which is what lets the caller keep it exactly as
|
||||||
|
// dropped and return to it once the room is there again.
|
||||||
|
QRect placeAtDesiredTopLeft(const QRect& band, const QPoint& desiredTopLeftPx,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx);
|
||||||
|
|||||||
@@ -154,3 +154,78 @@ TEST_CASE("A panel that fits on neither side is pushed inside the view", "[layou
|
|||||||
kMarginPx);
|
kMarginPx);
|
||||||
REQUIRE(placed == QRect(700, 100, 300, 200));
|
REQUIRE(placed == QRect(700, 100, 300, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Where it stands once the player has dragged it (REQ-UI-SELECTION-PANEL-DRAG)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("A dragged panel stands where it was dropped", "[layout]")
|
||||||
|
{
|
||||||
|
// With room for it there, the desired top-left corner is the answer: nothing about
|
||||||
|
// the selection it describes is consulted any more.
|
||||||
|
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(420, 180),
|
||||||
|
QSize(300, 200), {}, kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(420, 180, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel dragged past the view's edges is pushed back inside", "[layout]")
|
||||||
|
{
|
||||||
|
// Dropped off the right edge and below the bottom, it is brought back within the
|
||||||
|
// band, as an anchored panel is.
|
||||||
|
REQUIRE(placeAtDesiredTopLeft(makeBand(), QPoint(900, 500), QSize(300, 200), {},
|
||||||
|
kMarginPx)
|
||||||
|
== QRect(700, 400, 300, 200));
|
||||||
|
|
||||||
|
// And off the left edge and above the top, the other way.
|
||||||
|
REQUIRE(placeAtDesiredTopLeft(makeBand(), QPoint(-120, -60), QSize(300, 200), {},
|
||||||
|
kMarginPx)
|
||||||
|
== QRect(0, 0, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A dragged panel rises above the widget it was dropped over", "[layout]")
|
||||||
|
{
|
||||||
|
// The player drops it over the build button bar; it lands above the bar's top by the
|
||||||
|
// margin instead, the bar staying where it is (REQ-UI-BUILD-BAR).
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
|
||||||
|
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(450, 480),
|
||||||
|
QSize(300, 200), occupied, kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(450, 312, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget beside a dragged panel's column does not move it", "[layout]")
|
||||||
|
{
|
||||||
|
// Only what the panel's own column meets is in its way: the controls panel in the
|
||||||
|
// bottom-left corner leaves a panel dropped at the right edge alone
|
||||||
|
// (REQ-UI-CONTROLS-PANEL).
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 300, 260, 300) };
|
||||||
|
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(600, 380),
|
||||||
|
QSize(300, 200), occupied, kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(600, 380, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A dragged panel too tall for the room left is capped", "[layout]")
|
||||||
|
{
|
||||||
|
// Capping is the caller's cue to scroll here too: a card asking for 700 px over a bar
|
||||||
|
// that leaves 512 px of band gets what there is, from the top of the band.
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
|
||||||
|
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(450, 100),
|
||||||
|
QSize(300, 700), occupied, kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(450, 0, 300, 512));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A dragged panel returns to where it was dropped", "[layout]")
|
||||||
|
{
|
||||||
|
// The resolution never writes back to the desired position: a panel lifted above the
|
||||||
|
// build button bar goes back to the point the player dropped it at as soon as the bar
|
||||||
|
// stops meeting its column -- here because the bar's button set shrank.
|
||||||
|
const QPoint desired(450, 380);
|
||||||
|
const std::vector<QRect> wideBar = { QRect(400, 520, 200, 72) };
|
||||||
|
REQUIRE(placeAtDesiredTopLeft(makeBand(), desired, QSize(300, 200), wideBar,
|
||||||
|
kMarginPx)
|
||||||
|
== QRect(450, 312, 300, 200));
|
||||||
|
|
||||||
|
const std::vector<QRect> narrowBar = { QRect(400, 520, 40, 72) };
|
||||||
|
REQUIRE(placeAtDesiredTopLeft(makeBand(), desired, QSize(300, 200), narrowBar,
|
||||||
|
kMarginPx)
|
||||||
|
== QRect(450, 380, 300, 200));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "SelectionPanel.h"
|
#include "SelectionPanel.h"
|
||||||
|
|
||||||
#include <QLayout>
|
#include <QMouseEvent>
|
||||||
#include <QList>
|
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -105,6 +104,56 @@ void SelectionPanel::invalidateLayout()
|
|||||||
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectionPanel::mousePressEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
// Only the card's header moves the panel (REQ-UI-SELECTION-PANEL-DRAG). Presses on
|
||||||
|
// the header's labels arrive here by ordinary propagation, none of them being a
|
||||||
|
// widget that accepts mouse events, and carry a position in this panel's coordinates.
|
||||||
|
if (event->button() == Qt::LeftButton && m_content != nullptr)
|
||||||
|
{
|
||||||
|
QWidget* header = m_content->getHeaderWidget();
|
||||||
|
QRect headerRect(header->mapTo(this, QPoint(0, 0)), header->size());
|
||||||
|
// A card long enough to scroll can have its header scrolled out of sight; what is
|
||||||
|
// hidden is not a handle, and the pixels it would claim show other parts of the
|
||||||
|
// card.
|
||||||
|
QWidget* viewport = m_scrollArea->viewport();
|
||||||
|
headerRect &= QRect(viewport->mapTo(this, QPoint(0, 0)), viewport->size());
|
||||||
|
if (headerRect.contains(event->pos()))
|
||||||
|
{
|
||||||
|
m_dragGrabOffsetPx = event->pos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepted whether or not it started a drag: no mouse event over the panel reaches
|
||||||
|
// the game world (REQ-UI-SELECTION-PANEL).
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionPanel::mouseMoveEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (m_dragGrabOffsetPx.has_value() && parentWidget() != nullptr)
|
||||||
|
{
|
||||||
|
// Read from the cursor's position on the screen rather than from the panel's own
|
||||||
|
// coordinates, which move under the cursor as the drag places the panel again.
|
||||||
|
const QPoint topLeftPx =
|
||||||
|
parentWidget()->mapFromGlobal(event->globalPos()) - *m_dragGrabOffsetPx;
|
||||||
|
m_desiredTopLeftPx = topLeftPx - m_viewOriginPx;
|
||||||
|
// Where the panel actually lands follows from the desired position by the
|
||||||
|
// ordinary rules, the widgets it steps around included (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
|
invalidateLayout();
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionPanel::mouseReleaseEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
// Qt's implicit grab has kept the drag alive while the cursor was outside the panel
|
||||||
|
// or outside the view; it ends here, wherever the button was let go
|
||||||
|
// (REQ-UI-SELECTION-PANEL-DRAG). The desired position stays as dropped.
|
||||||
|
m_dragGrabOffsetPx.reset();
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
m_request.buildings = event->ids;
|
m_request.buildings = event->ids;
|
||||||
@@ -128,6 +177,13 @@ void SelectionPanel::handleEvent(
|
|||||||
// panel is placed, the two widgets being siblings in the same parent.
|
// panel is placed, the two widgets being siblings in the same parent.
|
||||||
m_anchorRect = event->rectPx;
|
m_anchorRect = event->rectPx;
|
||||||
m_side.reset();
|
m_side.reset();
|
||||||
|
// A position the player dragged the panel to belongs to the selection it was set in.
|
||||||
|
// A new selection places the panel anew against its own anchor
|
||||||
|
// (REQ-UI-SELECTION-PANEL-DRAG); this event is published only when one starts, and
|
||||||
|
// not when an existing selection is added to or reduced, which is exactly the scope
|
||||||
|
// the dragged position keeps.
|
||||||
|
m_desiredTopLeftPx.reset();
|
||||||
|
m_dragGrabOffsetPx.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(
|
void SelectionPanel::handleEvent(
|
||||||
@@ -240,6 +296,11 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
|
|||||||
|
|
||||||
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
|
|
||||||
|
// What the view's coordinates and this panel's differ by, kept for the drag gesture,
|
||||||
|
// which learns of the cursor in the latter and stores its result in the former
|
||||||
|
// (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
|
m_viewOriginPx = viewRect.topLeft();
|
||||||
|
|
||||||
// The anchor is published in the world view's coordinates and this panel is placed in
|
// The anchor is published in the world view's coordinates and this panel is placed in
|
||||||
// its parent's; the two widgets are siblings, so the view's own origin is the whole
|
// 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
|
// difference. Without an anchor the panel falls back to the top-right corner, by
|
||||||
@@ -265,16 +326,9 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
|
|||||||
// laid out, does the width it reports. Measuring at whatever width the panel
|
// laid out, does the width it reports. Measuring at whatever width the panel
|
||||||
// happens to have carries the previous card's shape into this one.
|
// happens to have carries the previous card's shape into this one.
|
||||||
//
|
//
|
||||||
// Each measurement re-runs the card's layouts -- every one of them, not just the
|
// Each measurement re-runs the body layout. Cards are built and discarded whole, so
|
||||||
// body's own. Changing a label's text posts a LayoutRequest to the widget holding it
|
// its cached hint describes the card before this one until it is invalidated, and
|
||||||
// and that event is only delivered when the event loop next runs, so a layout nested
|
// re-running it is also what accounts for the parts a card hides and shows as it
|
||||||
// inside the card still reports the width of the text before the change: measuring
|
|
||||||
// here and re-measuring on the following refresh then gives two different answers,
|
|
||||||
// and the panel visibly resizes a frame after its content changed. Re-activating
|
|
||||||
// them all is what a delivered LayoutRequest would have done.
|
|
||||||
//
|
|
||||||
// Cards are built and discarded whole, so this is also what discards the previous
|
|
||||||
// card's cached hints, and what accounts for the parts a card hides and shows as it
|
|
||||||
// refreshes. The polish belongs to the same step: a freshly created chip reports an
|
// refreshes. The polish belongs to the same step: a freshly created chip reports an
|
||||||
// unstyled hint until the stylesheet has reached it, and the chips carry border and
|
// unstyled hint until the stylesheet has reached it, and the chips carry border and
|
||||||
// padding that change their size.
|
// padding that change their size.
|
||||||
@@ -282,21 +336,27 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
|
|||||||
{
|
{
|
||||||
m_body->resize(widthPx, m_body->height());
|
m_body->resize(widthPx, m_body->height());
|
||||||
m_body->ensurePolished();
|
m_body->ensurePolished();
|
||||||
|
|
||||||
// Deepest first, so no layout is re-activated from children that are themselves
|
|
||||||
// still stale. findChildren walks parents before children, hence the reverse.
|
|
||||||
const QList<QLayout*> nested = m_body->findChildren<QLayout*>();
|
|
||||||
for (QList<QLayout*>::const_reverse_iterator it = nested.rbegin();
|
|
||||||
it != nested.rend(); ++it)
|
|
||||||
{
|
|
||||||
(*it)->invalidate();
|
|
||||||
(*it)->activate();
|
|
||||||
}
|
|
||||||
m_body->layout()->invalidate();
|
m_body->layout()->invalidate();
|
||||||
m_body->layout()->activate();
|
m_body->layout()->activate();
|
||||||
return m_body->sizeHint();
|
return m_body->sizeHint();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Where a panel of that size stands: beside the selection, or at the position the
|
||||||
|
// player dragged the panel to, which replaces both the anchor and the side for the
|
||||||
|
// rest of the selection (REQ-UI-SELECTION-PANEL-DRAG). Either way the result is
|
||||||
|
// resolved against the view's edges and the widgets placed before this one, so a
|
||||||
|
// dragged panel steps around them exactly as an anchored one does.
|
||||||
|
auto solve = [&](QSize wantedSize) -> QRect
|
||||||
|
{
|
||||||
|
if (m_desiredTopLeftPx.has_value())
|
||||||
|
{
|
||||||
|
return placeAtDesiredTopLeft(band, *m_desiredTopLeftPx + viewRect.topLeft(),
|
||||||
|
wantedSize, occupiedRects, kMarginPx);
|
||||||
|
}
|
||||||
|
return placeBesideAnchor(band, anchorRect, *m_side, wantedSize, occupiedRects,
|
||||||
|
kMarginPx);
|
||||||
|
};
|
||||||
|
|
||||||
// Run twice. Parts of a card report an unstyled size until the style has actually
|
// Run twice. Parts of a card report an unstyled size until the style has actually
|
||||||
// reached them, which for a freshly built card happens during the first round of
|
// reached them, which for a freshly built card happens during the first round of
|
||||||
// measuring; the second round then measures a card that is fully laid out and
|
// measuring; the second round then measures a card that is fully laid out and
|
||||||
@@ -326,9 +386,8 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
|
|||||||
// pass settles it again against the wider column. Asking for the whole band's
|
// 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.
|
// height is what makes the answer the most the panel could have there.
|
||||||
const int maxHeightPx =
|
const int maxHeightPx =
|
||||||
placeBesideAnchor(band, anchorRect, *m_side,
|
solve(QSize(contentWidthPx + 2 * borderPx, band.height())).height()
|
||||||
QSize(contentWidthPx + 2 * borderPx, band.height()),
|
- 2 * borderPx;
|
||||||
occupiedRects, kMarginPx).height() - 2 * borderPx;
|
|
||||||
if (maxHeightPx <= 0)
|
if (maxHeightPx <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -362,8 +421,6 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
|
|||||||
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
||||||
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
||||||
|
|
||||||
setGeometry(placeBesideAnchor(band, anchorRect, *m_side,
|
setGeometry(solve(QSize(panelWidthPx, panelHeightPx)));
|
||||||
QSize(panelWidthPx, panelHeightPx),
|
|
||||||
occupiedRects, kMarginPx));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class BuildingIconCache;
|
|||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
class SelectionContent;
|
class SelectionContent;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
class QMouseEvent;
|
||||||
class QScrollArea;
|
class QScrollArea;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
@@ -64,6 +66,14 @@ public:
|
|||||||
void placeIn(const QRect& viewRect,
|
void placeIn(const QRect& viewRect,
|
||||||
const std::vector<QRect>& occupiedRects) override;
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// The panel is moved by dragging its card header (REQ-UI-SELECTION-PANEL-DRAG). Every
|
||||||
|
// other mouse event over the panel is swallowed here so that none of them reaches the
|
||||||
|
// game world beneath (REQ-UI-SELECTION-PANEL).
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent* event) 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 SelectionAnchorChangedEvent> event) override;
|
||||||
@@ -98,10 +108,23 @@ private:
|
|||||||
// scrolling view or a moving ship, the side because a card that grows must not flip
|
// 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
|
// 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
|
// first placement after a new anchor, being the first point at which the panel's
|
||||||
// width is known.
|
// width is known. Dragging the panel supersedes the pair for the rest of the
|
||||||
|
// selection (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
QRect m_anchorRect;
|
QRect m_anchorRect;
|
||||||
std::optional<PanelSide> m_side;
|
std::optional<PanelSide> m_side;
|
||||||
|
|
||||||
|
// Where the player dragged the panel, in the game world view's coordinates, and the
|
||||||
|
// cursor's offset within the panel while a drag is running
|
||||||
|
// (REQ-UI-SELECTION-PANEL-DRAG). The desired position is kept exactly as dropped:
|
||||||
|
// resolving it against the view's edges and the widgets the panel steps around never
|
||||||
|
// writes back to it, so the panel returns to it once the room is there again. It
|
||||||
|
// lasts as long as the selection it was set in.
|
||||||
|
std::optional<QPoint> m_desiredTopLeftPx;
|
||||||
|
std::optional<QPoint> m_dragGrabOffsetPx;
|
||||||
|
// Origin of the view the panel was last placed in, which is what the two coordinate
|
||||||
|
// systems differ by -- the panel is a sibling of the view, not a child of it.
|
||||||
|
QPoint m_viewOriginPx;
|
||||||
|
|
||||||
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
||||||
// The card is a 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;
|
||||||
|
|||||||
@@ -520,8 +520,8 @@ void WorldRenderer::drawPortItems(QPainter& painter, const WorldCoordinates& coo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared with belt items (REQ-GW-TILE-SIZE): a half-tile item icon, or the
|
// Shared with belt items (REQ-GW-TILE-SIZE): a half-tile colored square carrying the
|
||||||
// colored-square fallback (REQ-UI-ITEM-ICON), via the same draw path.
|
// item's icon (REQ-UI-ITEM-ICON), via the same draw path.
|
||||||
const std::function<void(const ItemType&, QPointF)> drawItem =
|
const std::function<void(const ItemType&, QPointF)> drawItem =
|
||||||
[&](const ItemType& type, QPointF worldPos)
|
[&](const ItemType& type, QPointF worldPos)
|
||||||
{
|
{
|
||||||
@@ -544,25 +544,34 @@ void WorldRenderer::drawWorldItem(QPainter& painter, const std::string& itemId,
|
|||||||
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
|
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
|
||||||
halfPx * 2, halfPx * 2);
|
halfPx * 2, halfPx * 2);
|
||||||
|
|
||||||
// Prefer the item's icon (REQ-UI-ITEM-ICON); it is rasterized once at the current
|
// The colored square from visuals.toml (REQ-GW-TILE-SIZE) backs every item, icon or
|
||||||
// half-tile pixel size and cached, so this is a plain pixmap blit per frame.
|
// not: it is what gives the item contrast against the tile beneath it, and its dark
|
||||||
if (m_itemIcons && m_itemIcons->hasIcon(itemId))
|
// outline is what separates neighbouring items where they overlap on a belt.
|
||||||
{
|
|
||||||
int sizePx = qRound(static_cast<qreal>(halfPx * 2.0f));
|
|
||||||
if (sizePx < 1) { sizePx = 1; }
|
|
||||||
painter.drawPixmap(itemRect, m_itemIcons->getPixmap(itemId, sizePx),
|
|
||||||
QRectF(0, 0, sizePx, sizePx));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: the colored square from visuals.toml (REQ-GW-TILE-SIZE).
|
|
||||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||||
m_visuals.items.find(itemId);
|
m_visuals.items.find(itemId);
|
||||||
if (it == m_visuals.items.end()) { return; }
|
if (it != m_visuals.items.end())
|
||||||
|
{
|
||||||
painter.fillRect(itemRect, it->second.fill);
|
painter.fillRect(itemRect, it->second.fill);
|
||||||
painter.setPen(QPen(it->second.outline, 1));
|
painter.setPen(QPen(it->second.outline, 1));
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
painter.drawRect(itemRect);
|
painter.drawRect(itemRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_itemIcons || !m_itemIcons->hasIcon(itemId)) { return; }
|
||||||
|
|
||||||
|
// The icon goes on top, inset so a frame of the square's color stays visible all
|
||||||
|
// around it (REQ-UI-ITEM-ICON). The inset is needed because each icon's viewBox is
|
||||||
|
// cropped tight to its artwork: drawn at the full rect, a solid icon would cover the
|
||||||
|
// square entirely. It is rasterized once at the inset pixel size and cached, so this
|
||||||
|
// is a plain pixmap blit per frame.
|
||||||
|
constexpr double kIconInsetFraction = 0.15;
|
||||||
|
const double inset = kIconInsetFraction * static_cast<double>(halfPx * 2.0f);
|
||||||
|
const QRectF iconRect = itemRect.adjusted(inset, inset, -inset, -inset);
|
||||||
|
|
||||||
|
int sizePx = qRound(iconRect.width());
|
||||||
|
if (sizePx < 1) { sizePx = 1; }
|
||||||
|
painter.drawPixmap(iconRect, m_itemIcons->getPixmap(itemId, sizePx),
|
||||||
|
QRectF(0, 0, sizePx, sizePx));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldRenderer::drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates,
|
void WorldRenderer::drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ private:
|
|||||||
void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates,
|
void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
const WorldRenderFrame& frame);
|
const WorldRenderFrame& frame);
|
||||||
// Draws a single item centered at widget-space `center`, spanning `halfPx` in
|
// Draws a single item centered at widget-space `center`, spanning `halfPx` in
|
||||||
// each direction (a half-tile). Uses the item's icon when one exists
|
// each direction (a half-tile): the colored square from visuals.toml, carrying
|
||||||
// (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from
|
// the item's icon inset on top of it when one exists (REQ-UI-ITEM-ICON).
|
||||||
// visuals.toml. Shared by drawBeltItems and drawPortItems.
|
// Shared by drawBeltItems and drawPortItems.
|
||||||
void drawWorldItem(QPainter& painter, const std::string& itemId,
|
void drawWorldItem(QPainter& painter, const std::string& itemId,
|
||||||
QPointF center, float halfPx);
|
QPointF center, float halfPx);
|
||||||
void drawPortGlyph(QPainter& painter, const WorldCoordinates& coordinates,
|
void drawPortGlyph(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
|
|||||||
@@ -32,38 +32,6 @@ const int kSymbolSizePx = 20;
|
|||||||
const int kCardSpacingPx = 6;
|
const int kCardSpacingPx = 6;
|
||||||
const int kHeaderSpacingPx = 6;
|
const int kHeaderSpacingPx = 6;
|
||||||
|
|
||||||
// The Salvage Bay has no recipe and no cycle: its two states say whether it is holding
|
|
||||||
// scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY, REQ-UI-SELECTION-STATUS).
|
|
||||||
QString getStatusCaption(ProductionStatus status, bool isSalvageBay)
|
|
||||||
{
|
|
||||||
switch (status)
|
|
||||||
{
|
|
||||||
case ProductionStatus::Unconfigured: return QObject::tr("no recipe");
|
|
||||||
case ProductionStatus::Producing:
|
|
||||||
return isSalvageBay ? QObject::tr("holding scrap")
|
|
||||||
: QObject::tr("producing");
|
|
||||||
case ProductionStatus::Starved:
|
|
||||||
return isSalvageBay ? QObject::tr("empty") : QObject::tr("missing input");
|
|
||||||
case ProductionStatus::Blocked: return QObject::tr("output full");
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every caption the status slot can end up showing for this building, so the header can
|
|
||||||
// keep its width as the state changes rather than the panel jumping with it.
|
|
||||||
QStringList getAllStatusCaptions(bool isSalvageBay)
|
|
||||||
{
|
|
||||||
QStringList captions;
|
|
||||||
for (ProductionStatus status : { ProductionStatus::Unconfigured,
|
|
||||||
ProductionStatus::Producing,
|
|
||||||
ProductionStatus::Starved,
|
|
||||||
ProductionStatus::Blocked })
|
|
||||||
{
|
|
||||||
captions << getStatusCaption(status, isSalvageBay);
|
|
||||||
}
|
|
||||||
return captions;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -82,26 +50,30 @@ SelectionContent::SelectionContent(const SelectionContext& context,
|
|||||||
|
|
||||||
// Header: identity symbol, name, and the right slot pushed to the far edge
|
// Header: identity symbol, name, and the right slot pushed to the far edge
|
||||||
// (REQ-UI-SELECTION-CARD).
|
// (REQ-UI-SELECTION-CARD).
|
||||||
QWidget* header = new QWidget(this);
|
m_header = new QWidget(this);
|
||||||
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
// The header doubles as the panel's drag handle (REQ-UI-SELECTION-PANEL-DRAG); the
|
||||||
|
// cursor says so, as the controls panel's heading does for the click that collapses
|
||||||
|
// it. The gesture itself belongs to SelectionPanel, which owns the placement.
|
||||||
|
m_header->setCursor(Qt::SizeAllCursor);
|
||||||
|
QHBoxLayout* headerLayout = new QHBoxLayout(m_header);
|
||||||
headerLayout->setContentsMargins(0, 0, 0, 0);
|
headerLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
headerLayout->setSpacing(kHeaderSpacingPx);
|
headerLayout->setSpacing(kHeaderSpacingPx);
|
||||||
|
|
||||||
m_symbolLabel = new QLabel(header);
|
m_symbolLabel = new QLabel(m_header);
|
||||||
m_symbolLabel->hide();
|
m_symbolLabel->hide();
|
||||||
|
|
||||||
m_nameLabel = new QLabel(header);
|
m_nameLabel = new QLabel(m_header);
|
||||||
QFont nameFont = m_nameLabel->font();
|
QFont nameFont = m_nameLabel->font();
|
||||||
nameFont.setBold(true);
|
nameFont.setBold(true);
|
||||||
m_nameLabel->setFont(nameFont);
|
m_nameLabel->setFont(nameFont);
|
||||||
|
|
||||||
m_statusPill = new StatusPill(header);
|
m_statusPill = new StatusPill(m_header);
|
||||||
|
|
||||||
headerLayout->addWidget(m_symbolLabel);
|
headerLayout->addWidget(m_symbolLabel);
|
||||||
headerLayout->addWidget(m_nameLabel);
|
headerLayout->addWidget(m_nameLabel);
|
||||||
headerLayout->addStretch(1);
|
headerLayout->addStretch(1);
|
||||||
headerLayout->addWidget(m_statusPill);
|
headerLayout->addWidget(m_statusPill);
|
||||||
cardLayout->addWidget(header);
|
cardLayout->addWidget(m_header);
|
||||||
|
|
||||||
m_configurationGroup = new QWidget(this);
|
m_configurationGroup = new QWidget(this);
|
||||||
QVBoxLayout* configurationLayout = new QVBoxLayout(m_configurationGroup);
|
QVBoxLayout* configurationLayout = new QVBoxLayout(m_configurationGroup);
|
||||||
@@ -200,26 +172,25 @@ void SelectionContent::setProductionStatusSlot(const Building& building)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
|
||||||
// Room for every caption this building can show, so the panel keeps its width as the
|
|
||||||
// building's state changes rather than jumping with it.
|
|
||||||
reserveSlotFor(getAllStatusCaptions(isSalvageBay));
|
|
||||||
|
|
||||||
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
||||||
QColor dotColor;
|
// The Salvage Bay has no recipe and no cycle: its two states say whether it is
|
||||||
|
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
|
||||||
|
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
||||||
switch (*status)
|
switch (*status)
|
||||||
{
|
{
|
||||||
case ProductionStatus::Unconfigured: dotColor = colors.grey; break;
|
case ProductionStatus::Unconfigured:
|
||||||
case ProductionStatus::Producing: dotColor = colors.green; break;
|
setSlot(colors.grey, tr("no recipe"));
|
||||||
case ProductionStatus::Starved: dotColor = colors.red; break;
|
break;
|
||||||
case ProductionStatus::Blocked: dotColor = colors.yellow; break;
|
case ProductionStatus::Producing:
|
||||||
|
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
|
||||||
|
break;
|
||||||
|
case ProductionStatus::Starved:
|
||||||
|
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
|
||||||
|
break;
|
||||||
|
case ProductionStatus::Blocked:
|
||||||
|
setSlot(colors.yellow, tr("output full"));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
setSlot(dotColor, getStatusCaption(*status, isSalvageBay));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectionContent::reserveSlotFor(const QStringList& captions)
|
|
||||||
{
|
|
||||||
m_statusPill->reserveFor(captions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionContent::refreshConstruction()
|
void SelectionContent::refreshConstruction()
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
@@ -46,6 +45,11 @@ public:
|
|||||||
// SelectionPanel owns that decision.
|
// SelectionPanel owns that decision.
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|
||||||
|
// The card's header (REQ-UI-SELECTION-CARD), which is also the handle the panel is
|
||||||
|
// dragged by (REQ-UI-SELECTION-PANEL-DRAG). The panel hit-tests against it; the card
|
||||||
|
// itself does nothing with the gesture.
|
||||||
|
QWidget* getHeaderWidget() const { return m_header; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// constructionSiteId is set only while the card shows a construction site, in which
|
// constructionSiteId is set only while the card shows a construction site, in which
|
||||||
// case this base builds and drives the construction section in place of whatever the
|
// case this base builds and drives the construction section in place of whatever the
|
||||||
@@ -80,11 +84,6 @@ protected:
|
|||||||
void setCountSlot(int count);
|
void setCountSlot(int count);
|
||||||
void clearSlot();
|
void clearSlot();
|
||||||
|
|
||||||
// Reserves header room for the widest caption the slot will ever show. The panel is
|
|
||||||
// sized to its card (REQ-UI-SELECTION-PANEL), so without this it changes width every
|
|
||||||
// time the caption does -- and a card whose chips wrap changes height with it.
|
|
||||||
void reserveSlotFor(const QStringList& captions);
|
|
||||||
|
|
||||||
// Fills the right slot from the building's production status, mapped to the same
|
// Fills the right slot from the building's production status, mapped to the same
|
||||||
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
|
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
|
||||||
// the slot empty for a type that has no status light.
|
// the slot empty for a type that has no status light.
|
||||||
@@ -97,6 +96,7 @@ private:
|
|||||||
// Set while this card shows a construction site rather than a finished object.
|
// Set while this card shows a construction site rather than a finished object.
|
||||||
std::optional<BuildingId> m_siteId;
|
std::optional<BuildingId> m_siteId;
|
||||||
|
|
||||||
|
QWidget* m_header;
|
||||||
QLabel* m_symbolLabel;
|
QLabel* m_symbolLabel;
|
||||||
QLabel* m_nameLabel;
|
QLabel* m_nameLabel;
|
||||||
StatusPill* m_statusPill;
|
StatusPill* m_statusPill;
|
||||||
|
|||||||
@@ -33,16 +33,3 @@ QString getBehaviorLabel(BehaviorKind kind)
|
|||||||
}
|
}
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList getAllBehaviorLabels()
|
|
||||||
{
|
|
||||||
QStringList labels;
|
|
||||||
for (BehaviorKind kind : { BehaviorKind::Retreat, BehaviorKind::Attack,
|
|
||||||
BehaviorKind::SalvageScrap, BehaviorKind::Repair,
|
|
||||||
BehaviorKind::Rally, BehaviorKind::Standby,
|
|
||||||
BehaviorKind::Advance })
|
|
||||||
{
|
|
||||||
labels << getBehaviorLabel(kind);
|
|
||||||
}
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
@@ -14,8 +13,3 @@ QString getBuildingTypeName(BuildingType type);
|
|||||||
// Name of the behavior currently governing a ship, for the ship card's header slot
|
// Name of the behavior currently governing a ship, for the ship card's header slot
|
||||||
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
||||||
QString getBehaviorLabel(BehaviorKind kind);
|
QString getBehaviorLabel(BehaviorKind kind);
|
||||||
|
|
||||||
// Every name getBehaviorLabel can return. A ship's behavior changes as it fights, so the
|
|
||||||
// header reserves room for the widest of these rather than letting the panel change
|
|
||||||
// width each time (REQ-UI-SELECTION-PANEL).
|
|
||||||
QStringList getAllBehaviorLabels();
|
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ ShipContent::ShipContent(const SelectionContext& context,
|
|||||||
m_statsPanel = new ShipStatsPanel(context.config, this);
|
m_statsPanel = new ShipStatsPanel(context.config, this);
|
||||||
getRuntimeLayout()->addWidget(m_statsPanel);
|
getRuntimeLayout()->addWidget(m_statsPanel);
|
||||||
|
|
||||||
// A ship's behavior changes as it fights, so the header holds room for the longest
|
|
||||||
// name rather than the panel resizing under the player each time it does.
|
|
||||||
reserveSlotFor(getAllBehaviorLabels());
|
|
||||||
|
|
||||||
EntityAdmin& admin = context.sim->getAdmin();
|
EntityAdmin& admin = context.sim->getAdmin();
|
||||||
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
|
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "StatusPill.h"
|
#include "StatusPill.h"
|
||||||
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
@@ -50,23 +49,6 @@ StatusPill::StatusPill(QWidget* parent)
|
|||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusPill::reserveFor(const QStringList& captions)
|
|
||||||
{
|
|
||||||
if (captions == m_reservedFor)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_reservedFor = captions;
|
|
||||||
|
|
||||||
const QFontMetrics metrics(m_captionLabel->font());
|
|
||||||
int widestPx = 0;
|
|
||||||
for (const QString& caption : captions)
|
|
||||||
{
|
|
||||||
widestPx = qMax(widestPx, metrics.horizontalAdvance(caption));
|
|
||||||
}
|
|
||||||
m_captionLabel->setMinimumWidth(widestPx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
const QString& caption)
|
const QString& caption)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@@ -24,16 +23,7 @@ public:
|
|||||||
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
const QString& caption);
|
const QString& caption);
|
||||||
|
|
||||||
// Reserves room for the widest of the captions this pill can show, so the header --
|
|
||||||
// and with it the panel, which is sized to its content (REQ-UI-SELECTION-PANEL) --
|
|
||||||
// keeps its width as the caption changes. Without it the whole panel jumps every
|
|
||||||
// time a building's status does, and a card whose chips wrap changes height with it.
|
|
||||||
void reserveFor(const QStringList& captions);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_dotLabel;
|
QLabel* m_dotLabel;
|
||||||
QLabel* m_captionLabel;
|
QLabel* m_captionLabel;
|
||||||
// What the width is currently reserved for, so re-reserving the same set on every
|
|
||||||
// refresh costs nothing.
|
|
||||||
QStringList m_reservedFor;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user