Fix alignment issues

This commit is contained in:
jpirnay
2026-04-05 11:18:19 +02:00
parent 01503f9c84
commit 65474d4066
2 changed files with 112 additions and 26 deletions
+105 -26
View File
@@ -8,6 +8,7 @@
#include <cmath>
#include <functional>
#include <limits>
#include <set>
#include <vector>
#include "hyphenation/Hyphenator.h"
@@ -77,6 +78,8 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
std::string buildLinePreview(const std::vector<std::string>& words, const std::vector<bool>& continuesVec,
const size_t start, const size_t endExclusive, const size_t maxLen = 120) {
// Build a readable line preview while preserving continuation semantics
// (no synthetic spaces before attached tokens).
std::string preview;
for (size_t idx = start; idx < endExclusive; ++idx) {
if (idx > start && idx < continuesVec.size() && !continuesVec[idx]) {
@@ -150,26 +153,55 @@ void ParsedText::layoutAndExtractLines(
const std::string firstAttemptPreview = buildLinePreview(words, wordContinues, lineStart, lineEnd);
LOG_DBG("PTX", "Line %u requested rerender without hyphenation, first attempt: %s", static_cast<unsigned>(i),
firstAttemptPreview.c_str());
// Undo the split used to end this line so it can be relaid without hyphenation.
const int splitPrefixIndex = i < splitPrefixWordIndexes.size() ? splitPrefixWordIndexes[i] : -1;
if (splitPrefixIndex >= 0 && static_cast<size_t>(splitPrefixIndex + 1) < words.size()) {
std::string merged = words[splitPrefixIndex];
if (i < splitInsertedHyphen.size() && splitInsertedHyphen[i] && !merged.empty() && merged.back() == '-') {
// Undo precomputed splits from this line onward so the retry starts from
// clean, unsplit tokens and cannot inherit future hyphenation artifacts.
std::set<int, std::greater<int>> splitIndexesToUndo;
for (size_t lineIdx = i; lineIdx < splitPrefixWordIndexes.size(); ++lineIdx) {
const int splitIndex = splitPrefixWordIndexes[lineIdx];
if (splitIndex >= 0) {
splitIndexesToUndo.insert(splitIndex);
}
}
for (const int splitIndex : splitIndexesToUndo) {
if (splitIndex < 0 || static_cast<size_t>(splitIndex + 1) >= words.size()) {
continue;
}
bool removeInsertedHyphen = false;
for (size_t lineIdx = i; lineIdx < splitPrefixWordIndexes.size(); ++lineIdx) {
if (splitPrefixWordIndexes[lineIdx] == splitIndex && lineIdx < splitInsertedHyphen.size()) {
removeInsertedHyphen = splitInsertedHyphen[lineIdx];
break;
}
}
std::string merged = words[splitIndex];
if (removeInsertedHyphen && !merged.empty() && merged.back() == '-') {
merged.pop_back();
}
merged += words[splitPrefixIndex + 1];
words[splitPrefixIndex] = std::move(merged);
words.erase(words.begin() + splitPrefixIndex + 1);
wordStyles.erase(wordStyles.begin() + splitPrefixIndex + 1);
wordContinues.erase(wordContinues.begin() + splitPrefixIndex + 1);
merged += words[splitIndex + 1];
words[splitIndex] = std::move(merged);
words.erase(words.begin() + splitIndex + 1);
wordStyles.erase(wordStyles.begin() + splitIndex + 1);
wordContinues.erase(wordContinues.begin() + splitIndex + 1);
}
// Re-layout remaining output without hyphenation for this pass.
// Recompute widths after restoring unsplit words.
wordWidths = calculateWordWidths(renderer, fontId);
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
lineEndsWithHyphenatedWord.assign(lineBreakIndices.size(), false);
splitPrefixWordIndexes.assign(lineBreakIndices.size(), -1);
splitInsertedHyphen.assign(lineBreakIndices.size(), false);
// Keep previous lines fixed; recompute only this specific line without hyphenation.
// Suppression is intentionally line-local.
const size_t retryBreak =
computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues, lineStart);
lineBreakIndices.resize(i + 1);
lineEndsWithHyphenatedWord.resize(i + 1);
splitPrefixWordIndexes.resize(i + 1);
splitInsertedHyphen.resize(i + 1);
lineBreakIndices[i] = retryBreak;
lineEndsWithHyphenatedWord[i] = false;
splitPrefixWordIndexes[i] = -1;
splitInsertedHyphen[i] = false;
lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
if (i < lineCount) {
@@ -181,7 +213,7 @@ void ParsedText::layoutAndExtractLines(
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId, false,
true);
// Continue with regular hyphenation for subsequent lines only.
// Resume regular hyphenation from the first word after the retried line.
const size_t resumeIndex = lineBreakIndices[i];
std::vector<bool> suffixLineEndsWithHyphenatedWord;
std::vector<int> suffixSplitPrefixWordIndexes;
@@ -190,15 +222,6 @@ void ParsedText::layoutAndExtractLines(
renderer, fontId, pageWidth, wordWidths, wordContinues, resumeIndex, suffixLineEndsWithHyphenatedWord,
suffixSplitPrefixWordIndexes, suffixSplitInsertedHyphen);
lineBreakIndices.resize(i + 1);
lineEndsWithHyphenatedWord.resize(i + 1);
splitPrefixWordIndexes.resize(i + 1);
splitInsertedHyphen.resize(i + 1);
lineEndsWithHyphenatedWord[i] = false;
splitPrefixWordIndexes[i] = -1;
splitInsertedHyphen[i] = false;
lineBreakIndices.insert(lineBreakIndices.end(), hyphenatedSuffixBreaks.begin(), hyphenatedSuffixBreaks.end());
lineEndsWithHyphenatedWord.insert(lineEndsWithHyphenatedWord.end(), suffixLineEndsWithHyphenatedWord.begin(),
suffixLineEndsWithHyphenatedWord.end());
@@ -359,6 +382,58 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
return lineBreakIndices;
}
size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, const int fontId, const int pageWidth,
const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec,
const size_t lineStartIndex) const {
// One-line non-hyphenating breaker used by the page-boundary retry path.
if (lineStartIndex >= wordWidths.size()) {
return lineStartIndex;
}
const int firstLineIndent =
lineStartIndex == 0 && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
const int effectivePageWidth = pageWidth - firstLineIndent;
size_t currentIndex = lineStartIndex;
int lineWidth = 0;
while (currentIndex < wordWidths.size()) {
const bool isFirstWord = currentIndex == lineStartIndex;
int spacing = 0;
if (!isFirstWord) {
if (!continuesVec[currentIndex]) {
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
} else {
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
}
}
const int candidateWidth = spacing + wordWidths[currentIndex];
if (lineWidth + candidateWidth <= effectivePageWidth) {
lineWidth += candidateWidth;
++currentIndex;
continue;
}
if (currentIndex == lineStartIndex) {
++currentIndex;
}
break;
}
while (currentIndex > lineStartIndex + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
--currentIndex;
}
return currentIndex;
}
void ParsedText::applyParagraphIndent() {
if (extraParagraphSpacing || words.empty()) {
return;
@@ -489,6 +564,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaksFromIndex(
const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec, const size_t startIndex, std::vector<bool>& lineEndsWithHyphenatedWord,
std::vector<int>& splitPrefixWordIndexes, std::vector<bool>& splitInsertedHyphen) {
// Same greedy hyphenating breaker as the full pass, but scoped to a suffix.
if (startIndex >= wordWidths.size()) {
lineEndsWithHyphenatedWord.clear();
splitPrefixWordIndexes.clear();
@@ -707,7 +783,10 @@ ParsedText::LineProcessResult ParsedText::extractLine(
// Calculate spacing (account for indent reducing effective page width on first line)
const int effectivePageWidth = pageWidth - firstLineIndent;
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
// A line is only truly last when it consumes all paragraph words.
// During single-line retry we may temporarily pass a truncated break vector,
// so relying only on breakIndex would incorrectly disable justification.
const bool isLastLine = lineBreak == words.size();
// For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
+7
View File
@@ -35,12 +35,19 @@ class ParsedText {
std::vector<bool>& lineEndsWithHyphenatedWord,
std::vector<int>& splitPrefixWordIndexes,
std::vector<bool>& splitInsertedHyphen);
// Recompute hyphenated breaks for a suffix that starts at startIndex.
// Used after a single-line retry so later lines keep normal hyphenation.
std::vector<size_t> computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec, size_t startIndex,
std::vector<bool>& lineEndsWithHyphenatedWord,
std::vector<int>& splitPrefixWordIndexes,
std::vector<bool>& splitInsertedHyphen);
// Compute exactly one line break without hyphenating words.
// Used only for the page-boundary retry line.
size_t computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, int fontId, int pageWidth,
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
size_t lineStartIndex) const;
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
bool* outInsertedHyphen = nullptr);