From 7c371b04e302fdc6461d6a283ddc4c013be1470e Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Sat, 30 May 2026 13:06:55 +0200 Subject: [PATCH] C++, Spec: add support for automatic word wrap --- Specification.rst | 2 ++ cpp/src/monoformat_parseonly.cpp | 56 ++++++++++++++++++++++++++++- cpp/src/monoformat_schema.hpp | 3 ++ cpp/src/monoformat_structured.cpp | 59 +++++++++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/Specification.rst b/Specification.rst index 749e7fc..89db326 100644 --- a/Specification.rst +++ b/Specification.rst @@ -491,6 +491,8 @@ Text Flags +===========+===================================================+ | ``0`` | Text is dark, background is white | +-----------+---------------------------------------------------+ +| ``1`` | Enable automatic word wrap | ++-----------+---------------------------------------------------+ | ``1`` .. | Reserved for future use | | ``7`` | | +-----------+---------------------------------------------------+ diff --git a/cpp/src/monoformat_parseonly.cpp b/cpp/src/monoformat_parseonly.cpp index 10e6652..3f3c992 100644 --- a/cpp/src/monoformat_parseonly.cpp +++ b/cpp/src/monoformat_parseonly.cpp @@ -1109,7 +1109,61 @@ std::expected handleClippedText(std::span& bu ClippedImage target{screen, *x, *y, *width, *height}; auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); - renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + if (textFlags.autoWordWrap) { + auto dimensions = renderer.getRenderedDimensions(text, Point{0, 0}); + if (!dimensions) { + return {}; + } + std::int32_t y = 0; + while (dimensions->boundingBox && dimensions->boundingBox->size.width > (*width)) { + std::size_t pos = text.find('\n'); + if (pos == std::string_view::npos) { + pos = text.size(); + } + std::string_view currentLine = text.substr(0, pos); + if (pos < text.size()) { + text = text.substr(pos + 1); + } else { + text = text.substr(pos); + } + while (currentLine.size() > 0) { + dimensions = renderer.getRenderedDimensions(currentLine, Point{0, 0}); + if (!dimensions) { + return {}; + } + std::size_t pos = currentLine.size(); + while (pos > 0 && dimensions->boundingBox && dimensions->boundingBox->size.width > (*width)) { + std::size_t prevPos = pos; + pos = currentLine.rfind(' ', prevPos - 1); + if (pos == std::string_view::npos) { + pos = prevPos; + break; + } + dimensions = renderer.getRenderedDimensions(currentLine.substr(0, pos), Point{0, 0}); + if (!dimensions) { + return {}; + } + } + renderer.render(currentLine.substr(0, pos), Point{0, y + static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + y += renderer.lineHeight(); + if (pos < currentLine.size()) { + currentLine = currentLine.substr(pos + 1); + } else { + currentLine = currentLine.substr(pos); + } + } + dimensions = renderer.getRenderedDimensions(text, Point{0, 0}); + if (!dimensions) { + return {}; + } + } + if (text.size() > 0) { + renderer.render(text, Point{0, y + static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + y += renderer.lineHeight(); + } + } else { + renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + } return {}; } diff --git a/cpp/src/monoformat_schema.hpp b/cpp/src/monoformat_schema.hpp index 5b4f9f7..bfbd412 100644 --- a/cpp/src/monoformat_schema.hpp +++ b/cpp/src/monoformat_schema.hpp @@ -101,17 +101,20 @@ struct FillFlags { struct TextFlags { bool dark{}; + bool autoWordWrap{}; TextFlags() = default; explicit TextFlags(std::uint8_t flags) noexcept : dark{isBitSet(flags, 0)} + , autoWordWrap{isBitSet(flags, 1)} { } explicit operator std::uint8_t() const noexcept { std::uint8_t result{}; maybeSetBit(result, 0, dark); + maybeSetBit(result, 1, autoWordWrap); return result; } }; diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 05e52a0..f2ccdea 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -42,7 +42,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(FillPattern, { NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ScrollFlags, endless, invertDirection, padBefore, padAfter) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LineFlags, dark) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(FillFlags, dark) -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TextFlags, dark) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TextFlags, dark, autoWordWrap) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TimeDisplayFlags, use12h, showHours, showMinutes, showSeconds) ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) @@ -1466,7 +1466,62 @@ void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t bool fgColor = !m_textFlags.dark; auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); - renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + if (m_textFlags.autoWordWrap) { + std::string_view text = m_text; + auto dimensions = renderer.getRenderedDimensions(text, Point{0, 0}); + if (!dimensions) { + return; + } + std::int32_t y = 0; + while (dimensions->boundingBox && dimensions->boundingBox->size.width > m_width) { + std::size_t pos = text.find('\n'); + if (pos == std::string_view::npos) { + pos = text.size(); + } + std::string_view currentLine = text.substr(0, pos); + if (pos < text.size()) { + text = text.substr(pos + 1); + } else { + text = text.substr(pos); + } + while (currentLine.size() > 0) { + dimensions = renderer.getRenderedDimensions(currentLine, Point{0, 0}); + if (!dimensions) { + return; + } + std::size_t pos = currentLine.size(); + while (pos > 0 && dimensions->boundingBox && dimensions->boundingBox->size.width > m_width) { + std::size_t prevPos = pos; + pos = currentLine.rfind(' ', prevPos - 1); + if (pos == std::string_view::npos) { + pos = prevPos; + break; + } + dimensions = renderer.getRenderedDimensions(currentLine.substr(0, pos), Point{0, 0}); + if (!dimensions) { + return; + } + } + renderer.render(currentLine.substr(0, pos), Point{0, y + static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + y += renderer.lineHeight(); + if (pos < currentLine.size()) { + currentLine = currentLine.substr(pos + 1); + } else { + currentLine = currentLine.substr(pos); + } + } + dimensions = renderer.getRenderedDimensions(text, Point{0, 0}); + if (!dimensions) { + return; + } + } + if (text.size() > 0) { + renderer.render(text, Point{0, y + static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + y += renderer.lineHeight(); + } + } else { + renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 2)}, target, fgColor, bgColor); + } } std::expected, ParseError> ClippedTextElement::parse(std::span& buffer, std::uint32_t formatVersion) {