/src/serenity/Userland/Libraries/LibGfx/TextLayout.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021, sin-ack <sin-ack@protonmail.com> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include "TextLayout.h" |
9 | | #include "Font/Emoji.h" |
10 | | #include <AK/Debug.h> |
11 | | #include <LibUnicode/CharacterTypes.h> |
12 | | #include <LibUnicode/Emoji.h> |
13 | | |
14 | | namespace Gfx { |
15 | | |
16 | | enum class BlockType { |
17 | | Newline, |
18 | | Whitespace, |
19 | | Word |
20 | | }; |
21 | | |
22 | | struct Block { |
23 | | BlockType type; |
24 | | Utf8View characters; |
25 | | }; |
26 | | |
27 | | FloatRect TextLayout::bounding_rect(TextWrapping wrapping) const |
28 | 0 | { |
29 | 0 | auto lines = wrap_lines(TextElision::None, wrapping); |
30 | 0 | if (lines.is_empty()) { |
31 | 0 | return {}; |
32 | 0 | } |
33 | | |
34 | 0 | FloatRect bounding_rect = { |
35 | 0 | 0, 0, 0, (static_cast<float>(lines.size()) * (m_font_metrics.ascent + m_font_metrics.descent + m_font_metrics.line_gap)) - m_font_metrics.line_gap |
36 | 0 | }; |
37 | |
|
38 | 0 | for (auto& line : lines) { |
39 | 0 | auto line_width = m_font.width(line); |
40 | 0 | if (line_width > bounding_rect.width()) |
41 | 0 | bounding_rect.set_width(line_width); |
42 | 0 | } |
43 | |
|
44 | 0 | return bounding_rect; |
45 | 0 | } |
46 | | |
47 | | Vector<ByteString, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping) const |
48 | 0 | { |
49 | 0 | Vector<Block> blocks; |
50 | |
|
51 | 0 | Optional<BlockType> current_block_type; |
52 | 0 | size_t block_start_offset = 0; |
53 | |
|
54 | 0 | size_t offset = 0; |
55 | 0 | for (auto it = m_text.begin(); !it.done(); ++it) { |
56 | 0 | offset = m_text.iterator_offset(it); |
57 | |
|
58 | 0 | switch (*it) { |
59 | 0 | case '\t': |
60 | 0 | case ' ': { |
61 | 0 | if (current_block_type.has_value() && current_block_type.value() != BlockType::Whitespace) { |
62 | 0 | blocks.append({ |
63 | 0 | current_block_type.value(), |
64 | 0 | m_text.substring_view(block_start_offset, offset - block_start_offset), |
65 | 0 | }); |
66 | 0 | current_block_type.clear(); |
67 | 0 | } |
68 | |
|
69 | 0 | if (!current_block_type.has_value()) { |
70 | 0 | current_block_type = BlockType::Whitespace; |
71 | 0 | block_start_offset = offset; |
72 | 0 | } |
73 | |
|
74 | 0 | continue; |
75 | 0 | } |
76 | 0 | case '\r': |
77 | 0 | if (it.peek(1) == static_cast<u32>('\n')) |
78 | 0 | ++it; |
79 | 0 | [[fallthrough]]; |
80 | 0 | case '\n': { |
81 | 0 | if (current_block_type.has_value()) { |
82 | 0 | blocks.append({ |
83 | 0 | current_block_type.value(), |
84 | 0 | m_text.substring_view(block_start_offset, offset - block_start_offset), |
85 | 0 | }); |
86 | 0 | current_block_type.clear(); |
87 | 0 | } |
88 | |
|
89 | 0 | blocks.append({ BlockType::Newline, Utf8View {} }); |
90 | 0 | continue; |
91 | 0 | } |
92 | 0 | default: { |
93 | 0 | if (current_block_type.has_value() && current_block_type.value() != BlockType::Word) { |
94 | 0 | blocks.append({ |
95 | 0 | current_block_type.value(), |
96 | 0 | m_text.substring_view(block_start_offset, offset - block_start_offset), |
97 | 0 | }); |
98 | 0 | current_block_type.clear(); |
99 | 0 | } |
100 | |
|
101 | 0 | if (!current_block_type.has_value()) { |
102 | 0 | current_block_type = BlockType::Word; |
103 | 0 | block_start_offset = offset; |
104 | 0 | } |
105 | 0 | } |
106 | 0 | } |
107 | 0 | } |
108 | | |
109 | 0 | if (current_block_type.has_value()) { |
110 | 0 | blocks.append({ |
111 | 0 | current_block_type.value(), |
112 | 0 | m_text.substring_view(block_start_offset, m_text.byte_length() - block_start_offset), |
113 | 0 | }); |
114 | 0 | } |
115 | |
|
116 | 0 | Vector<ByteString> lines; |
117 | 0 | StringBuilder builder; |
118 | 0 | float line_width = 0; |
119 | 0 | size_t current_block = 0; |
120 | 0 | for (Block& block : blocks) { |
121 | 0 | switch (block.type) { |
122 | 0 | case BlockType::Newline: { |
123 | 0 | lines.append(builder.to_byte_string()); |
124 | 0 | builder.clear(); |
125 | 0 | line_width = 0; |
126 | 0 | current_block++; |
127 | 0 | continue; |
128 | 0 | } |
129 | 0 | case BlockType::Whitespace: |
130 | 0 | case BlockType::Word: { |
131 | 0 | float block_width = m_font.width(block.characters); |
132 | | // FIXME: This should look at the specific advance amount of the |
133 | | // last character, but we don't support that yet. |
134 | 0 | if (current_block != blocks.size() - 1) { |
135 | 0 | block_width += m_font.glyph_spacing(); |
136 | 0 | } |
137 | |
|
138 | 0 | if (wrapping == TextWrapping::Wrap && line_width + block_width > m_rect.width()) { |
139 | 0 | lines.append(builder.to_byte_string()); |
140 | 0 | builder.clear(); |
141 | 0 | line_width = 0; |
142 | 0 | } |
143 | |
|
144 | 0 | builder.append(block.characters.as_string()); |
145 | 0 | line_width += block_width; |
146 | 0 | current_block++; |
147 | 0 | } |
148 | 0 | } |
149 | 0 | } |
150 | | |
151 | 0 | auto last_line = builder.to_byte_string(); |
152 | 0 | if (!last_line.is_empty()) |
153 | 0 | lines.append(last_line); |
154 | |
|
155 | 0 | switch (elision) { |
156 | 0 | case TextElision::None: |
157 | 0 | break; |
158 | 0 | case TextElision::Right: { |
159 | 0 | lines.at(lines.size() - 1) = elide_text_from_right(Utf8View { lines.at(lines.size() - 1) }); |
160 | 0 | break; |
161 | 0 | } |
162 | 0 | } |
163 | | |
164 | 0 | return lines; |
165 | 0 | } |
166 | | |
167 | | ByteString TextLayout::elide_text_from_right(Utf8View text) const |
168 | 0 | { |
169 | 0 | float text_width = m_font.width(text); |
170 | 0 | if (text_width > static_cast<float>(m_rect.width())) { |
171 | 0 | float ellipsis_width = m_font.width("..."sv); |
172 | 0 | float current_width = ellipsis_width; |
173 | 0 | size_t glyph_spacing = m_font.glyph_spacing(); |
174 | | |
175 | | // FIXME: This code will break when the font has glyphs with advance |
176 | | // amounts different from the actual width of the glyph |
177 | | // (which is the case with many TrueType fonts). |
178 | 0 | if (ellipsis_width < text_width) { |
179 | 0 | size_t offset = 0; |
180 | 0 | for (auto it = text.begin(); !it.done(); ++it) { |
181 | 0 | auto glyph_width = m_font.glyph_or_emoji_width(it); |
182 | | // NOTE: Glyph spacing should not be added after the last glyph on the line, |
183 | | // but since we are here because the last glyph does not actually fit on the line, |
184 | | // we don't have to worry about spacing. |
185 | 0 | auto width_with_this_glyph_included = current_width + glyph_width + glyph_spacing; |
186 | 0 | if (width_with_this_glyph_included > m_rect.width()) |
187 | 0 | break; |
188 | 0 | current_width += glyph_width + glyph_spacing; |
189 | 0 | offset = text.iterator_offset(it); |
190 | 0 | } |
191 | |
|
192 | 0 | StringBuilder builder; |
193 | 0 | builder.append(text.substring_view(0, offset).as_string()); |
194 | 0 | builder.append("..."sv); |
195 | 0 | return builder.to_byte_string(); |
196 | 0 | } |
197 | 0 | } |
198 | | |
199 | 0 | return text.as_string(); |
200 | 0 | } |
201 | | |
202 | | DrawGlyphOrEmoji prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, Font const& font) |
203 | 0 | { |
204 | 0 | u32 code_point = *it; |
205 | 0 | auto next_code_point = it.peek(1); |
206 | |
|
207 | 0 | ScopeGuard consume_variation_selector = [&, initial_it = it] { |
208 | | // If we advanced the iterator to consume an emoji sequence, don't look for another variation selector. |
209 | 0 | if (initial_it != it) |
210 | 0 | return; |
211 | | |
212 | | // Otherwise, discard one code point if it's a variation selector. |
213 | 0 | if (next_code_point.has_value() && Unicode::code_point_has_variation_selector_property(*next_code_point)) |
214 | 0 | ++it; |
215 | 0 | }; |
216 | | |
217 | | // NOTE: We don't check for emoji |
218 | 0 | auto font_contains_glyph = font.contains_glyph(code_point); |
219 | 0 | auto check_for_emoji = !font.has_color_bitmaps() && Unicode::could_be_start_of_emoji_sequence(it, font_contains_glyph ? Unicode::SequenceType::EmojiPresentation : Unicode::SequenceType::Any); |
220 | | |
221 | | // If the font contains the glyph, and we know it's not the start of an emoji, draw a text glyph. |
222 | 0 | if (font_contains_glyph && !check_for_emoji) { |
223 | 0 | return DrawGlyph { |
224 | 0 | .position = point, |
225 | 0 | .code_point = code_point, |
226 | 0 | }; |
227 | 0 | } |
228 | | |
229 | | // If we didn't find a text glyph, or have an emoji variation selector or regional indicator, try to draw an emoji glyph. |
230 | 0 | if (auto const* emoji = Emoji::emoji_for_code_point_iterator(it)) { |
231 | 0 | return DrawEmoji { |
232 | 0 | .position = point, |
233 | 0 | .emoji = emoji, |
234 | 0 | }; |
235 | 0 | } |
236 | | |
237 | | // If that failed, but we have a text glyph fallback, draw that. |
238 | 0 | if (font_contains_glyph) { |
239 | 0 | return DrawGlyph { |
240 | 0 | .position = point, |
241 | 0 | .code_point = code_point, |
242 | 0 | }; |
243 | 0 | } |
244 | | |
245 | | // No suitable glyph found, draw a replacement character. |
246 | 0 | dbgln_if(EMOJI_DEBUG, "Failed to find a glyph or emoji for code_point {}", code_point); |
247 | 0 | return DrawGlyph { |
248 | 0 | .position = point, |
249 | 0 | .code_point = 0xFFFD, |
250 | 0 | }; |
251 | 0 | } |
252 | | |
253 | | } |