From ac0bfc2741fcab2015565a668ee092269069479e Mon Sep 17 00:00:00 2001 From: ericek111 Date: Wed, 14 Oct 2026 22:35:00 +0200 Subject: [PATCH] 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 --- src/layout/KeyGeometry.cpp | 25 ++++++++++++++++--------- src/layout/KeyGeometry.h | 15 +++++++++++---- src/layout/LayoutModel.cpp | 11 ++++++++++- tests/tst_controller.cpp | 14 ++++++++++++++ tests/tst_keygeometry.cpp | 15 +++++++++++++++ 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/layout/KeyGeometry.cpp b/src/layout/KeyGeometry.cpp index 882de32..077b063 100644 --- a/src/layout/KeyGeometry.cpp +++ b/src/layout/KeyGeometry.cpp @@ -7,7 +7,7 @@ namespace { qreal blockUnits(const Block &block) { - qreal units = 0; + qreal units = block.minUnits; for (const Row &row : block.rows) units = std::max(units, rowUnits(row)); return units; @@ -49,7 +49,7 @@ qsizetype totalRows(const QList &blocks) qsizetype rows = 0; for (const Block &block : blocks) { if (allFill || !block.fillHeight) - rows = std::max(rows, block.rows.size()); + rows = std::max({rows, block.rows.size(), block.minRows}); } return rows; } @@ -69,17 +69,24 @@ QList computeKeyRects(const QList &blocks, QSizeF size, KeyboardM // Each cell includes one trailing gap; the last gap of a row is dropped, // and blocks are separated by blockGap instead. 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 = heightFor(maxRows); + const qreal sharedRowHeight = (innerHeight - qreal(maxRows - 1) * metrics.gap) / qreal(maxRows); + const auto spanOf = [&](qsizetype rows) { return qreal(rows) * (sharedRowHeight + metrics.gap) - metrics.gap; }; qreal left = metrics.padding; for (const Block &block : blocks) { const qreal width = blockUnits(block); - const qsizetype rows = block.rows.size(); - const bool fill = block.fillHeight && rows > 0; - const qreal rowHeight = fill ? heightFor(rows) : sharedRowHeight; - qreal y = metrics.padding + (fill ? 0 : qreal(maxRows - rows) * (rowHeight + metrics.gap)); - for (const Row &row : block.rows) { + const auto rows = block.rows.size(); + const qreal span = block.fillHeight ? innerHeight : spanOf(std::max(rows, block.minRows)); + const qsizetype fixed = std::min(block.fixedRows, rows); + // Rows after the fixed ones share what's left; with none, the block sits at the bottom. + 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 widths = stretchedWidths(row, width); const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0); qreal x = left + (width - used) * unitWidth / 2; diff --git a/src/layout/KeyGeometry.h b/src/layout/KeyGeometry.h index db04cbd..c122131 100644 --- a/src/layout/KeyGeometry.h +++ b/src/layout/KeyGeometry.h @@ -12,16 +12,23 @@ struct KeyboardMetrics { }; // 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 { QList 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 // 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 -// shorter blocks sit at the bottom. In each block, stretch keys grow to make -// their row as wide as the widest one, and remaining shorter rows are centered. +// shorter blocks sit at the bottom. A block given more height than its rows +// 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 computeKeyRects(const QList &blocks, QSizeF size, KeyboardMetrics metrics = {}); inline QList computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {}) { @@ -32,5 +39,5 @@ inline QList computeKeyRects(const Layer &layer, QSizeF size, KeyboardMe qreal rowUnits(const Row &row); // Width of all blocks in key units, ignoring gaps. qreal totalUnits(const QList &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 &blocks); diff --git a/src/layout/LayoutModel.cpp b/src/layout/LayoutModel.cpp index c11b6a5..8828f81 100644 --- a/src/layout/LayoutModel.cpp +++ b/src/layout/LayoutModel.cpp @@ -1,5 +1,6 @@ #include "LayoutModel.h" +#include #include namespace { @@ -183,8 +184,16 @@ QList LayoutModel::composeBlocks() const if (section.placement == Section::Placement::Above && shown(section.id)) 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; + } QList blocks; if (!main.rows.isEmpty()) diff --git a/tests/tst_controller.cpp b/tests/tst_controller.cpp index 3fc8a00..fc7d23a 100644 --- a/tests/tst_controller.cpp +++ b/tests/tst_controller.cpp @@ -235,6 +235,20 @@ private Q_SLOTS: 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 key = makeKey(KeyType::Keysym); diff --git a/tests/tst_keygeometry.cpp b/tests/tst_keygeometry.cpp index 800dd3e..f38033b 100644 --- a/tests/tst_keygeometry.cpp +++ b/tests/tst_keygeometry.cpp @@ -82,6 +82,21 @@ private Q_SLOTS: QCOMPARE(totalRows({tall, filling}), 4); 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)