Reflow the deck into two categorical rows plus a double-height MASTER bus deck
Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
This commit is contained in:
@@ -10,8 +10,17 @@ namespace {
|
||||
|
||||
// The knob-row width of a group: cells side by side (no inter-cell gap — the 48px cell
|
||||
// already carries its own breathing room around the 28px knob), plus the optional row
|
||||
// toggle after a kDeckToggleGap.
|
||||
// toggle after a kDeckToggleGap. A spanning group's cells stack, so its knob row is one
|
||||
// cell wide plus whatever readout column sits beside it.
|
||||
int knobRowWidth(const DeckGroupDesc& g) {
|
||||
if (g.row == DeckRow::Spanning) {
|
||||
int w = g.cellIds.empty() ? 0 : kDeckCellW;
|
||||
if (g.column.id >= 0) {
|
||||
if (w > 0) w += kDeckColumnGap;
|
||||
w += g.column.width;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
int w = static_cast<int>(g.cellIds.size()) * kDeckCellW;
|
||||
if (g.rowToggle.id >= 0) {
|
||||
if (w > 0) w += kDeckToggleGap;
|
||||
@@ -29,6 +38,24 @@ int captionRowWidth(const DeckGroupDesc& g) {
|
||||
return w;
|
||||
}
|
||||
|
||||
// One knob cell inside `cell`: the centered dial square, its concentric inner disc, and the
|
||||
// label band beneath.
|
||||
DeckCellLayout layoutCell(int id, const Rect& cell) {
|
||||
DeckCellLayout c;
|
||||
c.id = id;
|
||||
c.cell = cell;
|
||||
const int knobLeft = cell.x + (cell.width - kDeckKnobSize) / 2;
|
||||
const int knobTop = cell.y + 4;
|
||||
c.knob = Rect::ltrb(knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize);
|
||||
const int innerLeftPx = knobLeft + (kDeckKnobSize - kDeckInnerDialSize) / 2;
|
||||
const int innerTopPx = knobTop + (kDeckKnobSize - kDeckInnerDialSize) / 2;
|
||||
c.inner = Rect::ltrb(innerLeftPx, innerTopPx, innerLeftPx + kDeckInnerDialSize,
|
||||
innerTopPx + kDeckInnerDialSize);
|
||||
const int labelTop = knobTop + kDeckKnobSize + 4;
|
||||
c.label = Rect::ltrb(cell.x, labelTop, cell.right(), labelTop + kDeckCellLabelH);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Place one group's inner geometry given its box.
|
||||
DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||
DeckGroupLayout out;
|
||||
@@ -46,7 +73,8 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||
const int radioTop = captionTop + (kDeckCaptionH - kDeckRadioSize) / 2;
|
||||
out.captionRadio = DeckRadioLayout{
|
||||
g.captionRadio.id, Rect::ltrb(innerRight - kDeckRadioSize, radioTop, innerRight,
|
||||
radioTop + kDeckRadioSize)};
|
||||
radioTop + kDeckRadioSize),
|
||||
g.captionRadio.passive};
|
||||
captionRight = out.captionRadio.box.x - kDeckToggleGap;
|
||||
out.caption.width = captionRight - out.caption.x;
|
||||
}
|
||||
@@ -65,10 +93,34 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||
placeToggle(g.captionToggle, out.captionToggle);
|
||||
placeToggle(g.captionToggle2, out.captionToggle2);
|
||||
|
||||
const int cellTop = captionTop + kDeckCaptionH + kDeckCaptionGap;
|
||||
|
||||
if (g.row == DeckRow::Spanning) {
|
||||
// FIXED slots down the left column, one per declared id (reserves advance the slot
|
||||
// without drawing a cell), spaced by a whole row pitch so slot k lands exactly on
|
||||
// categorical row k's knob baseline. Deliberately NOT the run-division law below.
|
||||
int slotTop = cellTop;
|
||||
for (int id : g.cellIds) {
|
||||
if (id >= 0) {
|
||||
out.cells.push_back(layoutCell(
|
||||
id, Rect::ltrb(innerLeft, slotTop, innerLeft + kDeckCellW,
|
||||
slotTop + kDeckCellH)));
|
||||
}
|
||||
slotTop += kDeckGroupH + kDeckRowGap;
|
||||
}
|
||||
if (g.column.id >= 0) {
|
||||
// ONE rect spanning every slot, not a readout per row.
|
||||
const int colX = innerLeft + (g.cellIds.empty() ? 0 : kDeckCellW + kDeckColumnGap);
|
||||
out.column = DeckColumnLayout{
|
||||
g.column.id, Rect::ltrb(colX, cellTop, colX + g.column.width,
|
||||
box.bottom() - kDeckGroupPadY)};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Knob row: the cells present divide the whole reserved run (one kDeckCellW per declared
|
||||
// id, reserves included). Integer division puts an indivisible residue in symmetric end
|
||||
// margins rather than in one odd-width cell — keyboard_strip's uniformity-wins rule.
|
||||
const int cellTop = captionTop + kDeckCaptionH + kDeckCaptionGap;
|
||||
const int runWidth = static_cast<int>(g.cellIds.size()) * kDeckCellW;
|
||||
int presentCells = 0;
|
||||
for (int id : g.cellIds) {
|
||||
@@ -78,19 +130,8 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||
int x = innerLeft + (runWidth - presentCells * cellW) / 2;
|
||||
for (int id : g.cellIds) {
|
||||
if (id < 0) continue;
|
||||
DeckCellLayout c;
|
||||
c.id = id;
|
||||
c.cell = Rect::ltrb(x, cellTop, x + cellW, cellTop + kDeckCellH);
|
||||
const int knobLeft = x + (cellW - kDeckKnobSize) / 2;
|
||||
const int knobTop = cellTop + 4;
|
||||
c.knob = Rect::ltrb(knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize);
|
||||
const int innerLeftPx = knobLeft + (kDeckKnobSize - kDeckInnerDialSize) / 2;
|
||||
const int innerTopPx = knobTop + (kDeckKnobSize - kDeckInnerDialSize) / 2;
|
||||
c.inner = Rect::ltrb(innerLeftPx, innerTopPx, innerLeftPx + kDeckInnerDialSize,
|
||||
innerTopPx + kDeckInnerDialSize);
|
||||
const int labelTop = knobTop + kDeckKnobSize + 4;
|
||||
c.label = Rect::ltrb(c.cell.x, labelTop, c.cell.right(), labelTop + kDeckCellLabelH);
|
||||
out.cells.push_back(c);
|
||||
out.cells.push_back(layoutCell(id, Rect::ltrb(x, cellTop, x + cellW,
|
||||
cellTop + kDeckCellH)));
|
||||
x += cellW;
|
||||
}
|
||||
if (g.rowToggle.id >= 0) {
|
||||
@@ -107,65 +148,110 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The gutters between `count` groups whose widths total `total`, justified space-between
|
||||
// inside `blockW`. Empty for a single group.
|
||||
std::vector<int> justifyGutters(int count, int total, int blockW) {
|
||||
const int gutters = count - 1;
|
||||
if (gutters <= 0) return {};
|
||||
const int slack = blockW - total;
|
||||
if (slack < gutters * kDeckGroupGap) {
|
||||
// The block cannot hold the row: minimum gutters, and the row overruns to the right
|
||||
// rather than wrapping. Unreachable in the editor — see layoutDeck's header note.
|
||||
return std::vector<int>(static_cast<std::size_t>(gutters), kDeckGroupGap);
|
||||
}
|
||||
const int base = slack / gutters;
|
||||
const int residue = slack % gutters; // both non-negative: slack >= gutters * 12 > 0
|
||||
std::vector<int> out(static_cast<std::size_t>(gutters), base);
|
||||
for (int i = 0; i < residue; ++i) ++out[static_cast<std::size_t>(i)];
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int deckGroupWidth(const DeckGroupDesc& g) {
|
||||
return (std::max)(captionRowWidth(g), knobRowWidth(g)) + 2 * kDeckGroupPadX;
|
||||
}
|
||||
|
||||
int deckRowCount(const std::vector<DeckGroupDesc>& groups, int availWidth) {
|
||||
if (groups.empty()) return 0;
|
||||
int rows = 1;
|
||||
int x = 0;
|
||||
int deckRowCount(const std::vector<DeckGroupDesc>& groups) {
|
||||
bool sound = false, contour = false;
|
||||
for (const DeckGroupDesc& g : groups) {
|
||||
const int w = deckGroupWidth(g);
|
||||
if (x > 0 && x + kDeckGroupGap + w > availWidth) {
|
||||
++rows;
|
||||
x = w;
|
||||
} else {
|
||||
x += (x > 0 ? kDeckGroupGap : 0) + w;
|
||||
}
|
||||
if (g.row == DeckRow::Sound) sound = true;
|
||||
else if (g.row == DeckRow::Contour) contour = true;
|
||||
}
|
||||
return rows;
|
||||
return (sound ? 1 : 0) + (contour ? 1 : 0);
|
||||
}
|
||||
|
||||
int deckHeight(const std::vector<DeckGroupDesc>& groups, int availWidth) {
|
||||
const int rows = deckRowCount(groups, availWidth);
|
||||
if (rows == 0) return 0;
|
||||
return rows * kDeckGroupH + (rows - 1) * kDeckRowGap;
|
||||
int deckHeight(const std::vector<DeckGroupDesc>& groups) {
|
||||
const int rows = deckRowCount(groups);
|
||||
int h = rows > 0 ? rows * kDeckGroupH + (rows - 1) * kDeckRowGap : 0;
|
||||
for (const DeckGroupDesc& g : groups) {
|
||||
if (g.row == DeckRow::Spanning) h = (std::max)(h, kDeckSpanningH);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
|
||||
int availWidth) {
|
||||
DeckLayout out;
|
||||
if (groups.empty()) return out;
|
||||
int x = left;
|
||||
int y = top;
|
||||
bool rowHasGroup = false;
|
||||
out.rowCount = 1;
|
||||
for (const DeckGroupDesc& g : groups) {
|
||||
const int w = deckGroupWidth(g);
|
||||
if (rowHasGroup && (x + kDeckGroupGap + w) > (left + availWidth)) {
|
||||
// Wrap: whole trailing group onto the next row (mirror of deckRowCount).
|
||||
++out.rowCount;
|
||||
x = left;
|
||||
y += kDeckGroupH + kDeckRowGap;
|
||||
rowHasGroup = false;
|
||||
}
|
||||
if (rowHasGroup) x += kDeckGroupGap;
|
||||
const Rect box = Rect::ltrb(x, y, x + w, y + kDeckGroupH);
|
||||
out.groups.push_back(layoutGroup(g, box));
|
||||
x = box.right();
|
||||
rowHasGroup = true;
|
||||
|
||||
// Partition by the group's OWN row (indices, so the OUTPUT keeps deck order — the shell
|
||||
// and the tests pair a layout with the descriptor at the same position).
|
||||
std::vector<std::size_t> rows[2];
|
||||
std::vector<std::size_t> spanning;
|
||||
for (std::size_t i = 0; i < groups.size(); ++i) {
|
||||
const DeckRow row = groups[i].row;
|
||||
if (row == DeckRow::Spanning) spanning.push_back(i);
|
||||
else rows[row == DeckRow::Contour ? 1 : 0].push_back(i);
|
||||
}
|
||||
out.height = out.rowCount * kDeckGroupH + (out.rowCount - 1) * kDeckRowGap;
|
||||
|
||||
// The spanning decks take the right edge; the row block is what is left of them.
|
||||
int spanTotal = 0;
|
||||
for (std::size_t i : spanning) spanTotal += deckGroupWidth(groups[i]);
|
||||
if (!spanning.empty()) {
|
||||
spanTotal += (static_cast<int>(spanning.size()) - 1) * kDeckGroupGap;
|
||||
}
|
||||
const int blockW = availWidth - (spanning.empty() ? 0 : spanTotal + kDeckGroupGap);
|
||||
|
||||
std::vector<Rect> boxes(groups.size());
|
||||
int y = top;
|
||||
for (const std::vector<std::size_t>& row : rows) {
|
||||
if (row.empty()) continue; // an absent category collapses; it leaves no empty band
|
||||
++out.rowCount;
|
||||
int total = 0;
|
||||
for (std::size_t i : row) total += deckGroupWidth(groups[i]);
|
||||
const std::vector<int> gutters =
|
||||
justifyGutters(static_cast<int>(row.size()), total, blockW);
|
||||
int x = left;
|
||||
for (std::size_t k = 0; k < row.size(); ++k) {
|
||||
const int w = deckGroupWidth(groups[row[k]]);
|
||||
boxes[row[k]] = Rect::ltrb(x, y, x + w, y + kDeckGroupH);
|
||||
x += w;
|
||||
if (k < gutters.size()) x += gutters[k];
|
||||
}
|
||||
y += kDeckGroupH + kDeckRowGap;
|
||||
}
|
||||
|
||||
int sx = left + availWidth - spanTotal;
|
||||
for (std::size_t i : spanning) {
|
||||
const int w = deckGroupWidth(groups[i]);
|
||||
boxes[i] = Rect::ltrb(sx, top, sx + w, top + kDeckSpanningH);
|
||||
sx += w + kDeckGroupGap;
|
||||
}
|
||||
|
||||
out.groups.reserve(groups.size());
|
||||
for (std::size_t i = 0; i < groups.size(); ++i) {
|
||||
out.groups.push_back(layoutGroup(groups[i], boxes[i]));
|
||||
}
|
||||
out.height = deckHeight(groups);
|
||||
return out;
|
||||
}
|
||||
|
||||
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
|
||||
for (const DeckGroupLayout& g : layout.groups) {
|
||||
if (!contains(g.box, x, y)) continue;
|
||||
if (g.captionRadio.id >= 0 && contains(g.captionRadio.box, x, y)) {
|
||||
if (g.captionRadio.id >= 0 && !g.captionRadio.passive &&
|
||||
contains(g.captionRadio.box, x, y)) {
|
||||
return {DeckHitKind::CaptionRadio, g.captionRadio.id, -1, false};
|
||||
}
|
||||
for (const DeckToggleLayout* t : {&g.captionToggle, &g.captionToggle2}) {
|
||||
@@ -185,7 +271,13 @@ DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
|
||||
return {DeckHitKind::Knob, c.id, -1, contains(c.inner, x, y)};
|
||||
}
|
||||
}
|
||||
return {}; // inside the box but on fence/padding — a miss (groups never overlap)
|
||||
if (g.column.id >= 0 && contains(g.column.box, x, y)) {
|
||||
return {DeckHitKind::Column, g.column.id, -1, false};
|
||||
}
|
||||
// Inside the box but on fence/padding — a miss. First-match is exact while the boxes
|
||||
// are disjoint, which they are at every width the row block fits; under the sub-floor
|
||||
// overrun an overrunning row can reach the spanning deck and the row group answers.
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user