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:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user