From e11940fd01c6c45ccaf4994cb588becd10c154f8 Mon Sep 17 00:00:00 2001 From: sin-ack Date: Sun, 25 Jul 2021 21:20:11 +0000 Subject: Userland: Move text wrapping/elision into the new TextLayout :^) This class now contains all the fun bits about laying out text in a rect. It will handle line wrapping at a certain width, cutting off lines that don't fit the given rect, and handling text elision. Painter::draw_text now internally uses this. Future work here would be not laying out text twice (once actually preparing the lines to be rendered and once to get the bounding box), and possibly adding left elision if necessary. Additionally, this commit makes the Utf32View versions of Painter::draw_text convert to Utf8View internally. The intention is to completely remove those versions, but they're kept at the moment to keep the scope of this PR small. --- Userland/Libraries/LibGUI/AbstractButton.cpp | 4 +- Userland/Libraries/LibGUI/AbstractButton.h | 3 +- Userland/Libraries/LibGUI/Button.cpp | 2 +- Userland/Libraries/LibGUI/CheckBox.cpp | 2 +- Userland/Libraries/LibGUI/Label.cpp | 97 ++------------- Userland/Libraries/LibGUI/Label.h | 8 +- Userland/Libraries/LibGUI/RadioButton.cpp | 2 +- Userland/Libraries/LibGfx/CMakeLists.txt | 1 + Userland/Libraries/LibGfx/Painter.cpp | 174 ++++++++------------------ Userland/Libraries/LibGfx/Painter.h | 22 ++-- Userland/Libraries/LibGfx/TextLayout.cpp | 178 +++++++++++++++++++++++++++ Userland/Libraries/LibGfx/TextLayout.h | 79 ++++++++++++ Userland/Libraries/LibGfx/TextWrapping.h | 17 +++ Userland/Services/Taskbar/TaskbarButton.cpp | 8 +- 14 files changed, 365 insertions(+), 232 deletions(-) create mode 100644 Userland/Libraries/LibGfx/TextLayout.cpp create mode 100644 Userland/Libraries/LibGfx/TextLayout.h create mode 100644 Userland/Libraries/LibGfx/TextWrapping.h (limited to 'Userland') diff --git a/Userland/Libraries/LibGUI/AbstractButton.cpp b/Userland/Libraries/LibGUI/AbstractButton.cpp index cf67ad6788..eec057f9e1 100644 --- a/Userland/Libraries/LibGUI/AbstractButton.cpp +++ b/Userland/Libraries/LibGUI/AbstractButton.cpp @@ -171,7 +171,7 @@ void AbstractButton::keyup_event(KeyEvent& event) Widget::keyup_event(event); } -void AbstractButton::paint_text(Painter& painter, const Gfx::IntRect& rect, const Gfx::Font& font, Gfx::TextAlignment text_alignment) +void AbstractButton::paint_text(Painter& painter, const Gfx::IntRect& rect, const Gfx::Font& font, Gfx::TextAlignment text_alignment, Gfx::TextWrapping text_wrapping) { auto clipped_rect = rect.intersected(this->rect()); @@ -183,7 +183,7 @@ void AbstractButton::paint_text(Painter& painter, const Gfx::IntRect& rect, cons if (text().is_empty()) return; - painter.draw_text(clipped_rect, text(), font, text_alignment, palette().color(foreground_role()), Gfx::TextElision::Right); + painter.draw_text(clipped_rect, text(), font, text_alignment, palette().color(foreground_role()), Gfx::TextElision::Right, text_wrapping); } void AbstractButton::change_event(Event& event) diff --git a/Userland/Libraries/LibGUI/AbstractButton.h b/Userland/Libraries/LibGUI/AbstractButton.h index bcf450e655..421d182bca 100644 --- a/Userland/Libraries/LibGUI/AbstractButton.h +++ b/Userland/Libraries/LibGUI/AbstractButton.h @@ -7,6 +7,7 @@ #pragma once #include +#include namespace GUI { @@ -51,7 +52,7 @@ protected: virtual void leave_event(Core::Event&) override; virtual void change_event(Event&) override; - void paint_text(Painter&, const Gfx::IntRect&, const Gfx::Font&, Gfx::TextAlignment); + void paint_text(Painter&, const Gfx::IntRect&, const Gfx::Font&, Gfx::TextAlignment, Gfx::TextWrapping); private: String m_text; diff --git a/Userland/Libraries/LibGUI/Button.cpp b/Userland/Libraries/LibGUI/Button.cpp index 107eaccbe5..2175f9a21e 100644 --- a/Userland/Libraries/LibGUI/Button.cpp +++ b/Userland/Libraries/LibGUI/Button.cpp @@ -85,7 +85,7 @@ void Button::paint_event(PaintEvent& event) if (text_rect.width() > content_rect.width()) text_rect.set_width(content_rect.width()); text_rect.align_within(content_rect, text_alignment()); - paint_text(painter, text_rect, font, text_alignment()); + paint_text(painter, text_rect, font, text_alignment(), Gfx::TextWrapping::DontWrap); if (is_focused()) { Gfx::IntRect focus_rect; diff --git a/Userland/Libraries/LibGUI/CheckBox.cpp b/Userland/Libraries/LibGUI/CheckBox.cpp index 2cfd26d88c..7acf65c729 100644 --- a/Userland/Libraries/LibGUI/CheckBox.cpp +++ b/Userland/Libraries/LibGUI/CheckBox.cpp @@ -56,7 +56,7 @@ void CheckBox::paint_event(PaintEvent& event) Gfx::StylePainter::paint_check_box(painter, box_rect, palette(), is_enabled(), is_checked(), is_being_pressed()); - paint_text(painter, text_rect, font(), Gfx::TextAlignment::TopLeft); + paint_text(painter, text_rect, font(), Gfx::TextAlignment::TopLeft, Gfx::TextWrapping::DontWrap); if (is_focused()) painter.draw_focus_rect(text_rect.inflated(6, 6), palette().focus_outline()); diff --git a/Userland/Libraries/LibGUI/Label.cpp b/Userland/Libraries/LibGUI/Label.cpp index 88a829bf87..e0b9ea033e 100644 --- a/Userland/Libraries/LibGUI/Label.cpp +++ b/Userland/Libraries/LibGUI/Label.cpp @@ -4,11 +4,13 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include #include #include +#include REGISTER_WIDGET(GUI, Label) @@ -27,7 +29,6 @@ Label::Label(String text) REGISTER_STRING_PROPERTY("text", text, set_text); REGISTER_BOOL_PROPERTY("autosize", is_autosize, set_autosize); - REGISTER_BOOL_PROPERTY("word_wrap", is_word_wrap, set_word_wrap); } Label::~Label() @@ -43,15 +44,6 @@ void Label::set_autosize(bool autosize) size_to_fit(); } -void Label::set_word_wrap(bool wrap) -{ - if (m_word_wrap == wrap) - return; - m_word_wrap = wrap; - if (is_word_wrap()) - wrap_text(); -} - void Label::set_icon(const Gfx::Bitmap* icon) { if (m_icon == icon) @@ -65,21 +57,19 @@ void Label::set_text(String text) if (text == m_text) return; m_text = move(text); - if (is_word_wrap()) - wrap_text(); + if (m_autosize) size_to_fit(); update(); did_change_text(); } -Gfx::IntRect Label::text_rect(size_t line) const +Gfx::IntRect Label::text_rect() const { int indent = 0; if (frame_thickness() > 0) indent = font().glyph_width('x') / 2; auto rect = frame_inner_rect(); - rect.translate_by(indent, line * (font().glyph_height() + 1)); rect.set_width(rect.width() - indent * 2); return rect; } @@ -103,26 +93,12 @@ void Label::paint_event(PaintEvent& event) if (text().is_empty()) return; - if (is_word_wrap()) { - wrap_text(); - for (size_t i = 0; i < m_lines.size(); i++) { - auto& line = m_lines[i]; - auto text_rect = this->text_rect(i); - if (is_enabled()) { - painter.draw_text(text_rect, line, m_text_alignment, palette().color(foreground_role()), Gfx::TextElision::None); - } else { - painter.draw_text(text_rect.translated(1, 1), line, font(), text_alignment(), Color::White, Gfx::TextElision::Right); - painter.draw_text(text_rect, line, font(), text_alignment(), Color::from_rgb(0x808080), Gfx::TextElision::Right); - } - } + auto text_rect = this->text_rect(); + if (is_enabled()) { + painter.draw_text(text_rect, text(), m_text_alignment, palette().color(foreground_role()), Gfx::TextElision::Right); } else { - auto text_rect = this->text_rect(); - if (is_enabled()) { - painter.draw_text(text_rect, text(), m_text_alignment, palette().color(foreground_role()), Gfx::TextElision::Right); - } else { - painter.draw_text(text_rect.translated(1, 1), text(), font(), text_alignment(), Color::White, Gfx::TextElision::Right); - painter.draw_text(text_rect, text(), font(), text_alignment(), Color::from_rgb(0x808080), Gfx::TextElision::Right); - } + painter.draw_text(text_rect.translated(1, 1), text(), font(), text_alignment(), Color::White, Gfx::TextElision::Right); + painter.draw_text(text_rect, text(), font(), text_alignment(), Color::from_rgb(0x808080), Gfx::TextElision::Right); } } @@ -131,58 +107,11 @@ void Label::size_to_fit() set_fixed_width(font().width(m_text)); } -void Label::wrap_text() +int Label::preferred_height() const { - Vector words; - Optional start; - for (size_t i = 0; i < m_text.length(); i++) { - switch (m_text[i]) { - case '\n': - case '\r': - case '\t': - case ' ': { - if (start.has_value()) - words.append(m_text.substring(start.value(), i - start.value())); - start.clear(); - continue; - } - default: { - if (!start.has_value()) - start = i; - } - } - } - - if (start.has_value()) - words.append(m_text.substring(start.value(), m_text.length() - start.value())); - - auto rect = frame_inner_rect(); - if (frame_thickness() > 0) - rect.set_width(rect.width() - font().glyph_width('x')); - - Vector lines; - StringBuilder builder; - int line_width = 0; - for (auto& word : words) { - int word_width = font().width(word); - if (line_width > 0) - word_width += font().glyph_width('x'); - if (line_width + word_width > rect.width()) { - lines.append(builder.to_string()); - builder.clear(); - line_width = 0; - } - if (line_width > 0) - builder.append(' '); - builder.append(word); - line_width += word_width; - } - - auto last_line = builder.to_string(); - if (!last_line.is_empty()) - lines.append(last_line); - - m_lines = lines; + // FIXME: The 4 is taken from Gfx::Painter and should be available as + // a constant instead. + return Gfx::TextLayout(&font(), Utf8View { m_text }, text_rect()).bounding_rect(Gfx::TextWrapping::Wrap, 4).height(); } } diff --git a/Userland/Libraries/LibGUI/Label.h b/Userland/Libraries/LibGUI/Label.h index 3b2d83d686..6b2bc496e9 100644 --- a/Userland/Libraries/LibGUI/Label.h +++ b/Userland/Libraries/LibGUI/Label.h @@ -33,10 +33,9 @@ public: bool is_autosize() const { return m_autosize; } void set_autosize(bool); - bool is_word_wrap() const { return m_word_wrap; } - void set_word_wrap(bool); + int preferred_height() const; - Gfx::IntRect text_rect(size_t line = 0) const; + Gfx::IntRect text_rect() const; protected: explicit Label(String text = {}); @@ -46,15 +45,12 @@ protected: private: void size_to_fit(); - void wrap_text(); String m_text; RefPtr m_icon; Gfx::TextAlignment m_text_alignment { Gfx::TextAlignment::Center }; bool m_should_stretch_icon { false }; bool m_autosize { false }; - bool m_word_wrap { false }; - Vector m_lines; }; } diff --git a/Userland/Libraries/LibGUI/RadioButton.cpp b/Userland/Libraries/LibGUI/RadioButton.cpp index f4763976c6..7d97a485b3 100644 --- a/Userland/Libraries/LibGUI/RadioButton.cpp +++ b/Userland/Libraries/LibGUI/RadioButton.cpp @@ -50,7 +50,7 @@ void RadioButton::paint_event(PaintEvent& event) Gfx::IntRect text_rect { circle_rect.right() + 7, 0, font().width(text()), font().glyph_height() }; text_rect.center_vertically_within(rect()); - paint_text(painter, text_rect, font(), Gfx::TextAlignment::TopLeft); + paint_text(painter, text_rect, font(), Gfx::TextAlignment::TopLeft, Gfx::TextWrapping::DontWrap); if (is_focused()) painter.draw_focus_rect(text_rect.inflated(6, 6), palette().focus_outline()); diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index ad84121ece..fc9b6a42e5 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -31,6 +31,7 @@ set(SOURCES StylePainter.cpp SystemTheme.cpp TextDirection.cpp + TextLayout.cpp Triangle.cpp Typeface.cpp WindowTheme.cpp diff --git a/Userland/Libraries/LibGfx/Painter.cpp b/Userland/Libraries/LibGfx/Painter.cpp index 8b995250cc..9a1815759b 100644 --- a/Userland/Libraries/LibGfx/Painter.cpp +++ b/Userland/Libraries/LibGfx/Painter.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #if defined(__GNUC__) && !defined(__clang__) @@ -1204,67 +1205,10 @@ void Painter::draw_glyph_or_emoji(const IntPoint& point, u32 code_point, const F draw_emoji(point, *emoji, font); } -static void apply_elision(Utf8View& final_text, String& elided_text, size_t offset) -{ - StringBuilder builder; - builder.append(final_text.substring_view(0, offset).as_string()); - builder.append("..."); - elided_text = builder.to_string(); - final_text = Utf8View { elided_text }; -} - -static void apply_elision(Utf32View& final_text, Vector& elided_text, size_t offset) -{ - elided_text.append(final_text.code_points(), offset); - elided_text.append('.'); - elided_text.append('.'); - elided_text.append('.'); - final_text = Utf32View { elided_text.data(), elided_text.size() }; -} - -template -struct ElidedText { -}; - -template<> -struct ElidedText { - typedef String Type; -}; - -template<> -struct ElidedText { - typedef Vector Type; -}; - -template -void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, TextDirection direction, DrawGlyphFunction draw_glyph) +template +void draw_text_line(IntRect const& a_rect, Utf8View const& text, Font const& font, TextAlignment alignment, TextDirection direction, DrawGlyphFunction draw_glyph) { auto rect = a_rect; - TextType final_text(text); - typename ElidedText::Type elided_text; - if (elision == TextElision::Right) { // FIXME: This needs to be specialized for bidirectional text - int text_width = font.width(final_text); - if (font.width(final_text) > rect.width()) { - int glyph_spacing = font.glyph_spacing(); - int new_width = font.width("..."); - if (new_width < text_width) { - size_t offset = 0; - for (auto it = text.begin(); it != text.end(); ++it) { - auto code_point = *it; - int glyph_width = font.glyph_or_emoji_width(code_point); - // NOTE: Glyph spacing should not be added after the last glyph on the line, - // but since we are here because the last glyph does not actually fit on the line, - // we don't have to worry about spacing. - int width_with_this_glyph_included = new_width + glyph_width + glyph_spacing; - if (width_with_this_glyph_included > rect.width()) - break; - new_width += glyph_width + glyph_spacing; - offset = text.iterator_offset(it); - } - apply_elision(final_text, elided_text, offset); - } - } - } switch (alignment) { case TextAlignment::TopLeft: @@ -1274,11 +1218,11 @@ void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& fon case TextAlignment::TopRight: case TextAlignment::CenterRight: case TextAlignment::BottomRight: - rect.set_x(rect.right() - font.width(final_text)); + rect.set_x(rect.right() - font.width(text)); break; case TextAlignment::Center: { auto shrunken_rect = rect; - shrunken_rect.set_width(font.width(final_text)); + shrunken_rect.set_width(font.width(text)); shrunken_rect.center_within(rect); rect = shrunken_rect; break; @@ -1300,7 +1244,7 @@ void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& fon space_width = -space_width; // Draw spaces backwards } - for (u32 code_point : final_text) { + for (u32 code_point : text) { if (code_point == ' ') { point.translate_by(space_width, 0); continue; @@ -1314,28 +1258,12 @@ void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& fon } } -static inline size_t draw_text_iterator_offset(const Utf8View& text, const Utf8View::Iterator& it) -{ - return text.byte_offset_of(it); -} - -static inline size_t draw_text_iterator_offset(const Utf32View& text, const Utf32View::Iterator& it) -{ - return it - text.begin(); -} - static inline size_t draw_text_get_length(const Utf8View& text) { return text.byte_length(); } -static inline size_t draw_text_get_length(const Utf32View& text) -{ - return text.length(); -} - -template -Vector split_text_into_directional_runs(const TextType& text, TextDirection initial_direction) +Vector Painter::split_text_into_directional_runs(Utf8View const& text, TextDirection initial_direction) { // FIXME: This is a *very* simplified version of the UNICODE BIDIRECTIONAL ALGORITHM (https://www.unicode.org/reports/tr9/), that can render most bidirectional text // but also produces awkward results in a large amount of edge cases. This should probably be replaced with a fully spec compliant implementation at some point. @@ -1479,8 +1407,7 @@ Vector split_text_into_directional_runs(const TextType& text, Te return runs; } -template -bool text_contains_bidirectional_text(const TextType& text, TextDirection initial_direction) +bool Painter::text_contains_bidirectional_text(Utf8View const& text, TextDirection initial_direction) { for (u32 code_point : text) { auto char_class = get_char_bidi_class(code_point); @@ -1492,39 +1419,19 @@ bool text_contains_bidirectional_text(const TextType& text, TextDirection initia return false; } -template -void do_draw_text(const IntRect& rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, DrawGlyphFunction draw_glyph) +template +void Painter::do_draw_text(IntRect const& rect, Utf8View const& text, Font const& font, TextAlignment alignment, TextElision elision, TextWrapping wrapping, DrawGlyphFunction draw_glyph) { if (draw_text_get_length(text) == 0) return; - Vector lines; - - size_t start_of_current_line = 0; - for (auto it = text.begin(); it != text.end(); ++it) { - u32 code_point = *it; - if (code_point == '\n') { - auto offset = draw_text_iterator_offset(text, it); - TextType line = text.substring_view(start_of_current_line, offset - start_of_current_line); - lines.append(line); - start_of_current_line = offset + 1; - } - } - - if (start_of_current_line != draw_text_get_length(text)) { - TextType line = text.substring_view(start_of_current_line, draw_text_get_length(text) - start_of_current_line); - lines.append(line); - } + TextLayout layout(&font, text, rect); static const int line_spacing = 4; int line_height = font.glyph_height() + line_spacing; - IntRect bounding_rect { 0, 0, 0, (static_cast(lines.size()) * line_height) - line_spacing }; - for (auto& line : lines) { - auto line_width = font.width(line); - if (line_width > bounding_rect.width()) - bounding_rect.set_width(line_width); - } + auto lines = layout.lines(elision, wrapping, line_spacing); + auto bounding_rect = layout.bounding_rect(wrapping, line_spacing); switch (alignment) { case TextAlignment::TopLeft: @@ -1559,8 +1466,8 @@ void do_draw_text(const IntRect& rect, const TextType& text, const Font& font, T line_rect.intersect(rect); TextDirection line_direction = get_text_direction(line); - if (text_contains_bidirectional_text(line, line_direction)) { // Slow Path: The line contains mixed BiDi classes - auto directional_runs = split_text_into_directional_runs(line, line_direction); + if (text_contains_bidirectional_text(Utf8View { line }, line_direction)) { // Slow Path: The line contains mixed BiDi classes + auto directional_runs = split_text_into_directional_runs(Utf8View { line }, line_direction); auto current_dx = line_direction == TextDirection::LTR ? 0 : line_rect.width(); for (auto& directional_run : directional_runs) { auto run_width = font.width(directional_run.text()); @@ -1568,65 +1475,82 @@ void do_draw_text(const IntRect& rect, const TextType& text, const Font& font, T current_dx -= run_width; auto run_rect = line_rect.translated(current_dx, 0); run_rect.set_width(run_width); - draw_text_line(run_rect, directional_run.text(), font, alignment, elision, directional_run.direction(), draw_glyph); + + // NOTE: DirectionalRun returns Utf32View which isn't + // compatible with draw_text_line. + StringBuilder builder; + builder.append(directional_run.text()); + auto text = Utf8View { builder.to_string() }; + + draw_text_line(run_rect, text, font, alignment, directional_run.direction(), draw_glyph); if (line_direction == TextDirection::LTR) current_dx += run_width; } } else { - draw_text_line(line_rect, line, font, alignment, elision, line_direction, draw_glyph); + draw_text_line(line_rect, Utf8View { line }, font, alignment, line_direction, draw_glyph); } } } -void Painter::draw_text(const IntRect& rect, const StringView& text, TextAlignment alignment, Color color, TextElision elision) +void Painter::draw_text(const IntRect& rect, const StringView& text, TextAlignment alignment, Color color, TextElision elision, TextWrapping wrapping) { - draw_text(rect, text, font(), alignment, color, elision); + draw_text(rect, text, font(), alignment, color, elision, wrapping); } -void Painter::draw_text(const IntRect& rect, const Utf32View& text, TextAlignment alignment, Color color, TextElision elision) +void Painter::draw_text(const IntRect& rect, const Utf32View& text, TextAlignment alignment, Color color, TextElision elision, TextWrapping wrapping) { - draw_text(rect, text, font(), alignment, color, elision); + draw_text(rect, text, font(), alignment, color, elision, wrapping); } -void Painter::draw_text(const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, Color color, TextElision elision) +void Painter::draw_text(const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, Color color, TextElision elision, TextWrapping wrapping) { Utf8View text { raw_text }; - do_draw_text(rect, Utf8View(text), font, alignment, elision, [&](const IntRect& r, u32 code_point) { + do_draw_text(rect, text, font, alignment, elision, wrapping, [&](const IntRect& r, u32 code_point) { draw_glyph_or_emoji(r.location(), code_point, font, color); }); } -void Painter::draw_text(const IntRect& rect, const Utf32View& text, const Font& font, TextAlignment alignment, Color color, TextElision elision) +void Painter::draw_text(const IntRect& rect, const Utf32View& raw_text, const Font& font, TextAlignment alignment, Color color, TextElision elision, TextWrapping wrapping) { - do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + // FIXME: UTF-32 should eventually be completely removed, but for the time + // being some places might depend on it, so we do some internal conversion. + StringBuilder builder; + builder.append(raw_text); + auto text = Utf8View { builder.string_view() }; + do_draw_text(rect, text, font, alignment, elision, wrapping, [&](const IntRect& r, u32 code_point) { draw_glyph_or_emoji(r.location(), code_point, font, color); }); } -void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, TextElision elision) +void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const Utf8View& text, const Font& font, TextAlignment alignment, TextElision elision, TextWrapping wrapping) { VERIFY(scale() == 1); // FIXME: Add scaling support. - Utf8View text { raw_text }; - do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + do_draw_text(rect, text, font, alignment, elision, wrapping, [&](const IntRect& r, u32 code_point) { draw_one_glyph(r, code_point); }); } -void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const Utf8View& text, const Font& font, TextAlignment alignment, TextElision elision) +void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, TextElision elision, TextWrapping wrapping) { VERIFY(scale() == 1); // FIXME: Add scaling support. - do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + Utf8View text { raw_text }; + do_draw_text(rect, text, font, alignment, elision, wrapping, [&](const IntRect& r, u32 code_point) { draw_one_glyph(r, code_point); }); } -void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const Utf32View& text, const Font& font, TextAlignment alignment, TextElision elision) +void Painter::draw_text(Function draw_one_glyph, const IntRect& rect, const Utf32View& raw_text, const Font& font, TextAlignment alignment, TextElision elision, TextWrapping wrapping) { VERIFY(scale() == 1); // FIXME: Add scaling support. - do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + // FIXME: UTF-32 should eventually be completely removed, but for the time + // being some places might depend on it, so we do some internal conversion. + StringBuilder builder; + builder.append(raw_text); + auto text = Utf8View { builder.string_view() }; + do_draw_text(rect, text, font, alignment, elision, wrapping, [&](const IntRect& r, u32 code_point) { draw_one_glyph(r, code_point); }); } diff --git a/Userland/Libraries/LibGfx/Painter.h b/Userland/Libraries/LibGfx/Painter.h index 7080a9effb..cc99629093 100644 --- a/Userland/Libraries/LibGfx/Painter.h +++ b/Userland/Libraries/LibGfx/Painter.h @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include namespace Gfx { @@ -60,13 +62,13 @@ public: void blit_offset(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect, const IntPoint&); void blit_disabled(const IntPoint&, const Gfx::Bitmap&, const IntRect&, const Palette&); void blit_tiled(const IntRect&, const Gfx::Bitmap&, const IntRect& src_rect); - void draw_text(const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); - void draw_text(const IntRect&, const StringView&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); - void draw_text(const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); - void draw_text(const IntRect&, const Utf32View&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); - void draw_text(Function, const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); - void draw_text(Function, const IntRect&, const Utf8View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); - void draw_text(Function, const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); + void draw_text(const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(const IntRect&, const StringView&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(const IntRect&, const Utf32View&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(Function, const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(Function, const IntRect&, const Utf8View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); + void draw_text(Function, const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap); void draw_ui_text(const Gfx::IntRect&, const StringView&, const Gfx::Font&, TextAlignment, Gfx::Color); void draw_glyph(const IntPoint&, u32, Color); void draw_glyph(const IntPoint&, u32, const Font&, Color); @@ -151,6 +153,12 @@ protected: IntRect m_clip_origin; NonnullRefPtr m_target; Vector m_state_stack; + +private: + Vector split_text_into_directional_runs(Utf8View const&, TextDirection initial_direction); + bool text_contains_bidirectional_text(Utf8View const&, TextDirection); + template + void do_draw_text(IntRect const&, Utf8View const& text, Font const&, TextAlignment, TextElision, TextWrapping, DrawGlyphFunction); }; class PainterStateSaver { diff --git a/Userland/Libraries/LibGfx/TextLayout.cpp b/Userland/Libraries/LibGfx/TextLayout.cpp new file mode 100644 index 0000000000..b3aafe379d --- /dev/null +++ b/Userland/Libraries/LibGfx/TextLayout.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TextLayout.h" + +namespace Gfx { + +// HACK: We need to point to some valid memory with Utf8Views. +char const s_the_newline[] = "\n"; + +IntRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) const +{ + auto lines = wrap_lines(TextElision::None, wrapping, line_spacing, FitWithinRect::No); + if (!lines.size()) { + return {}; + } + + IntRect bounding_rect = { + 0, 0, 0, static_cast((lines.size() * (m_font->glyph_height() + line_spacing)) - line_spacing) + }; + + for (auto& line : lines) { + auto line_width = m_font->width(line); + if (line_width > bounding_rect.width()) + bounding_rect.set_width(line_width); + } + + return bounding_rect; +} + +Vector TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping, int line_spacing, FitWithinRect fit_within_rect) const +{ + Vector words; + + Optional start_byte_offset; + size_t current_byte_offset = 0; + for (auto it = m_text.begin(); !it.done(); ++it) { + current_byte_offset = m_text.iterator_offset(it); + + switch (*it) { + case '\n': + case '\r': + case '\t': + case ' ': { + if (start_byte_offset.has_value()) + words.append(m_text.substring_view(start_byte_offset.value(), current_byte_offset - start_byte_offset.value())); + start_byte_offset.clear(); + + if (*it == '\n') { + words.append(Utf8View { s_the_newline }); + } + + continue; + } + default: { + if (!start_byte_offset.has_value()) + start_byte_offset = current_byte_offset; + } + } + } + + if (start_byte_offset.has_value()) + words.append(m_text.substring_view(start_byte_offset.value(), m_text.byte_length() - start_byte_offset.value())); + + size_t max_lines_that_can_fit = 0; + if (m_rect.height() >= m_font->glyph_height()) { + // NOTE: If glyph height is 10 and line spacing is 1, we can fit a + // single line into a 10px rect and a 20px rect, but 2 lines into a + // 21px rect. + max_lines_that_can_fit = 1 + (m_rect.height() - m_font->glyph_height()) / (m_font->glyph_height() + line_spacing); + } + + if (max_lines_that_can_fit == 0) + return {}; + + Vector lines; + StringBuilder builder; + size_t line_width = 0; + bool did_not_finish = false; + for (auto& word : words) { + + if (word.as_string() == s_the_newline) { + lines.append(builder.to_string()); + builder.clear(); + line_width = 0; + + if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { + did_not_finish = true; + break; + } + } else { + size_t word_width = font().width(word); + + if (line_width > 0) { + word_width += font().glyph_width('x'); + + if (wrapping == TextWrapping::Wrap && line_width + word_width > static_cast(m_rect.width())) { + lines.append(builder.to_string()); + builder.clear(); + line_width = 0; + + if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { + did_not_finish = true; + break; + } + } + + builder.append(' '); + } + if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) { + did_not_finish = true; + break; + } + + builder.append(word.as_string()); + line_width += word_width; + } + } + + if (!did_not_finish) { + auto last_line = builder.to_string(); + if (!last_line.is_empty()) + lines.append(last_line); + } + + switch (elision) { + case TextElision::None: + break; + case TextElision::Right: { + lines.at(lines.size() - 1) = elide_text_from_right(Utf8View { lines.at(lines.size() - 1) }, did_not_finish); + break; + } + } + + return lines; +} + +String TextLayout::elide_text_from_right(Utf8View text, bool force_elision) const +{ + size_t text_width = m_font->width(text); + if (force_elision || text_width > static_cast(m_rect.width())) { + size_t ellipsis_width = m_font->width("..."); + size_t current_width = ellipsis_width; + size_t glyph_spacing = m_font->glyph_spacing(); + + // FIXME: This code will break when the font has glyphs with advance + // amounts different from the actual width of the glyph + // (which is the case with many TrueType fonts). + if (ellipsis_width < text_width) { + size_t offset = 0; + for (auto it = text.begin(); !it.done(); ++it) { + auto code_point = *it; + int glyph_width = m_font->glyph_or_emoji_width(code_point); + // NOTE: Glyph spacing should not be added after the last glyph on the line, + // but since we are here because the last glyph does not actually fit on the line, + // we don't have to worry about spacing. + int width_with_this_glyph_included = current_width + glyph_width + glyph_spacing; + if (width_with_this_glyph_included > m_rect.width()) + break; + current_width += glyph_width + glyph_spacing; + offset = text.iterator_offset(it); + } + + StringBuilder builder; + builder.append(text.substring_view(0, offset).as_string()); + builder.append("..."); + return builder.to_string(); + } + } + + return text.as_string(); +} + +} diff --git a/Userland/Libraries/LibGfx/TextLayout.h b/Userland/Libraries/LibGfx/TextLayout.h new file mode 100644 index 0000000000..ae509fdd40 --- /dev/null +++ b/Userland/Libraries/LibGfx/TextLayout.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "AK/Forward.h" +#include "LibGfx/Forward.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gfx { + +enum class FitWithinRect { + Yes, + No +}; + +// FIXME: This currently isn't an ideal way of doing things; ideally, TextLayout +// would be doing the rendering by painting individual glyphs. However, this +// would regress our Unicode bidirectional text support. Therefore, fixing this +// requires: +// - Moving the bidirectional algorithm either here, or some place TextLayout +// can access; +// - Making TextLayout render the given text into something like a Vector +// where: +// using Line = Vector; +// struct DirectionalRun { +// Utf32View glyphs; +// Vector advance; +// TextDirection direction; +// }; +// - Either; +// a) Making TextLayout output these Lines directly using a given Painter, or +// b) Taking the Lines from TextLayout and painting each glyph. +class TextLayout { +public: + TextLayout(Gfx::Font const* font, Utf8View const& text, IntRect const& rect) + : m_font(font) + , m_text(text) + , m_rect(rect) + { + } + + Font const& font() const { return *m_font; } + void set_font(Font const* font) { m_font = font; } + + Utf8View const& text() const { return m_text; } + void set_text(Utf8View const& text) { m_text = text; } + + IntRect const& rect() const { return m_rect; } + void set_rect(IntRect const& rect) { m_rect = rect; } + + Vector lines(TextElision elision, TextWrapping wrapping, int line_spacing) const + { + return wrap_lines(elision, wrapping, line_spacing, FitWithinRect::Yes); + } + + IntRect bounding_rect(TextWrapping wrapping, int line_spacing) const; + +private: + Vector wrap_lines(TextElision, TextWrapping, int line_spacing, FitWithinRect) const; + String elide_text_from_right(Utf8View, bool force_elision) const; + + Font const* m_font; + Utf8View m_text; + IntRect m_rect; +}; + +} diff --git a/Userland/Libraries/LibGfx/TextWrapping.h b/Userland/Libraries/LibGfx/TextWrapping.h new file mode 100644 index 0000000000..fa98f47175 --- /dev/null +++ b/Userland/Libraries/LibGfx/TextWrapping.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +namespace Gfx { + +enum class TextWrapping { + Wrap, + DontWrap, +}; + +} diff --git a/Userland/Services/Taskbar/TaskbarButton.cpp b/Userland/Services/Taskbar/TaskbarButton.cpp index e0a3087283..c405247ac2 100644 --- a/Userland/Services/Taskbar/TaskbarButton.cpp +++ b/Userland/Services/Taskbar/TaskbarButton.cpp @@ -71,8 +71,8 @@ static void paint_custom_progressbar(GUI::Painter& painter, const Gfx::IntRect& painter.fill_rect_with_gradient(rect, start_color, end_color); if (!text.is_null()) { - painter.draw_text(text_rect.translated(1, 1), text, font, text_alignment, palette.base_text(), Gfx::TextElision::Right); - painter.draw_text(text_rect, text, font, text_alignment, palette.base_text().inverted(), Gfx::TextElision::Right); + painter.draw_text(text_rect.translated(1, 1), text, font, text_alignment, palette.base_text(), Gfx::TextElision::Right, Gfx::TextWrapping::DontWrap); + painter.draw_text(text_rect, text, font, text_alignment, palette.base_text().inverted(), Gfx::TextElision::Right, Gfx::TextWrapping::DontWrap); } } @@ -82,7 +82,7 @@ static void paint_custom_progressbar(GUI::Painter& painter, const Gfx::IntRect& Gfx::PainterStateSaver saver(painter); painter.add_clip_rect(hole_rect); if (!text.is_null()) - painter.draw_text(text_rect, text, font, text_alignment, palette.base_text(), Gfx::TextElision::Right); + painter.draw_text(text_rect, text, font, text_alignment, palette.base_text(), Gfx::TextElision::Right, Gfx::TextWrapping::DontWrap); } void TaskbarButton::paint_event(GUI::PaintEvent& event) @@ -138,5 +138,5 @@ void TaskbarButton::paint_event(GUI::PaintEvent& event) } if (!window.progress().has_value()) - paint_text(painter, text_rect, font, text_alignment()); + paint_text(painter, text_rect, font, text_alignment(), Gfx::TextWrapping::DontWrap); } -- cgit v1.2.3