LibGUI: Support variable-width fonts in TextEditor

This patch reworks metric and coordinate computation to iterate over
text content instead of making assumptions about fixed glyph widths.
This commit is contained in:
Andreas Kling 2020-05-18 16:38:28 +02:00
parent 7207276697
commit 2e11c16be4
Notes: sideshowbarker 2024-07-19 06:29:56 +09:00
2 changed files with 47 additions and 32 deletions

View File

@ -159,27 +159,34 @@ TextPosition TextEditor::text_position_at(const Gfx::Point& a_position) const
line_index = max((size_t)0, min(line_index, line_count() - 1));
size_t column_index;
size_t column_index = 0;
switch (m_text_alignment) {
case Gfx::TextAlignment::CenterLeft:
if (position.x() <= 0)
column_index = 0;
else
column_index = (position.x() + glyph_width() / 2) / glyph_width();
if (is_line_wrapping_enabled()) {
for_each_visual_line(line_index, [&](const Gfx::Rect& rect, auto&, size_t start_of_line) {
if (rect.contains_vertically(position.y())) {
column_index += start_of_line;
return IterationDecision::Break;
for_each_visual_line(line_index, [&](const Gfx::Rect& rect, auto& view, size_t start_of_line) {
if (rect.contains_vertically(position.y())) {
column_index = start_of_line;
if (position.x() <= 0) {
// We're outside the text on the left side, put cursor at column 0 on this visual line.
} else {
int glyph_x = 0;
size_t i = 0;
for (; i < view.length(); ++i) {
int advance = font().glyph_width(view.codepoints()[i]) + font().glyph_spacing();
if ((glyph_x + (advance / 2)) >= position.x())
break;
glyph_x += advance;
}
column_index += i;
}
return IterationDecision::Continue;
});
}
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
break;
case Gfx::TextAlignment::CenterRight:
// FIXME: Support right-aligned line wrapping, I guess.
ASSERT(!is_line_wrapping_enabled());
column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width();
column_index = (position.x() - content_x_for_position({ line_index, 0 }) + fixed_glyph_width() / 2) / fixed_glyph_width();
break;
default:
ASSERT_NOT_REACHED();
@ -440,9 +447,9 @@ void TextEditor::paint_event(PaintEvent& event)
auto color = palette().color(is_enabled() ? foreground_role() : Gfx::ColorRole::DisabledText);
painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, color);
} else {
int advance = font().glyph_width(' ') + font().glyph_spacing();
Gfx::Rect character_rect = { visual_line_rect.location(), { font().glyph_width(' '), line_height() } };
Gfx::Rect character_rect = { visual_line_rect.location(), { 0, line_height() } };
for (size_t i = 0; i < visual_line_text.length(); ++i) {
u32 codepoint = visual_line_text.substring_view(i, 1).codepoints()[0];
const Gfx::Font* font = &this->font();
Color color;
Optional<Color> background_color;
@ -459,16 +466,17 @@ void TextEditor::paint_event(PaintEvent& event)
underline = span.is_underlined;
break;
}
character_rect.set_width(font->glyph_width(codepoint) + font->glyph_spacing());
if (background_color.has_value())
painter.fill_rect(character_rect, background_color.value());
painter.draw_text(character_rect, visual_line_text.substring_view(i, 1), *font, m_text_alignment, color);
if (underline) {
painter.draw_line(character_rect.bottom_left().translated(0, 1), character_rect.bottom_right().translated(0, 1), color);
}
character_rect.move_by(advance, 0);
character_rect.move_by(character_rect.width(), 0);
}
}
bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
if (physical_line_has_selection) {
size_t start_of_selection_within_visual_line = (size_t)max(0, (int)selection_start_column_within_line - (int)start_of_visual_line);
size_t end_of_selection_within_visual_line = selection_end_column_within_line - start_of_visual_line;
@ -928,12 +936,18 @@ void TextEditor::do_delete()
int TextEditor::content_x_for_position(const TextPosition& position) const
{
auto& line = this->line(position.line());
int x_offset = -1;
int x_offset = 0;
switch (m_text_alignment) {
case Gfx::TextAlignment::CenterLeft:
for_each_visual_line(position.line(), [&](const Gfx::Rect&, auto& view, size_t start_of_visual_line) {
if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
x_offset = (position.column() - start_of_visual_line) * glyph_width();
for_each_visual_line(position.line(), [&](const Gfx::Rect&, auto& visual_line_view, size_t start_of_visual_line) {
size_t offset_in_visual_line = position.column() - start_of_visual_line;
if (position.column() >= start_of_visual_line && (offset_in_visual_line <= visual_line_view.length())) {
if (offset_in_visual_line == 0) {
x_offset = 0;
} else {
x_offset = font().width(visual_line_view.substring_view(0, offset_in_visual_line));
x_offset += font().glyph_spacing();
}
return IterationDecision::Break;
}
return IterationDecision::Continue;
@ -942,7 +956,7 @@ int TextEditor::content_x_for_position(const TextPosition& position) const
case Gfx::TextAlignment::CenterRight:
// FIXME
ASSERT(!is_line_wrapping_enabled());
return content_width() - m_horizontal_content_padding - (line.length() * glyph_width()) + (position.column() * glyph_width());
return content_width() - m_horizontal_content_padding - (line.length() * fixed_glyph_width()) + (position.column() * fixed_glyph_width());
default:
ASSERT_NOT_REACHED();
}
@ -1016,7 +1030,7 @@ Gfx::Rect TextEditor::line_content_rect(size_t line_index) const
{
auto& line = this->line(line_index);
if (is_single_line()) {
Gfx::Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, (int)line.length() * glyph_width(), font().glyph_height() + 2 };
Gfx::Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, font().width(line.view()), font().glyph_height() + 2 };
line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
return line_rect;
}
@ -1025,7 +1039,7 @@ Gfx::Rect TextEditor::line_content_rect(size_t line_index) const
return {
content_x_for_position({ line_index, 0 }),
(int)line_index * line_height(),
(int)line.length() * glyph_width(),
font().width(line.view()),
line_height()
};
}
@ -1385,15 +1399,16 @@ void TextEditor::recompute_visual_lines(size_t line_index)
if (is_line_wrapping_enabled()) {
int line_width_so_far = 0;
auto glyph_spacing = font().glyph_spacing();
for (size_t i = 0; i < line.length(); ++i) {
auto codepoint = line.codepoints()[i];
auto glyph_width = font().glyph_or_emoji_width(codepoint);
if ((line_width_so_far + glyph_width) > available_width) {
if ((line_width_so_far + glyph_width + glyph_spacing) > available_width) {
visual_data.visual_line_breaks.append(i);
line_width_so_far = glyph_width;
line_width_so_far = glyph_width + glyph_spacing;
continue;
}
line_width_so_far += glyph_width;
line_width_so_far += glyph_width + glyph_spacing;
}
}
@ -1558,9 +1573,10 @@ int TextEditor::line_height() const
return font().glyph_height() + m_line_spacing;
}
int TextEditor::glyph_width() const
int TextEditor::fixed_glyph_width() const
{
return font().glyph_width('x');
ASSERT(font().is_fixed_width());
return font().glyph_width(' ');
}
}

View File

@ -85,8 +85,6 @@ public:
int line_height() const;
TextPosition cursor() const { return m_cursor; }
TextRange normalized_selection() const { return m_selection.normalized(); }
// FIXME: This should take glyph spacing into account, no?
int glyph_width() const;
void insert_at_cursor_or_replace_selection(const StringView&);
bool write_to_file(const StringView& path);
@ -172,6 +170,7 @@ private:
void paint_ruler(Painter&);
void update_content_size();
void did_change();
int fixed_glyph_width() const;
Gfx::Rect line_content_rect(size_t item_index) const;
Gfx::Rect line_widget_rect(size_t line_index) const;