1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 02:07:35 +00:00

LibGfx: Make TextLayout algorithm whitespace-aware

This converts the TextLayout algorithm from handling just words to
handling blocks of strings. With this model, whitespace is preserved
and inserted as-is, rather than being eaten and then replaced with a
single space (or none, if the whitespace was at the start of the word).

Closes #9032.
This commit is contained in:
sin-ack 2021-07-27 19:50:11 +00:00 committed by Andreas Kling
parent 4c9c85ac01
commit 7696e96c7f

View file

@ -9,8 +9,16 @@
namespace Gfx { namespace Gfx {
// HACK: We need to point to some valid memory with Utf8Views. enum class BlockType {
char const s_the_newline[] = "\n"; Newline,
Whitespace,
Word
};
struct Block {
BlockType type;
Utf8View characters;
};
IntRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) const IntRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) const
{ {
@ -34,37 +42,69 @@ IntRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) const
Vector<String, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping, int line_spacing, FitWithinRect fit_within_rect) const Vector<String, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping, int line_spacing, FitWithinRect fit_within_rect) const
{ {
Vector<Utf8View> words; Vector<Block> blocks;
Optional<size_t> start_byte_offset; Optional<BlockType> current_block_type;
size_t current_byte_offset = 0; size_t block_start_offset;
size_t offset = 0;
for (auto it = m_text.begin(); !it.done(); ++it) { for (auto it = m_text.begin(); !it.done(); ++it) {
current_byte_offset = m_text.iterator_offset(it); offset = m_text.iterator_offset(it);
switch (*it) { switch (*it) {
case '\n':
case '\r':
case '\t': case '\t':
case ' ': { case ' ': {
if (start_byte_offset.has_value()) if (current_block_type.has_value() && current_block_type.value() != BlockType::Whitespace) {
words.append(m_text.substring_view(start_byte_offset.value(), current_byte_offset - start_byte_offset.value())); blocks.append({
start_byte_offset.clear(); current_block_type.value(),
m_text.substring_view(block_start_offset, offset - block_start_offset),
});
current_block_type.clear();
}
if (*it == '\n') { if (!current_block_type.has_value()) {
words.append(Utf8View { s_the_newline }); current_block_type = BlockType::Whitespace;
block_start_offset = offset;
} }
continue; continue;
} }
case '\n':
case '\r': {
if (current_block_type.has_value()) {
blocks.append({
current_block_type.value(),
m_text.substring_view(block_start_offset, offset - block_start_offset),
});
current_block_type.clear();
}
blocks.append({ BlockType::Newline, Utf8View {} });
continue;
}
default: { default: {
if (!start_byte_offset.has_value()) if (current_block_type.has_value() && current_block_type.value() != BlockType::Word) {
start_byte_offset = current_byte_offset; blocks.append({
current_block_type.value(),
m_text.substring_view(block_start_offset, offset - block_start_offset),
});
current_block_type.clear();
}
if (!current_block_type.has_value()) {
current_block_type = BlockType::Word;
block_start_offset = offset;
}
} }
} }
} }
if (start_byte_offset.has_value()) if (current_block_type.has_value()) {
words.append(m_text.substring_view(start_byte_offset.value(), m_text.byte_length() - start_byte_offset.value())); blocks.append({
current_block_type.value(),
m_text.substring_view(block_start_offset, m_text.byte_length() - block_start_offset),
});
}
size_t max_lines_that_can_fit = 0; size_t max_lines_that_can_fit = 0;
if (m_rect.height() >= m_font->glyph_height()) { if (m_rect.height() >= m_font->glyph_height()) {
@ -81,46 +121,51 @@ Vector<String, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrap
StringBuilder builder; StringBuilder builder;
size_t line_width = 0; size_t line_width = 0;
bool did_not_finish = false; bool did_not_finish = false;
for (auto& word : words) { for (Block& block : blocks) {
switch (block.type) {
if (word.as_string() == s_the_newline) { case BlockType::Newline: {
lines.append(builder.to_string()); lines.append(builder.to_string());
builder.clear(); builder.clear();
line_width = 0; line_width = 0;
if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) {
did_not_finish = true; did_not_finish = true;
break; goto blocks_processed;
} }
} else {
size_t word_width = font().width(word); continue;
}
case BlockType::Whitespace:
case BlockType::Word: {
size_t block_width = font().width(block.characters);
if (line_width > 0) { if (line_width > 0) {
word_width += font().glyph_width('x'); block_width += font().glyph_width('x');
if (wrapping == TextWrapping::Wrap && line_width + word_width > static_cast<unsigned>(m_rect.width())) { if (wrapping == TextWrapping::Wrap && line_width + block_width > static_cast<unsigned>(m_rect.width())) {
lines.append(builder.to_string()); lines.append(builder.to_string());
builder.clear(); builder.clear();
line_width = 0; line_width = 0;
if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) {
did_not_finish = true; did_not_finish = true;
break; goto blocks_processed;
} }
} }
builder.append(' ');
} }
if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) {
did_not_finish = true; did_not_finish = true;
break; break;
} }
builder.append(word.as_string()); builder.append(block.characters.as_string());
line_width += word_width; line_width += block_width;
}
} }
} }
blocks_processed:
if (!did_not_finish) { if (!did_not_finish) {
auto last_line = builder.to_string(); auto last_line = builder.to_string();
if (!last_line.is_empty()) if (!last_line.is_empty())