Keep blocks still when switching layers

- The main block reserves the width and rows of its biggest layer, so the
  numpad and function row no longer shift between letters and symbols
- Smaller layers stretch into the room: stretch keys widen, rows grow
  taller (rows above the layer, like the function row, keep their height)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 22:35:00 +02:00
co-authored by Claude Opus 5.5
parent 75bbe2e566
commit ac0bfc2741
5 changed files with 66 additions and 14 deletions
+16 -9
View File
@@ -7,7 +7,7 @@ namespace {
qreal blockUnits(const Block &block) qreal blockUnits(const Block &block)
{ {
qreal units = 0; qreal units = block.minUnits;
for (const Row &row : block.rows) for (const Row &row : block.rows)
units = std::max(units, rowUnits(row)); units = std::max(units, rowUnits(row));
return units; return units;
@@ -49,7 +49,7 @@ qsizetype totalRows(const QList<Block> &blocks)
qsizetype rows = 0; qsizetype rows = 0;
for (const Block &block : blocks) { for (const Block &block : blocks) {
if (allFill || !block.fillHeight) if (allFill || !block.fillHeight)
rows = std::max(rows, block.rows.size()); rows = std::max({rows, block.rows.size(), block.minRows});
} }
return rows; return rows;
} }
@@ -69,17 +69,24 @@ QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardM
// Each cell includes one trailing gap; the last gap of a row is dropped, // Each cell includes one trailing gap; the last gap of a row is dropped,
// and blocks are separated by blockGap instead. // and blocks are separated by blockGap instead.
const qreal unitWidth = (innerWidth + blockCount * metrics.gap - (blockCount - 1) * metrics.blockGap) / units; const qreal unitWidth = (innerWidth + blockCount * metrics.gap - (blockCount - 1) * metrics.blockGap) / units;
const auto heightFor = [&](qsizetype rows) { return (innerHeight - qreal(rows - 1) * metrics.gap) / qreal(rows); }; const qreal sharedRowHeight = (innerHeight - qreal(maxRows - 1) * metrics.gap) / qreal(maxRows);
const qreal sharedRowHeight = heightFor(maxRows); const auto spanOf = [&](qsizetype rows) { return qreal(rows) * (sharedRowHeight + metrics.gap) - metrics.gap; };
qreal left = metrics.padding; qreal left = metrics.padding;
for (const Block &block : blocks) { for (const Block &block : blocks) {
const qreal width = blockUnits(block); const qreal width = blockUnits(block);
const qsizetype rows = block.rows.size(); const auto rows = block.rows.size();
const bool fill = block.fillHeight && rows > 0; const qreal span = block.fillHeight ? innerHeight : spanOf(std::max(rows, block.minRows));
const qreal rowHeight = fill ? heightFor(rows) : sharedRowHeight; const qsizetype fixed = std::min(block.fixedRows, rows);
qreal y = metrics.padding + (fill ? 0 : qreal(maxRows - rows) * (rowHeight + metrics.gap)); // Rows after the fixed ones share what's left; with none, the block sits at the bottom.
for (const Row &row : block.rows) { const qreal flexHeight = rows > fixed
? (span - qreal(fixed) * (sharedRowHeight + metrics.gap) - qreal(rows - fixed - 1) * metrics.gap)
/ qreal(rows - fixed)
: sharedRowHeight;
qreal y = metrics.padding + innerHeight - (rows > fixed ? span : spanOf(rows));
for (qsizetype r = 0; r < rows; ++r) {
const Row &row = block.rows[r];
const qreal rowHeight = r < fixed ? sharedRowHeight : flexHeight;
const QList<qreal> widths = stretchedWidths(row, width); const QList<qreal> widths = stretchedWidths(row, width);
const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0); const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0);
qreal x = left + (width - used) * unitWidth / 2; qreal x = left + (width - used) * unitWidth / 2;
+11 -4
View File
@@ -12,16 +12,23 @@ struct KeyboardMetrics {
}; };
// The keys on screen: blocks of rows, side by side. // The keys on screen: blocks of rows, side by side.
// A block can reserve room (minUnits, minRows) so that swapping its rows, e.g.
// switching layers, moves nothing else; its keys stretch into the room.
struct Block { struct Block {
QList<Row> rows; QList<Row> rows;
bool fillHeight = false; // rows grow to span the full height instead of sharing the row height bool fillHeight = false; // spans the full height instead of max(rows, minRows) shared rows
qreal minUnits = 0; // reserved width
qsizetype minRows = 0; // reserved height, in shared rows
qsizetype fixedRows = 0; // leading rows that keep the shared height when the block stretches
}; };
// Lays out blocks to fill `size`. Returns one rect per key, block by block in // Lays out blocks to fill `size`. Returns one rect per key, block by block in
// row order (spacers included). One key unit has the same width everywhere; // row order (spacers included). One key unit has the same width everywhere;
// rows share one height, set by the tallest block that doesn't fill, and // rows share one height, set by the tallest block that doesn't fill, and
// shorter blocks sit at the bottom. In each block, stretch keys grow to make // shorter blocks sit at the bottom. A block given more height than its rows
// their row as wide as the widest one, and remaining shorter rows are centered. // need (fillHeight, minRows) grows its rows after fixedRows to take it up.
// In each block, stretch keys grow to make their row as wide as the widest
// one (or minUnits), and remaining shorter rows are centered.
QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics = {}); QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics = {});
inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {}) inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {})
{ {
@@ -32,5 +39,5 @@ inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMe
qreal rowUnits(const Row &row); qreal rowUnits(const Row &row);
// Width of all blocks in key units, ignoring gaps. // Width of all blocks in key units, ignoring gaps.
qreal totalUnits(const QList<Block> &blocks); qreal totalUnits(const QList<Block> &blocks);
// Rows of the tallest block that sets the row height (see computeKeyRects). // Shared rows of the tallest block that sets the row height (see computeKeyRects).
qsizetype totalRows(const QList<Block> &blocks); qsizetype totalRows(const QList<Block> &blocks);
+10 -1
View File
@@ -1,5 +1,6 @@
#include "LayoutModel.h" #include "LayoutModel.h"
#include <algorithm>
#include <limits> #include <limits>
namespace { namespace {
@@ -183,8 +184,16 @@ QList<Block> LayoutModel::composeBlocks() const
if (section.placement == Section::Placement::Above && shown(section.id)) if (section.placement == Section::Placement::Above && shown(section.id))
main.rows += section.rows; main.rows += section.rows;
} }
if (layer && shown(MainSectionId)) if (layer && shown(MainSectionId)) {
// Room for the biggest layer, so switching layers keeps the other blocks still.
main.fixedRows = main.rows.size();
for (const Layer &other : m_layout.layers) {
main.minRows = std::max(main.minRows, main.fixedRows + other.rows.size());
for (const Row &row : other.rows)
main.minUnits = std::max(main.minUnits, rowUnits(row));
}
main.rows += layer->rows; main.rows += layer->rows;
}
QList<Block> blocks; QList<Block> blocks;
if (!main.rows.isEmpty()) if (!main.rows.isEmpty())
+14
View File
@@ -235,6 +235,20 @@ private Q_SLOTS:
QCOMPARE(model.rowCount(), mainKeys); // never nothing QCOMPARE(model.rowCount(), mainKeys); // never nothing
} }
void switchingLayersKeepsTheOtherBlocksStill()
{
model.setLayout(TestLayouts::build("us", KeyboardMode::Full)); // letters have more rows than symbols
model.setVisibleSections({u"function"_s, MainSectionId, u"numpad"_s});
model.setSize({1000, 300});
const auto lastKeyRect = [this] { return model.keyRect(model.rowCount() - 1); }; // numpad "="
const auto firstKeyRect = [this] { return model.keyRect(0); }; // esc
const QRectF numpad = lastKeyRect();
const QRectF esc = firstKeyRect();
model.setLayer(u"numbers"_s);
QCOMPARE(lastKeyRect(), numpad);
QCOMPARE(firstKeyRect(), esc);
}
KeyDef keysymKey(quint32 keysym) KeyDef keysymKey(quint32 keysym)
{ {
KeyDef key = makeKey(KeyType::Keysym); KeyDef key = makeKey(KeyType::Keysym);
+15
View File
@@ -82,6 +82,21 @@ private Q_SLOTS:
QCOMPARE(totalRows({tall, filling}), 4); QCOMPARE(totalRows({tall, filling}), 4);
QCOMPARE(totalRows({filling}), 2); QCOMPARE(totalRows({filling}), 2);
} }
void reservedRoomStretchesTheRowsAfterTheFixedOnes()
{
Block reserved{{{key()}, {key()}, {key()}}};
reserved.minUnits = 2;
reserved.minRows = 5;
reserved.fixedRows = 1;
const Block side{{{key()}}};
const auto rects = computeKeyRects({reserved, side}, {300, 100}, {.padding = 0, .gap = 0, .blockGap = 0});
QCOMPARE(totalRows({reserved, side}), 5);
QCOMPARE(rects[0], QRectF(50, 0, 100, 20)); // fixed row keeps the shared height, centered in 2 units
QCOMPARE(rects[1], QRectF(50, 20, 100, 40));
QCOMPARE(rects[2], QRectF(50, 60, 100, 40));
QCOMPARE(rects[3], QRectF(200, 80, 100, 20)); // the side block doesn't move
}
}; };
QTEST_GUILESS_MAIN(TestKeyGeometry) QTEST_GUILESS_MAIN(TestKeyGeometry)