mirror of
https://github.com/RGBCube/serenity
synced 2025-05-14 06:04:57 +00:00
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.
This commit is contained in:
parent
a5a32fbcce
commit
e11940fd01
14 changed files with 371 additions and 238 deletions
|
@ -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)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibGfx/TextWrapping.h>
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
#include <LibGfx/TextLayout.h>
|
||||
|
||||
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<String> words;
|
||||
Optional<size_t> 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<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Gfx::Bitmap> 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<String> m_lines;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -31,6 +31,7 @@ set(SOURCES
|
|||
StylePainter.cpp
|
||||
SystemTheme.cpp
|
||||
TextDirection.cpp
|
||||
TextLayout.cpp
|
||||
Triangle.cpp
|
||||
Typeface.cpp
|
||||
WindowTheme.cpp
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <LibGfx/Palette.h>
|
||||
#include <LibGfx/Path.h>
|
||||
#include <LibGfx/TextDirection.h>
|
||||
#include <LibGfx/TextLayout.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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<u32>& 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<typename TextType>
|
||||
struct ElidedText {
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ElidedText<Utf8View> {
|
||||
typedef String Type;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ElidedText<Utf32View> {
|
||||
typedef Vector<u32> Type;
|
||||
};
|
||||
|
||||
template<typename TextType, typename DrawGlyphFunction>
|
||||
void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, TextDirection direction, DrawGlyphFunction draw_glyph)
|
||||
template<typename DrawGlyphFunction>
|
||||
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<TextType>::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<typename TextType>
|
||||
Vector<DirectionalRun> split_text_into_directional_runs(const TextType& text, TextDirection initial_direction)
|
||||
Vector<DirectionalRun> 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<DirectionalRun> split_text_into_directional_runs(const TextType& text, Te
|
|||
return runs;
|
||||
}
|
||||
|
||||
template<typename TextType>
|
||||
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<typename TextType, typename DrawGlyphFunction>
|
||||
void do_draw_text(const IntRect& rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, DrawGlyphFunction draw_glyph)
|
||||
template<typename DrawGlyphFunction>
|
||||
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<TextType, 32> 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<int>(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<void(const IntRect&, u32)> draw_one_glyph, const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, TextElision elision)
|
||||
void Painter::draw_text(Function<void(const IntRect&, u32)> 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<void(const IntRect&, u32)> 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<void(const IntRect&, u32)> draw_one_glyph, const IntRect& rect, const Utf8View& text, const Font& font, TextAlignment alignment, TextElision elision)
|
||||
void Painter::draw_text(Function<void(const IntRect&, u32)> 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<void(const IntRect&, u32)> 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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/Size.h>
|
||||
#include <LibGfx/TextAlignment.h>
|
||||
#include <LibGfx/TextDirection.h>
|
||||
#include <LibGfx/TextElision.h>
|
||||
#include <LibGfx/TextWrapping.h>
|
||||
|
||||
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<void(const IntRect&, u32)>, const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None);
|
||||
void draw_text(Function<void(const IntRect&, u32)>, const IntRect&, const Utf8View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None);
|
||||
void draw_text(Function<void(const IntRect&, u32)>, 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<void(const IntRect&, u32)>, const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap);
|
||||
void draw_text(Function<void(const IntRect&, u32)>, const IntRect&, const Utf8View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None, TextWrapping = TextWrapping::Wrap);
|
||||
void draw_text(Function<void(const IntRect&, u32)>, 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<Gfx::Bitmap> m_target;
|
||||
Vector<State, 4> m_state_stack;
|
||||
|
||||
private:
|
||||
Vector<DirectionalRun> split_text_into_directional_runs(Utf8View const&, TextDirection initial_direction);
|
||||
bool text_contains_bidirectional_text(Utf8View const&, TextDirection);
|
||||
template<typename DrawGlyphFunction>
|
||||
void do_draw_text(IntRect const&, Utf8View const& text, Font const&, TextAlignment, TextElision, TextWrapping, DrawGlyphFunction);
|
||||
};
|
||||
|
||||
class PainterStateSaver {
|
||||
|
|
178
Userland/Libraries/LibGfx/TextLayout.cpp
Normal file
178
Userland/Libraries/LibGfx/TextLayout.cpp
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
|
||||
*
|
||||
* 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<int>((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<String, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping, int line_spacing, FitWithinRect fit_within_rect) const
|
||||
{
|
||||
Vector<Utf8View> words;
|
||||
|
||||
Optional<size_t> 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<String> 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<unsigned>(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<unsigned>(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();
|
||||
}
|
||||
|
||||
}
|
79
Userland/Libraries/LibGfx/TextLayout.h
Normal file
79
Userland/Libraries/LibGfx/TextLayout.h
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AK/Forward.h"
|
||||
#include "LibGfx/Forward.h"
|
||||
#include <AK/String.h>
|
||||
#include <AK/Utf32View.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/TextElision.h>
|
||||
#include <LibGfx/TextWrapping.h>
|
||||
|
||||
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<Line>
|
||||
// where:
|
||||
// using Line = Vector<DirectionalRun>;
|
||||
// struct DirectionalRun {
|
||||
// Utf32View glyphs;
|
||||
// Vector<int> 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<String, 32> 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<String, 32> 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;
|
||||
};
|
||||
|
||||
}
|
17
Userland/Libraries/LibGfx/TextWrapping.h
Normal file
17
Userland/Libraries/LibGfx/TextWrapping.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
enum class TextWrapping {
|
||||
Wrap,
|
||||
DontWrap,
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue