diff --git a/README.md b/README.md index 81e1e02..36b6804 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso - Layer `full`: rows used instead of `rows` in full-keyboard mode - `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout) - Top-level `alternates`: long-press choices per letter, after the keymap's own -- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "rows"}`; the user picks which show +- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "fillHeight", "rows"}`; the user picks which show; `fillHeight` stretches a right-hand block over the full keyboard height ## Settings diff --git a/layouts/template.json b/layouts/template.json index b77c59e..09aa80b 100644 --- a/layouts/template.json +++ b/layouts/template.json @@ -66,6 +66,7 @@ { "id": "numpad", "name": "Numeric keypad", + "fillHeight": true, "rows": [ ["7", "8", "9", "/", {"type": "backspace"}], ["4", "5", "6", "*", "-"], diff --git a/src/layout/KeyGeometry.cpp b/src/layout/KeyGeometry.cpp index 981ddf5..882de32 100644 --- a/src/layout/KeyGeometry.cpp +++ b/src/layout/KeyGeometry.cpp @@ -8,7 +8,7 @@ namespace { qreal blockUnits(const Block &block) { qreal units = 0; - for (const Row &row : block) + for (const Row &row : block.rows) units = std::max(units, rowUnits(row)); return units; } @@ -44,9 +44,13 @@ qreal totalUnits(const QList &blocks) qsizetype totalRows(const QList &blocks) { + // Filling blocks only count when nothing else is shown. + const bool allFill = std::ranges::all_of(blocks, &Block::fillHeight); qsizetype rows = 0; - for (const Block &block : blocks) - rows = std::max(rows, block.size()); + for (const Block &block : blocks) { + if (allFill || !block.fillHeight) + rows = std::max(rows, block.rows.size()); + } return rows; } @@ -65,13 +69,17 @@ 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 qreal rowHeight = (innerHeight - qreal(maxRows - 1) * metrics.gap) / qreal(maxRows); + const auto heightFor = [&](qsizetype rows) { return (innerHeight - qreal(rows - 1) * metrics.gap) / qreal(rows); }; + const qreal sharedRowHeight = heightFor(maxRows); qreal left = metrics.padding; for (const Block &block : blocks) { const qreal width = blockUnits(block); - qreal y = metrics.padding + qreal(maxRows - block.size()) * (rowHeight + metrics.gap); - for (const Row &row : 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 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 58ae586..db04cbd 100644 --- a/src/layout/KeyGeometry.h +++ b/src/layout/KeyGeometry.h @@ -12,22 +12,25 @@ struct KeyboardMetrics { }; // The keys on screen: blocks of rows, side by side. -using Block = QList; +struct Block { + QList rows; + bool fillHeight = false; // rows grow to span the full height instead of sharing the row height +}; // 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; -// all rows share one height, 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. +// 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. QList computeKeyRects(const QList &blocks, QSizeF size, KeyboardMetrics metrics = {}); inline QList computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {}) { - return computeKeyRects({layer.rows}, size, metrics); + return computeKeyRects({{layer.rows}}, size, metrics); } // Total width of a row in key units, before stretching. qreal rowUnits(const Row &row); // Width of all blocks in key units, ignoring gaps. qreal totalUnits(const QList &blocks); -// Rows of the tallest block. +// Rows of the tallest block that sets the row height (see computeKeyRects). qsizetype totalRows(const QList &blocks); diff --git a/src/layout/Layout.h b/src/layout/Layout.h index e5ee3e0..7dc2900 100644 --- a/src/layout/Layout.h +++ b/src/layout/Layout.h @@ -75,6 +75,7 @@ struct Section { QString id; // "main" is reserved for the layers QString name; // shown in settings Placement placement = Placement::Right; + bool fillHeight = false; // a right-hand block whose rows span the whole keyboard height QList rows; }; diff --git a/src/layout/LayoutLoader.cpp b/src/layout/LayoutLoader.cpp index 796a55d..c1769ba 100644 --- a/src/layout/LayoutLoader.cpp +++ b/src/layout/LayoutLoader.cpp @@ -161,6 +161,7 @@ std::optional
parseSection(const QJsonObject &obj, QString *error) return std::nullopt; } section.placement = placement == u"above" ? Section::Placement::Above : Section::Placement::Right; + section.fillHeight = obj.value(u"fillHeight").toBool(false); if (!parseRows(obj.value(u"rows"), §ion.rows, error)) { *error = QStringLiteral("section \"%1\": %2").arg(section.id, *error); return std::nullopt; diff --git a/src/layout/LayoutModel.cpp b/src/layout/LayoutModel.cpp index 87a064d..c11b6a5 100644 --- a/src/layout/LayoutModel.cpp +++ b/src/layout/LayoutModel.cpp @@ -181,21 +181,21 @@ QList LayoutModel::composeBlocks() const Block main; for (const Section §ion : m_layout.sections) { if (section.placement == Section::Placement::Above && shown(section.id)) - main += section.rows; + main.rows += section.rows; } if (layer && shown(MainSectionId)) - main += layer->rows; + main.rows += layer->rows; QList blocks; - if (!main.isEmpty()) + if (!main.rows.isEmpty()) blocks.append(main); for (const Section §ion : m_layout.sections) { if (section.placement == Section::Placement::Right && shown(section.id)) - blocks.append(section.rows); + blocks.append({section.rows, section.fillHeight}); } // Never show nothing. if (blocks.isEmpty() && layer) - blocks.append(layer->rows); + blocks.append({layer->rows}); return blocks; } @@ -205,7 +205,7 @@ void LayoutModel::rebuild() m_blocks = composeBlocks(); m_keys.clear(); for (const Block &block : std::as_const(m_blocks)) { - for (const Row &row : block) { + for (const Row &row : block.rows) { for (const KeyDef &key : row) { if (key.type != KeyType::Spacer) m_keys.append(key); @@ -227,7 +227,7 @@ void LayoutModel::relayout() const QList all = computeKeyRects(m_blocks, m_size); qsizetype i = 0; for (const Block &block : std::as_const(m_blocks)) { - for (const Row &row : block) { + for (const Row &row : block.rows) { for (const KeyDef &key : row) { if (key.type != KeyType::Spacer) m_rects.append(all[i]); diff --git a/tests/tst_keygeometry.cpp b/tests/tst_keygeometry.cpp index 71d1a92..800dd3e 100644 --- a/tests/tst_keygeometry.cpp +++ b/tests/tst_keygeometry.cpp @@ -62,13 +62,26 @@ private Q_SLOTS: void blocksShareUnitAndSitAtTheBottom() { - const Block tall{{key(), key()}, {key(), key()}}; - const Block shortBlock{{key()}}; + const Block tall{{{key(), key()}, {key(), key()}}}; + const Block shortBlock{{{key()}}}; const auto rects = computeKeyRects({tall, shortBlock}, {400, 100}, {.padding = 0, .gap = 0, .blockGap = 100}); // 3 units in 300 px: 100 each. QCOMPARE(rects[1].right(), 200.0); QCOMPARE(rects[4], QRectF(300, 50, 100, 50)); } + + void fillingBlockSpansTheFullHeight() + { + const Block tall{{{key()}, {key()}, {key()}, {key()}}}; + const Block filling{{{key()}, {key()}}, true}; + const auto rects = computeKeyRects({tall, filling}, {200, 100}, {.padding = 0, .gap = 0, .blockGap = 0}); + QCOMPARE(rects[0].height(), 25.0); + QCOMPARE(rects[4], QRectF(100, 0, 100, 50)); + QCOMPARE(rects[5], QRectF(100, 50, 100, 50)); + // A filling block taller than the rest doesn't shrink the shared rows. + QCOMPARE(totalRows({tall, filling}), 4); + QCOMPARE(totalRows({filling}), 2); + } }; QTEST_GUILESS_MAIN(TestKeyGeometry)