HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_shaper_line.hpp
1// Copyright Take Vos 2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "text_shaper_char.hpp"
8#include "../font/font.hpp"
9#include "../unicode/unicode.hpp"
10#include "../geometry/geometry.hpp"
11#include "../macros.hpp"
12#include <vector>
13
14hi_export_module(hikogui.text.text_shaper_line);
15
16hi_export namespace hi::inline v1 {
17
19public:
20 using iterator = std::vector<text_shaper_char>::iterator;
21 using const_iterator = std::vector<text_shaper_char>::const_iterator;
22 using column_vector = std::vector<iterator>;
23
26 iterator first;
27
30 iterator last;
31
36 column_vector columns;
37
40 font_metrics_px metrics;
41
44 float line_spacing = 1.0f;
45
48 float paragraph_spacing = 1.5f;
49
52 size_t line_nr;
53
56 float y;
57
66 aarectangle rectangle;
67
70 float width;
71
79 unicode_general_category last_category;
80
85 unicode_bidi_class paragraph_direction;
86
97 size_t line_nr,
98 const_iterator begin,
99 iterator first,
100 iterator last,
101 float width,
102 font_metrics_px const& metrics) noexcept :
104 {
105 auto last_visible_it = first;
106 for (auto it = first; it != last; ++it) {
107 // Reset the trailing white space marker.
108 it->is_trailing_white_space = false;
109
110 // Only calculate line metrics based on visible characters.
111 // For example a paragraph separator is seldom available in a font.
112 if (is_visible(it->general_category)) {
113 this->metrics = max(metrics, it->font_metrics());
114 this->line_spacing = std::max(this->line_spacing, it->style.line_spacing());
115 this->paragraph_spacing = std::max(this->paragraph_spacing, it->style.paragraph_spacing());
116 last_visible_it = it;
117 }
118 }
119
120 if (first != last) {
121 // Mark trailing whitespace as such
122 for (auto it = last_visible_it + 1; it != last; ++it) {
123 it->is_trailing_white_space = true;
124 }
125
126 last_category = (last - 1)->general_category;
127 } else {
128 last_category = unicode_general_category::Cn;
129 }
130 }
131
132 [[nodiscard]] constexpr size_t size() const noexcept
133 {
134 return columns.size();
135 }
136
137 [[nodiscard]] constexpr iterator front() const noexcept
138 {
139 return columns.front();
140 }
141
142 [[nodiscard]] constexpr iterator back() const noexcept
143 {
144 return columns.back();
145 }
146
147 iterator operator[](size_t index) const noexcept
148 {
149 hi_assert_bounds(index, columns);
150 return columns[index];
151 }
152
153 void layout(horizontal_alignment alignment, float min_x, float max_x, float sub_pixel_width) noexcept
154 {
155 // Reset the position and advance the glyphs.
156 advance_glyphs(columns, y);
157
158 // Calculate the precise width of the line.
159 auto const[visible_width, num_internal_white_space] = calculate_precise_width(columns, paragraph_direction);
160
161 // Align the glyphs for a given width. But keep the left side at x=0.0.
162 align_glyphs(columns, alignment, paragraph_direction, max_x - min_x, visible_width, num_internal_white_space);
163
164 // Move the glyphs to where the left side is.
165 move_glyphs(columns, min_x);
166
167 // Round the glyphs to sub-pixels to improve sharpness of rendered glyphs.
168 round_glyph_positions(columns, sub_pixel_width);
169
170 // Create the bounding rectangles around each glyph, for use to draw selection boxes/cursors and handle mouse control.
171 create_bounding_rectangles(columns, y, metrics.ascender.in(unit::pixels), metrics.descender.in(unit::pixels));
172
173 // Create a bounding rectangle around the visible part of the line.
174 if (columns.empty()) {
175 rectangle = {point2{0.0f, y - metrics.descender.in(unit::pixels)}, point2{1.0f, y + metrics.ascender.in(unit::pixels)}};
176 } else {
177 rectangle = columns.front()->rectangle | columns.back()->rectangle;
178 }
179 }
180
185 [[nodiscard]] std::pair<const_iterator, bool> get_nearest(point2 position) const noexcept
186 {
187 if (columns.empty()) {
188 // This is the last line, so return an the iterator to the end-of-document.
189 return {last, false};
190 }
191
192 auto column_it = std::lower_bound(columns.begin(), columns.end(), position.x(), [](auto const& char_it, auto const& x) {
193 return char_it->rectangle.right() < x;
194 });
195 if (column_it == columns.end()) {
196 column_it = columns.end() - 1;
197 }
198
199 auto char_it = *column_it;
200 if (is_Zp_or_Zl(char_it->general_category)) {
201 // Do not put the cursor on a paragraph separator or line separator.
202 if (paragraph_direction == unicode_bidi_class::L) {
203 if (column_it != columns.begin()) {
204 char_it = *--column_it;
205 } else {
206 // If there is only a paragraph separator, place the cursor before it.
207 return {char_it, false};
208 }
209 } else {
210 if (column_it + 1 != columns.end()) {
211 char_it = *++column_it;
212 } else {
213 // If there is only a paragraph separator, place the cursor before it.
214 return {char_it, false};
215 }
216 }
217 }
218
219 auto const after = (char_it->direction == unicode_bidi_class::L) == position.x() > char_it->rectangle.center();
220 return {char_it, after};
221 }
222
223private:
224 static void advance_glyphs_run(
225 point2& p,
226 text_shaper_line::column_vector::iterator first,
227 text_shaper_line::column_vector::iterator last) noexcept
228 {
229 hi_axiom(first != last);
230
231 auto const char_it = *first;
232 auto const font = char_it->glyphs.font;
233 auto const script = char_it->script;
234 auto const language = iso_639{};
235
236 auto run = gstring{};
237 run.reserve(std::distance(first, last));
238 for (auto it = first; it != last; ++it) {
239 run += (*it)->grapheme;
240 }
241
242 auto result = font->shape_run(language, script, run);
243 result.scale_and_offset(char_it->font_size.in(unit::pixels_per_em));
244 hi_axiom(result.advances.size() == run.size());
245 hi_axiom(result.glyph_count.size() == run.size());
246
247 auto grapheme_index = 0_uz;
248 for (auto it = first; it != last; ++it, ++grapheme_index) {
249 (*it)->position = p;
250
251 p += vector2{result.advances[grapheme_index], 0.0f};
252 }
253 }
254
257 static void advance_glyphs(text_shaper_line::column_vector& columns, float y) noexcept
258 {
259 if (columns.empty()) {
260 return;
261 }
262
263 auto p = point2{0.0f, y};
264
265 auto run_start = columns.begin();
266 for (auto it = run_start + 1; it != columns.end(); ++it) {
267 auto const start_char_it = *run_start;
268 auto const char_it = *it;
269
270 auto const same_font = start_char_it->glyphs.font == char_it->glyphs.font;
271 auto const same_style = start_char_it->style == char_it->style;
272 auto const same_size = start_char_it->font_size == char_it->font_size;
273 auto const same_language = true;
274 auto const same_script = start_char_it->script == char_it->script;
275
276 if (not(same_font and same_style and same_size and same_language and same_script)) {
277 advance_glyphs_run(p, run_start, it);
278 run_start = it;
279 }
280 }
281 advance_glyphs_run(p, run_start, columns.end());
282 }
283
284 [[nodiscard]] static std::pair<float, size_t>
285 calculate_precise_width(text_shaper_line::column_vector& columns, unicode_bidi_class paragraph_direction)
286 {
287 if (columns.empty()) {
288 return {0.0f, 0_uz};
289 }
290
291 auto it = columns.begin();
292 for (; it != columns.end(); ++it) {
293 if (not(*it)->is_trailing_white_space) {
294 break;
295 }
296 }
297 auto const left_x = (*it)->position.x();
298
299 auto right_x = left_x;
300 auto num_white_space = 0_uz;
301 for (; it != columns.end(); ++it) {
302 if ((*it)->is_trailing_white_space) {
303 // Stop at the first trailing white space.
304 break;
305 }
306
307 right_x = (*it)->position.x() + (*it)->metrics.advance;
308 if (not is_visible((*it)->general_category)) {
309 ++num_white_space;
310 }
311 }
312
313 auto const width = right_x - left_x;
314
315 // Adjust the offset to left align on the first visible character.
316 for (auto& char_it : columns) {
317 char_it->position.x() -= left_x;
318 }
319
320 return {width, num_white_space};
321 }
322
323 static void move_glyphs(text_shaper_line::column_vector& columns, float offset) noexcept
324 {
325 for (auto const& char_it : columns) {
326 char_it->position.x() += offset;
327 }
328 }
329
330 [[nodiscard]] static bool align_glyphs_justified(
331 text_shaper_line::column_vector& columns,
332 float max_line_width,
333 float visible_width,
334 size_t num_internal_white_space) noexcept
335 {
336 if (num_internal_white_space == 0) {
337 return false;
338 }
339
340 auto const extra_space = max_line_width - visible_width;
341 if (extra_space > max_line_width * 0.25f) {
342 return false;
343 }
344
345 auto const extra_space_per_whitespace = extra_space / num_internal_white_space;
346 auto offset = 0.0f;
347 for (auto const& char_it : columns) {
348 char_it->position.x() += offset;
349
350 // Add extra space for each white space in the visible part of the line. Leave the
351 // sizes of trailing white space normal.
352 if (not char_it->is_trailing_white_space and not is_visible(char_it->general_category)) {
353 offset += extra_space_per_whitespace;
354 }
355 }
356
357 return true;
358 }
359
360 static void align_glyphs(
361 text_shaper_line::column_vector& columns,
362 horizontal_alignment alignment,
363 unicode_bidi_class paragraph_direction,
364 float max_line_width,
365 float visible_width,
366 size_t num_internal_white_space) noexcept
367 {
368 if (alignment == horizontal_alignment::justified) {
369 if (align_glyphs_justified(columns, max_line_width, visible_width, num_internal_white_space)) {
370 return;
371 }
372 }
373
374 if (alignment == horizontal_alignment::flush or alignment == horizontal_alignment::justified) {
375 alignment = paragraph_direction == unicode_bidi_class::R ? horizontal_alignment::right : horizontal_alignment::left;
376 }
377
378 // clang-format off
379 auto const offset =
380 alignment == horizontal_alignment::left ? 0.0f :
381 alignment == horizontal_alignment::right ? max_line_width - visible_width :
382 (max_line_width - visible_width) * 0.5f;
383 // clang-format on
384
385 return move_glyphs(columns, offset);
386 }
387
388 static void round_glyph_positions(text_shaper_line::column_vector& columns, float sub_pixel_width) noexcept
389 {
390 auto const rcp_sub_pixel_width = 1.0f / sub_pixel_width;
391 for (auto it : columns) {
392 it->position.x() = std::round(it->position.x() * rcp_sub_pixel_width) * sub_pixel_width;
393 }
394 }
395
396 static void
397 create_bounding_rectangles(text_shaper_line::column_vector& columns, float y, float ascender, float descender) noexcept
398 {
399 for (auto it = columns.begin(); it != columns.end(); ++it) {
400 auto const next_it = it + 1;
401 auto const char_it = *it;
402 if (next_it == columns.end()) {
403 char_it->rectangle = {
404 point2{char_it->position.x(), y - descender},
405 point2{char_it->position.x() + char_it->metrics.advance, y + ascender}};
406 } else {
407 auto const next_char_it = *next_it;
408
409 if (next_char_it->position.x() <= char_it->position.x()) {
410 // Somehow the next character is overlapping with the current character, use the advance instead.
411 char_it->rectangle = {
412 point2{char_it->position.x(), y - descender},
413 point2{char_it->position.x() + char_it->metrics.advance, y + ascender}};
414 } else {
415 char_it->rectangle = {
416 point2{char_it->position.x(), y - descender}, point2{next_char_it->position.x(), y + ascender}};
417 }
418 }
419 }
420 }
421};
422
423} // namespace hi::inline v1
@ rectangle
The gui_event has rectangle data.
Definition gui_event_variant.hpp:44
@ alignment
A alignment was changed.
Definition style_modify_mask.hpp:43
@ layout
A layout (size, alignment) value was modified.
Definition style_modify_mask.hpp:55
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Definition font_font.hpp:32
au::Quantity< Unit, T > ascender
Distance from baseline of highest ascender.
Definition font_metrics.hpp:25
au::Quantity< Unit, T > descender
Distance from baseline of lowest descender.
Definition font_metrics.hpp:31
A high-level geometric point Part of the high-level vec, point, mat and color types.
Definition point2.hpp:28
ISO-639 language code.
Definition iso_639.hpp:29
size_t line_nr
The line number of this line, counted from top to bottom.
Definition text_shaper_line.hpp:52
unicode_general_category last_category
Category of the last character on the line.
Definition text_shaper_line.hpp:79
std::pair< const_iterator, bool > get_nearest(point2 position) const noexcept
Get the character nearest to position.
Definition text_shaper_line.hpp:185
float width
The width of this line, excluding trailing white space, glyph morphing and kerning.
Definition text_shaper_line.hpp:70
float line_spacing
The maximum line spacing for each grapheme on this line.
Definition text_shaper_line.hpp:44
text_shaper_line(size_t line_nr, const_iterator begin, iterator first, iterator last, float width, font_metrics_px const &metrics) noexcept
Construct a line.
Definition text_shaper_line.hpp:96
float y
Position of the base-line of this line.
Definition text_shaper_line.hpp:56
font_metrics_px metrics
The maximum metrics of the font of each glyph on this line.
Definition text_shaper_line.hpp:40
iterator first
The first character in the line, in logical order.
Definition text_shaper_line.hpp:26
unicode_bidi_class paragraph_direction
The writing direction of the paragraph.
Definition text_shaper_line.hpp:85
column_vector columns
Iterators to the characters in the text.
Definition text_shaper_line.hpp:36
float paragraph_spacing
The maximum paragraph spacing for each grapheme on this line.
Definition text_shaper_line.hpp:48
iterator last
One beyond the last character in the line, in logical order.
Definition text_shaper_line.hpp:30
aarectangle rectangle
The rectangle of the line.
Definition text_shaper_line.hpp:66
T back(T... args)
T begin(T... args)
T distance(T... args)
T empty(T... args)
T end(T... args)
T front(T... args)
T lower_bound(T... args)
T max(T... args)
T reserve(T... args)
T round(T... args)
T size(T... args)