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. + + 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 StringView& raw_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 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) { - 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) -{ - 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); }