From b4738fee0c9a0c8a4aab2182d025e31b5878142a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 20 May 2026 01:04:28 +0200 Subject: [PATCH] Tighten layout --- src/components/CardLayout.cpp | 113 +++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 15 deletions(-) diff --git a/src/components/CardLayout.cpp b/src/components/CardLayout.cpp index 3c8d75d5..eb59687f 100644 --- a/src/components/CardLayout.cpp +++ b/src/components/CardLayout.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "fontIds.h" CardLayout::CardLayout(GfxRenderer& renderer, Rect contentRect, int startY, CardLayoutConfig cfg) @@ -43,23 +46,103 @@ void CardLayout::Body::rowLR(const char* label, const std::string& value) { innerY += layout.rowStep_; } -void CardLayout::Body::statGrid(const std::array, 4>& cells) { - const int cellW = layout.innerWidth_ / 4; - const int valueY = innerY; - const int labelY = innerY + layout.lineH_ + 2; - for (int i = 0; i < 4; ++i) { - const auto& [value, label] = cells[i]; - const int cellCenterX = layout.innerLeft_ + cellW * i + cellW / 2; - const int vw = layout.renderer_.getTextWidth(UI_12_FONT_ID, value.c_str(), EpdFontFamily::BOLD); - const int lw = layout.renderer_.getTextWidth(UI_10_FONT_ID, label); - layout.renderer_.drawText(UI_12_FONT_ID, cellCenterX - vw / 2, valueY, value.c_str(), true, EpdFontFamily::BOLD); - layout.renderer_.drawText(UI_10_FONT_ID, cellCenterX - lw / 2, labelY, label); - if (i < 3) { - const int divX = layout.innerLeft_ + cellW * (i + 1); - layout.renderer_.drawLine(divX, valueY - 2, divX, labelY + layout.lineH_, true); +namespace { +// Split `label` into 1-2 centered lines that fit within `maxWidth`. Splits +// at the last space whose prefix still fits; if no such split exists (single +// long word) we fall back to character-wise truncation of the original with +// an ellipsis. The two-line variant is preferred whenever both halves fit +// inside the cell, so "Books tracked" wraps rather than getting clipped. +struct WrappedLabel { + std::string line1; + std::string line2; // empty when single-line +}; + +WrappedLabel wrapLabel(const GfxRenderer& renderer, const char* label, int maxWidth, int fontId) { + WrappedLabel out; + if (!label || !*label) return out; + if (renderer.getTextWidth(fontId, label) <= maxWidth) { + out.line1 = label; + return out; + } + + // Find the last whitespace whose prefix fits and whose suffix also fits. + const std::string s(label); + size_t bestSplit = std::string::npos; + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] != ' ') continue; + const std::string left = s.substr(0, i); + const std::string right = s.substr(i + 1); + if (renderer.getTextWidth(fontId, left.c_str()) <= maxWidth && + renderer.getTextWidth(fontId, right.c_str()) <= maxWidth) { + bestSplit = i; } } - innerY = labelY + layout.lineH_ + 4; + if (bestSplit != std::string::npos) { + out.line1 = s.substr(0, bestSplit); + out.line2 = s.substr(bestSplit + 1); + return out; + } + + // No good split — truncate the single token. Keep dropping characters + // until label + "…" fits. + const int ellipsisW = renderer.getTextWidth(fontId, "…"); + std::string truncated = s; + while (!truncated.empty() && + renderer.getTextWidth(fontId, truncated.c_str()) + ellipsisW > maxWidth) { + truncated.pop_back(); + } + out.line1 = truncated + "…"; + return out; +} +} // namespace + +void CardLayout::Body::statGrid(const std::array, 4>& cells) { + // Labels use SMALL_FONT_ID so multi-word labels like "Pages turned" / + // "Books tracked" / "Avg session" usually fit on one line. The two-line + // wrap below is still the fallback for genuinely long labels. + constexpr int kLabelFont = SMALL_FONT_ID; + const int labelLineH = layout.renderer_.getLineHeight(kLabelFont); + + const int cellW = layout.innerWidth_ / 4; + // Leave a small horizontal breathing room inside each cell so wrapped + // labels don't kiss the vertical divider on the next cell over. + constexpr int kLabelPadX = 4; + const int labelMaxW = std::max(0, cellW - kLabelPadX * 2); + + // Pre-wrap so we can compute the row's vertical extent before drawing the + // dividers (which span both label lines whenever any cell wraps). + std::array wrapped; + bool anyTwoLines = false; + for (int i = 0; i < 4; ++i) { + wrapped[i] = wrapLabel(layout.renderer_, cells[i].second, labelMaxW, kLabelFont); + if (!wrapped[i].line2.empty()) anyTwoLines = true; + } + + const int valueY = innerY; + const int labelY = innerY + layout.lineH_ + 2; + const int labelLine2Y = labelY + labelLineH; + const int gridBottom = (anyTwoLines ? labelLine2Y : labelY) + labelLineH; + + for (int i = 0; i < 4; ++i) { + const auto& [value, _label] = cells[i]; + const int cellCenterX = layout.innerLeft_ + cellW * i + cellW / 2; + const int vw = layout.renderer_.getTextWidth(UI_12_FONT_ID, value.c_str(), EpdFontFamily::BOLD); + layout.renderer_.drawText(UI_12_FONT_ID, cellCenterX - vw / 2, valueY, value.c_str(), true, EpdFontFamily::BOLD); + + const auto& w = wrapped[i]; + const int lw1 = layout.renderer_.getTextWidth(kLabelFont, w.line1.c_str()); + layout.renderer_.drawText(kLabelFont, cellCenterX - lw1 / 2, labelY, w.line1.c_str()); + if (!w.line2.empty()) { + const int lw2 = layout.renderer_.getTextWidth(kLabelFont, w.line2.c_str()); + layout.renderer_.drawText(kLabelFont, cellCenterX - lw2 / 2, labelLine2Y, w.line2.c_str()); + } + + if (i < 3) { + const int divX = layout.innerLeft_ + cellW * (i + 1); + layout.renderer_.drawLine(divX, valueY - 2, divX, gridBottom, true); + } + } + innerY = gridBottom + 4; } void CardLayout::Body::centeredMessage(const char* msg) {