From 2e11c16be4bd58fb5668728c5664cd4a319dd316 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 18 May 2020 16:38:28 +0200 Subject: [PATCH] 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. --- Libraries/LibGUI/TextEditor.cpp | 76 ++++++++++++++++++++------------- Libraries/LibGUI/TextEditor.h | 3 +- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/Libraries/LibGUI/TextEditor.cpp b/Libraries/LibGUI/TextEditor.cpp index 83f84234eb..f3b13503eb 100644 --- a/Libraries/LibGUI/TextEditor.cpp +++ b/Libraries/LibGUI/TextEditor.cpp @@ -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 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(' '); } } diff --git a/Libraries/LibGUI/TextEditor.h b/Libraries/LibGUI/TextEditor.h index c18ec59845..65e2ef6128 100644 --- a/Libraries/LibGUI/TextEditor.h +++ b/Libraries/LibGUI/TextEditor.h @@ -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;