diff --git a/docs/requirements.md b/docs/requirements.md index fbb16fb..e48604f 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -591,6 +591,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE - REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon on its colored square (REQ-UI-ITEM-ICON) with its per-cycle amount and the inputs separated by `+`, an arrow, each output item's icon on its colored square with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. A building with no recipe or schematic selected shows no summary — including an auto-recipe building that has yet to select one (REQ-BLD-AUTO-RECIPE), which shows none until it does and keeps it from then on, so the card does not resize in step with the building's status (REQ-UI-SELECTION-STATUS, REQ-UI-SELECTION-PANEL). - REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all. - REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT). + - **When the rectangle appears.** The rectangle is drawn only once the cursor has moved at least **2 pixels** from the position the button went down at — a press alone draws nothing, so a plain click does not flash a one-tile rectangle. The threshold is in screen pixels rather than tiles because it only separates a click from a drag, which is a question of hand steadiness: a drag that stays inside the starting tile still shows its rectangle. Once shown, the rectangle stays shown for the rest of the drag, including when the cursor comes back to where it started. Scrolling the view while the button is held (REQ-UI-SCROLL) moves no cursor but does grow the box, so it too shows the rectangle as soon as the box covers more than the starting tile. This governs the drawing only: what the gesture selects (or, in deconstruct mode, marks) on release is unchanged, and a click that never passes the threshold still resolves as a one-tile box. - **Rectangle color.** While dragging, the selection rectangle is drawn as an outline in `visuals.toml [overlays].selected_outline` — the same color and config entry as the outline drawn in the world around the objects that end up selected, so the box and the selection it produces read as one thing. **Exception:** while deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), the box drag marks buildings for demolition instead (REQ-BLD-DECONSTRUCT-BOX) and the rectangle is drawn in the deconstruct color — the RGB of `visuals.toml [overlays].deconstruct_tint`, drawn **fully opaque**. That entry's alpha channel governs only the fills it tints (the deconstruct-mode hover tint and queued buildings, REQ-UI-DECONSTRUCT-BORDER, REQ-BLD-DECON-QUEUE) and is not applied to this outline, which would otherwise be too faint to see. The rectangle's geometry is the same in both modes. - REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as ` buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress. - REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). Recipe selection (miner, assembler, smelter, reprocessing plant) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW). diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index f601faf..bd4bd2e 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -148,6 +148,7 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config, , m_debugDraw(false) , m_rng(std::random_device{}()) , m_boxSelecting(false) + , m_boxDragMoved(false) , m_gameOverShown(false) , m_schematicChoiceShown(false) { @@ -300,6 +301,10 @@ void GameWorldView::onFrame() { m_boxCurrentTile = getCoordinates().widgetToTile(mapFromGlobal(QCursor::pos())); + // The cursor has not travelled a pixel, so the drag threshold above never + // trips; but the scroll has grown the box past the tile it started on, + // which has to become visible. + if (m_boxCurrentTile != m_boxStartTile) { m_boxDragMoved = true; } } } @@ -420,8 +425,9 @@ void GameWorldView::paintGL() WorldRenderFrame GameWorldView::makeRenderFrame() const { - return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, m_boxSelecting, - m_boxStartTile, m_boxCurrentTile, m_debugDraw}; + return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, + m_boxSelecting, m_boxDragMoved, m_boxStartTile, + m_boxCurrentTile, m_debugDraw}; } // --------------------------------------------------------------------------- @@ -1224,6 +1230,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) m_boxSelecting = true; m_boxStartTile = tile; m_boxCurrentTile = tile; + m_boxStartPos = event->pos(); + m_boxDragMoved = false; break; case ControlAction::Select: @@ -1238,6 +1246,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) m_boxSelecting = true; m_boxStartTile = tile; m_boxCurrentTile = tile; + m_boxStartPos = event->pos(); + m_boxDragMoved = false; } break; @@ -1349,6 +1359,14 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event) const QPoint tile = coordinates.widgetToTile(event->pos()); m_cursorWorldPos = coordinates.widgetToWorld(event->pos()); + // A press is a drag once the cursor has travelled far enough from it; below that + // it stays a click and shows no rectangle (REQ-UI-MULTI-SELECT). + if (m_boxSelecting + && (event->pos() - m_boxStartPos).manhattanLength() >= kBoxDragThresholdPixels) + { + m_boxDragMoved = true; + } + if (m_buildMode.isBuilderMode()) { m_buildMode.setGhostTile(tile); diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index 87bedf6..51b8085 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -260,6 +260,11 @@ private: // paused or slowed, instead of fading on wall-clock time (REQ-SHP-FIRING-BEAM). static constexpr Tick kBeamLifetimeTicks = secondsToTicks(0.3); + // How far the cursor must travel from the press position, in widget pixels + // (Manhattan distance), before a box drag shows its rectangle + // (REQ-UI-MULTI-SELECT). + static constexpr int kBoxDragThresholdPixels = 2; + Simulation* m_sim; const GameConfig* m_config; const VisualsConfig* m_visuals; @@ -310,6 +315,15 @@ private: bool m_boxSelecting; QPoint m_boxStartTile; QPoint m_boxCurrentTile; + // Where the button went down, in widget pixels; the origin the drag threshold + // below measures from. Pixels, not tiles: the threshold separates a click from a + // drag, which is a hand-steadiness question and not a tile-sized one. + QPoint m_boxStartPos; + // Whether the cursor has moved far enough from m_boxStartPos for this to read as + // a drag. Until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT): a plain + // click would otherwise flash a one-tile rectangle. Sticky for the rest of the + // drag, so coming back to the press position does not hide the rectangle again. + bool m_boxDragMoved; // Interprets this widget's key events into semantic actions and publishes them // (REQ-UI-HOTKEYS). Owned here for now because this is the widget that holds diff --git a/src/ui/WorldRenderer.cpp b/src/ui/WorldRenderer.cpp index 3f66718..c734760 100644 --- a/src/ui/WorldRenderer.cpp +++ b/src/ui/WorldRenderer.cpp @@ -1019,8 +1019,9 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor } } - // Box-select rectangle - if (frame.isBoxSelecting) + // Box-select rectangle. Not drawn until the drag has moved far enough to read as + // one, so a plain click does not flash a rectangle (REQ-UI-MULTI-SELECT). + if (frame.isBoxSelecting && frame.isBoxDragMoved) { const QPoint tl(std::min(frame.boxStartTile.x(), frame.boxCurrentTile.x()), std::min(frame.boxStartTile.y(), frame.boxCurrentTile.y())); diff --git a/src/ui/WorldRenderer.h b/src/ui/WorldRenderer.h index 647ff98..023530a 100644 --- a/src/ui/WorldRenderer.h +++ b/src/ui/WorldRenderer.h @@ -55,6 +55,9 @@ struct WorldRenderFrame const BuildModeController& buildMode; const std::vector& beams; bool isBoxSelecting; + // Whether that box drag has passed the movement threshold that tells it apart + // from a click; until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT). + bool isBoxDragMoved; QPoint boxStartTile; QPoint boxCurrentTile; bool isDebugDrawEnabled;