## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
115 lines
4.7 KiB
C++
115 lines
4.7 KiB
C++
#include "TextBlock.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <Logging.h>
|
|
#include <Serialization.h>
|
|
|
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
|
// Validate iterator bounds before rendering
|
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
|
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
|
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < words.size(); i++) {
|
|
const int wordX = wordXpos[i] + x;
|
|
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
|
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
|
|
|
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
|
const std::string& w = words[i];
|
|
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
|
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
|
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
|
|
|
int startX = wordX;
|
|
int underlineWidth = fullWordWidth;
|
|
|
|
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
|
|
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
|
static_cast<uint8_t>(w[2]) == 0x83) {
|
|
const char* visiblePtr = w.c_str() + 3;
|
|
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
|
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
|
startX = wordX + prefixWidth;
|
|
underlineWidth = visibleWidth;
|
|
}
|
|
|
|
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TextBlock::serialize(FsFile& file) const {
|
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
|
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
|
wordXpos.size(), wordStyles.size());
|
|
return false;
|
|
}
|
|
|
|
// Word data
|
|
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
|
for (const auto& w : words) serialization::writeString(file, w);
|
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
|
|
|
// Style (alignment + margins/padding/indent)
|
|
serialization::writePod(file, blockStyle.alignment);
|
|
serialization::writePod(file, blockStyle.textAlignDefined);
|
|
serialization::writePod(file, blockStyle.marginTop);
|
|
serialization::writePod(file, blockStyle.marginBottom);
|
|
serialization::writePod(file, blockStyle.marginLeft);
|
|
serialization::writePod(file, blockStyle.marginRight);
|
|
serialization::writePod(file, blockStyle.paddingTop);
|
|
serialization::writePod(file, blockStyle.paddingBottom);
|
|
serialization::writePod(file, blockStyle.paddingLeft);
|
|
serialization::writePod(file, blockStyle.paddingRight);
|
|
serialization::writePod(file, blockStyle.textIndent);
|
|
serialization::writePod(file, blockStyle.textIndentDefined);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|
uint16_t wc;
|
|
std::vector<std::string> words;
|
|
std::vector<int16_t> wordXpos;
|
|
std::vector<EpdFontFamily::Style> wordStyles;
|
|
BlockStyle blockStyle;
|
|
|
|
// Word count
|
|
serialization::readPod(file, wc);
|
|
|
|
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
|
if (wc > 10000) {
|
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
|
return nullptr;
|
|
}
|
|
|
|
// Word data
|
|
words.resize(wc);
|
|
wordXpos.resize(wc);
|
|
wordStyles.resize(wc);
|
|
for (auto& w : words) serialization::readString(file, w);
|
|
for (auto& x : wordXpos) serialization::readPod(file, x);
|
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
|
|
|
// Style (alignment + margins/padding/indent)
|
|
serialization::readPod(file, blockStyle.alignment);
|
|
serialization::readPod(file, blockStyle.textAlignDefined);
|
|
serialization::readPod(file, blockStyle.marginTop);
|
|
serialization::readPod(file, blockStyle.marginBottom);
|
|
serialization::readPod(file, blockStyle.marginLeft);
|
|
serialization::readPod(file, blockStyle.marginRight);
|
|
serialization::readPod(file, blockStyle.paddingTop);
|
|
serialization::readPod(file, blockStyle.paddingBottom);
|
|
serialization::readPod(file, blockStyle.paddingLeft);
|
|
serialization::readPod(file, blockStyle.paddingRight);
|
|
serialization::readPod(file, blockStyle.textIndent);
|
|
serialization::readPod(file, blockStyle.textIndentDefined);
|
|
|
|
return std::unique_ptr<TextBlock>(
|
|
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
|
|
}
|