Compare commits
2 Commits
a5c51f08f8
...
f06d79e60d
| Author | SHA1 | Date | |
|---|---|---|---|
| f06d79e60d | |||
| 97c269576e |
@@ -124,13 +124,31 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
Three product targets plus tests:
|
Three product targets plus tests:
|
||||||
|
|
||||||
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
||||||
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar. Depends on `lib` and on Qt's OpenGL widgets module.
|
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar, controls panel. Depends on `lib` and on Qt's OpenGL widgets module.
|
||||||
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
||||||
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
||||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||||
|
|
||||||
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
||||||
|
|
||||||
|
## Player Input
|
||||||
|
|
||||||
|
Every player control is declared once, in `lib/core/ControlAction.h`, and read by three consumers that must never disagree about it:
|
||||||
|
|
||||||
|
* **`ControlsPanel`** asks which actions apply and draws a row per action (REQ-UI-CONTROLS-CONTENT).
|
||||||
|
* **`InputMapper`** resolves a key press to an action and fires the event that action stands for.
|
||||||
|
* **`GameWorldView`** resolves a mouse gesture to an action and runs the branch that carries it out.
|
||||||
|
|
||||||
|
The file declares; it never performs. It holds no simulation access, fires no events, and names nothing — display strings live in `ui/ControlActionText.h`, which renders each badge from the binding the resolver actually matches, so a chip cannot claim a key that does nothing. What an action *does* stays in the widget that always did it: the drag state machines, hit-testing, and command enqueuing were not moved.
|
||||||
|
|
||||||
|
Three invariants are easy to break here:
|
||||||
|
|
||||||
|
* **Do not add a shortcut straight to `InputMapper`'s switch or `mousePressEvent`'s branches.** Add the action and its binding to the table; the handler switches on the resolved action. A binding added directly is invisible to the panel, which is the drift the table exists to prevent. (Build hotkeys and `F3`/`F4` are deliberate exceptions, documented in the header and in REQ-UI-CONTROLS-ACCURACY.)
|
||||||
|
* **Availability and display are different questions.** An action can be live in a context the panel does not advertise it in — `Ctrl`+click with an empty selection is the standing example. `isControlActionAvailable` answers the first, the per-context row lists answer the second, and `ControlActionTest` asserts the pairing that matters: every row's bindings resolve back to that row's action.
|
||||||
|
* **Gesture state is shared, not owned by an action.** Whether a belt drag is in progress decides what the right mouse button means, so it lives on `BuildModeController` where the resolver can see it — as does the hovered-transfer flag. `m_boxSelecting` is likewise one gesture serving two actions (box select and deconstruct area).
|
||||||
|
|
||||||
|
`ControlContext` is a plain snapshot rather than references to the live controllers, which is what keeps the rules testable without a world and stops an action reaching into the simulation: if a rule needs a fact, the fact is named in the struct and the caller supplies it. When bindings become player-configurable, only the binding tables in `ControlAction.cpp` turn from hard-coded data into loaded data.
|
||||||
|
|
||||||
## Belt Subsystem
|
## Belt Subsystem
|
||||||
|
|
||||||
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QScrollBar>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
@@ -15,10 +17,15 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
const int kMarginPx = 8; // between the band's edge and the panel
|
const int kMarginPx = 8; // between the view's edge and the panel
|
||||||
const int kCardMarginPx = 8; // inside the panel, around its content
|
const int kCardMarginPx = 8; // inside the panel, around its content
|
||||||
|
const int kHeadingGapPx = 4; // between the heading and the rows
|
||||||
const int kRefreshMs = 50; // see the class comment on why this polls
|
const int kRefreshMs = 50; // see the class comment on why this polls
|
||||||
|
|
||||||
|
// The panel's border, from the stylesheet below. Spelled out because the stylesheet box
|
||||||
|
// is what sets it and asking the style for it before the first show is unreliable.
|
||||||
|
const int kBorderPx = 1;
|
||||||
|
|
||||||
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
|
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
|
||||||
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
|
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
|
||||||
|
|
||||||
@@ -117,7 +124,7 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||||
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
|
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
|
||||||
kCardMarginPx, kCardMarginPx);
|
kCardMarginPx, kCardMarginPx);
|
||||||
outerLayout->setSpacing(4);
|
outerLayout->setSpacing(kHeadingGapPx);
|
||||||
|
|
||||||
m_heading = new QLabel(this);
|
m_heading = new QLabel(this);
|
||||||
m_heading->setObjectName(QStringLiteral("controlHeading"));
|
m_heading->setObjectName(QStringLiteral("controlHeading"));
|
||||||
@@ -125,11 +132,24 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
|
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
|
||||||
outerLayout->addWidget(m_heading);
|
outerLayout->addWidget(m_heading);
|
||||||
|
|
||||||
|
// Rows taller than the space available scroll rather than being cut off
|
||||||
|
// (REQ-UI-CONTROLS-PANEL). The viewport is transparent so the panel's own rounded
|
||||||
|
// chrome shows through, and horizontal scrolling is off because the width always
|
||||||
|
// follows the content.
|
||||||
m_rows = new QWidget(this);
|
m_rows = new QWidget(this);
|
||||||
m_rowsLayout = new QVBoxLayout(m_rows);
|
m_rowsLayout = new QVBoxLayout(m_rows);
|
||||||
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
m_rowsLayout->setSpacing(0);
|
m_rowsLayout->setSpacing(0);
|
||||||
outerLayout->addWidget(m_rows);
|
m_rows->setAutoFillBackground(false);
|
||||||
|
|
||||||
|
m_scrollArea = new QScrollArea(this);
|
||||||
|
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
|
m_scrollArea->setWidgetResizable(true);
|
||||||
|
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_scrollArea->viewport()->setAutoFillBackground(false);
|
||||||
|
m_scrollArea->setWidget(m_rows);
|
||||||
|
outerLayout->addWidget(m_scrollArea);
|
||||||
|
|
||||||
// Polling rather than subscribing; see the class comment.
|
// Polling rather than subscribing; see the class comment.
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
@@ -153,7 +173,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
|||||||
if (m_heading->geometry().contains(event->pos()))
|
if (m_heading->geometry().contains(event->pos()))
|
||||||
{
|
{
|
||||||
m_collapsed = !m_collapsed;
|
m_collapsed = !m_collapsed;
|
||||||
m_rows->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
refit();
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
@@ -220,7 +240,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
|
|||||||
addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
|
addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_rows->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
refit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,22 +290,33 @@ void ControlsPanel::refit()
|
|||||||
m_rowsLayout->invalidate();
|
m_rowsLayout->invalidate();
|
||||||
m_rowsLayout->activate();
|
m_rowsLayout->activate();
|
||||||
|
|
||||||
// Deliberately not activating the panel's own layout here. That lays the heading
|
|
||||||
// and the rows out inside the geometry the panel still has from the previous
|
|
||||||
// context, which is the wrong frame of reference for choosing the new one.
|
|
||||||
// totalSizeHint answers "how big does this need to be" without reference to how big
|
|
||||||
// it currently is; setGeometry below then re-runs the outer layout on its own.
|
|
||||||
layout()->invalidate();
|
|
||||||
const QSize needed = layout()->totalSizeHint();
|
|
||||||
|
|
||||||
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
if (band.width() <= 0 || band.height() <= 0) { return; }
|
if (band.width() <= 0 || band.height() <= 0) { return; }
|
||||||
|
|
||||||
|
// Measured from the heading and the rows directly rather than from the panel's own
|
||||||
|
// layout: the rows now sit in a scroll area, whose size hint describes a viewport
|
||||||
|
// and says nothing about how tall its contents are. Deliberately not activating the
|
||||||
|
// panel's own layout either -- that lays its children out inside the geometry left
|
||||||
|
// over from the previous context, which is the wrong frame of reference for
|
||||||
|
// choosing the new one. setGeometry below re-runs it.
|
||||||
|
const int chromePx = 2 * (kCardMarginPx + kBorderPx);
|
||||||
|
const QSize headingHint = m_heading->sizeHint();
|
||||||
|
const QSize rowsHint = m_rows->sizeHint();
|
||||||
|
|
||||||
|
int contentWidthPx = headingHint.width();
|
||||||
|
int contentHeightPx = headingHint.height();
|
||||||
|
if (!m_collapsed)
|
||||||
|
{
|
||||||
|
contentWidthPx = qMax(contentWidthPx, rowsHint.width());
|
||||||
|
contentHeightPx += kHeadingGapPx + rowsHint.height();
|
||||||
|
}
|
||||||
|
const int wantedHeightPx = contentHeightPx + chromePx;
|
||||||
|
|
||||||
// The bottom-left corner of the view, growing upward as rows are added
|
// The bottom-left corner of the view, growing upward as rows are added
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
// (REQ-UI-CONTROLS-PANEL).
|
||||||
const int widthPx = qMin(needed.width(), band.width());
|
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||||
int heightPx = qMin(needed.height(), band.height());
|
int heightPx = qMin(wantedHeightPx, band.height());
|
||||||
int topPx = band.bottom() - heightPx + 1;
|
int topPx = band.bottom() - heightPx + 1;
|
||||||
|
|
||||||
// The build button bar is centered and sized to its buttons, so it usually leaves
|
// The build button bar is centered and sized to its buttons, so it usually leaves
|
||||||
// this corner free and the panel can share the bottom edge with it. Only when the
|
// this corner free and the panel can share the bottom edge with it. Only when the
|
||||||
@@ -299,5 +330,13 @@ void ControlsPanel::refit()
|
|||||||
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
||||||
|
// own width, or it would appear over the labels.
|
||||||
|
if (heightPx < wantedHeightPx)
|
||||||
|
{
|
||||||
|
widthPx = qMin(widthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
|
||||||
|
band.width());
|
||||||
|
}
|
||||||
|
|
||||||
setGeometry(band.left(), topPx, widthPx, heightPx);
|
setGeometry(band.left(), topPx, widthPx, heightPx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QScrollArea;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ private:
|
|||||||
const GameWorldView* m_view;
|
const GameWorldView* m_view;
|
||||||
|
|
||||||
QLabel* m_heading;
|
QLabel* m_heading;
|
||||||
|
// Scrolls the rows once they outgrow the space the panel has (REQ-UI-CONTROLS-PANEL).
|
||||||
|
// The heading is deliberately outside it, so it stays put and stays clickable.
|
||||||
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_rows;
|
QWidget* m_rows;
|
||||||
QVBoxLayout* m_rowsLayout;
|
QVBoxLayout* m_rowsLayout;
|
||||||
QTimer* m_refreshTimer;
|
QTimer* m_refreshTimer;
|
||||||
|
|||||||
Reference in New Issue
Block a user