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:
parent
4c9c85ac01
commit
7696e96c7f
1 changed files with 75 additions and 30 deletions
|
@ -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())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue