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;
23
26 iterator first;
27
30 iterator last;
31
37
41
44 size_t line_nr;
45
48 float y;
49
58 aarectangle rectangle;
59
62 float width;
63
71 unicode_general_category last_category;
72
77 unicode_bidi_class paragraph_direction;
78
89 size_t line_nr,
90 const_iterator begin,
91 iterator first,
92 iterator last,
93 float width,
94 font_metrics_px const& metrics) noexcept :
95 first(first), last(last), columns(), metrics(metrics), line_nr(line_nr), y(0.0f), width(width), last_category()
96 {
97 auto last_visible_it = first;
98 for (auto it = first; it != last; ++it) {
99 // Reset the trailing white space marker.
100 it->is_trailing_white_space = false;
101
102 // Only calculate line metrics based on visible characters.
103 // For example a paragraph separator is seldom available in a font.
104 if (is_visible(it->general_category)) {
105 this->metrics = max(metrics, it->font_metrics());
106 last_visible_it = it;
107 }
108 }
109
110 if (first != last) {
111 // Mark trailing whitespace as such
112 for (auto it = last_visible_it + 1; it != last; ++it) {
113 it->is_trailing_white_space = true;
114 }
115
116 last_category = (last - 1)->general_category;
117 } else {
118 last_category = unicode_general_category::Cn;
119 }
120 }
121
122 [[nodiscard]] constexpr size_t size() const noexcept
123 {
124 return columns.size();
125 }
126
127 [[nodiscard]] constexpr iterator front() const noexcept
128 {
129 return columns.front();
130 }
131
132 [[nodiscard]] constexpr iterator back() const noexcept
133 {
134 return columns.back();
135 }
136
137 iterator operator[](size_t index) const noexcept
138 {
139 hi_assert_bounds(index, columns);
140 return columns[index];
141 }
142
143 void layout(horizontal_alignment alignment, float min_x, float max_x, float sub_pixel_width) noexcept
144 {
145 // Reset the position and advance the glyphs.
146 advance_glyphs(columns, y);
147
148 // Calculate the precise width of the line.
149 auto const[visible_width, num_internal_white_space] = calculate_precise_width(columns, paragraph_direction);
150
151 // Align the glyphs for a given width. But keep the left side at x=0.0.
152 align_glyphs(columns, alignment, paragraph_direction, max_x - min_x, visible_width, num_internal_white_space);
153
154 // Move the glyphs to where the left side is.
155 move_glyphs(columns, min_x);
156
157 // Round the glyphs to sub-pixels to improve sharpness of rendered glyphs.
158 round_glyph_positions(columns, sub_pixel_width);
159
160 // Create the bounding rectangles around each glyph, for use to draw selection boxes/cursors and handle mouse control.
161 create_bounding_rectangles(columns, y, metrics.ascender.in(pixels), metrics.descender.in(pixels));
162
163 // Create a bounding rectangle around the visible part of the line.
164 if (columns.empty()) {
165 rectangle = {point2{0.0f, y - metrics.descender.in(pixels)}, point2{1.0f, y + metrics.ascender.in(pixels)}};
166 } else {
167 rectangle = columns.front()->rectangle | columns.back()->rectangle;
168 }
169 }
170
175 [[nodiscard]] std::pair<const_iterator, bool> get_nearest(point2 position) const noexcept
176 {
177 if (columns.empty()) {
178 // This is the last line, so return an the iterator to the end-of-document.
179 return {last, false};
180 }
181
182 auto column_it = std::lower_bound(columns.begin(), columns.end(), position.x(), [](auto const& char_it, auto const& x) {
183 return char_it->rectangle.right() < x;
184 });
185 if (column_it == columns.end()) {
186 column_it = columns.end() - 1;
187 }
188
189 auto char_it = *column_it;
190 if (is_Zp_or_Zl(char_it->general_category)) {
191 // Do not put the cursor on a paragraph separator or line separator.
192 if (paragraph_direction == unicode_bidi_class::L) {
193 if (column_it != columns.begin()) {
194 char_it = *--column_it;
195 } else {
196 // If there is only a paragraph separator, place the cursor before it.
197 return {char_it, false};
198 }
199 } else {
200 if (column_it + 1 != columns.end()) {
201 char_it = *++column_it;
202 } else {
203 // If there is only a paragraph separator, place the cursor before it.
204 return {char_it, false};
205 }
206 }
207 }
208
209 auto const after = (char_it->direction == unicode_bidi_class::L) == position.x() > char_it->rectangle.center();
210 return {char_it, after};
211 }
212
213private:
214 static void advance_glyphs_run(
215 point2& p,
216 text_shaper_line::column_vector::iterator first,
217 text_shaper_line::column_vector::iterator last) noexcept
218 {
219 hi_axiom(first != last);
220
221 auto const char_it = *first;
222 auto const& font = *char_it->glyphs.font;
223 auto const script = char_it->script;
224 auto const language = iso_639{};
225
226 auto run = gstring{};
227 run.reserve(std::distance(first, last));
228 for (auto it = first; it != last; ++it) {
229 run += (*it)->grapheme;
230 }
231
232 auto result = font.shape_run(language, script, run);
233 result.scale_and_offset(char_it->font_size.in(pixels_per_em));
234 hi_axiom(result.advances.size() == run.size());
235 hi_axiom(result.glyph_count.size() == run.size());
236
237 auto grapheme_index = 0_uz;
238 for (auto it = first; it != last; ++it, ++grapheme_index) {
239 (*it)->position = p;
240
241 p += vector2{result.advances[grapheme_index], 0.0f};
242 }
243 }
244
247 static void advance_glyphs(text_shaper_line::column_vector& columns, float y) noexcept
248 {
249 if (columns.empty()) {
250 return;
251 }
252
253 auto p = point2{0.0f, y};
254
255 auto run_start = columns.begin();
256 for (auto it = run_start + 1; it != columns.end(); ++it) {
257 auto const start_char_it = *run_start;
258 auto const char_it = *it;
259
260 auto const same_font = start_char_it->glyphs.font == char_it->glyphs.font;
261 auto const same_style = start_char_it->style == char_it->style;
262 auto const same_size = start_char_it->font_size == char_it->font_size;
263 auto const same_language = true;
264 auto const same_script = start_char_it->script == char_it->script;
265
266 if (not(same_font and same_style and same_size and same_language and same_script)) {
267 advance_glyphs_run(p, run_start, it);
268 run_start = it;
269 }
270 }
271 advance_glyphs_run(p, run_start, columns.end());
272 }
273
274 [[nodiscard]] static std::pair<float, size_t>
275 calculate_precise_width(text_shaper_line::column_vector& columns, unicode_bidi_class paragraph_direction)
276 {
277 if (columns.empty()) {
278 return {0.0f, 0_uz};
279 }
280
281 auto it = columns.begin();
282 for (; it != columns.end(); ++it) {
283 if (not(*it)->is_trailing_white_space) {
284 break;
285 }
286 }
287 auto const left_x = (*it)->position.x();
288
289 auto right_x = left_x;
290 auto num_white_space = 0_uz;
291 for (; it != columns.end(); ++it) {
292 if ((*it)->is_trailing_white_space) {
293 // Stop at the first trailing white space.
294 break;
295 }
296
297 right_x = (*it)->position.x() + (*it)->metrics.advance;
298 if (not is_visible((*it)->general_category)) {
299 ++num_white_space;
300 }
301 }
302
303 auto const width = right_x - left_x;
304
305 // Adjust the offset to left align on the first visible character.
306 for (auto& char_it : columns) {
307 char_it->position.x() -= left_x;
308 }
309
310 return {width, num_white_space};
311 }
312
313 static void move_glyphs(text_shaper_line::column_vector& columns, float offset) noexcept
314 {
315 for (auto const& char_it : columns) {
316 char_it->position.x() += offset;
317 }
318 }
319
320 [[nodiscard]] static bool align_glyphs_justified(
321 text_shaper_line::column_vector& columns,
322 float max_line_width,
323 float visible_width,
324 size_t num_internal_white_space) noexcept
325 {
326 if (num_internal_white_space == 0) {
327 return false;
328 }
329
330 auto const extra_space = max_line_width - visible_width;
331 if (extra_space > max_line_width * 0.25f) {
332 return false;
333 }
334
335 auto const extra_space_per_whitespace = extra_space / num_internal_white_space;
336 auto offset = 0.0f;
337 for (auto const& char_it : columns) {
338 char_it->position.x() += offset;
339
340 // Add extra space for each white space in the visible part of the line. Leave the
341 // sizes of trailing white space normal.
342 if (not char_it->is_trailing_white_space and not is_visible(char_it->general_category)) {
343 offset += extra_space_per_whitespace;
344 }
345 }
346
347 return true;
348 }
349
350 static void align_glyphs(
351 text_shaper_line::column_vector& columns,
352 horizontal_alignment alignment,
353 unicode_bidi_class paragraph_direction,
354 float max_line_width,
355 float visible_width,
356 size_t num_internal_white_space) noexcept
357 {
358 if (alignment == horizontal_alignment::justified) {
359 if (align_glyphs_justified(columns, max_line_width, visible_width, num_internal_white_space)) {
360 return;
361 }
362 }
363
364 if (alignment == horizontal_alignment::flush or alignment == horizontal_alignment::justified) {
365 alignment = paragraph_direction == unicode_bidi_class::R ? horizontal_alignment::right : horizontal_alignment::left;
366 }
367
368 // clang-format off
369 auto const offset =
370 alignment == horizontal_alignment::left ? 0.0f :
371 alignment == horizontal_alignment::right ? max_line_width - visible_width :
372 (max_line_width - visible_width) * 0.5f;
373 // clang-format on
374
375 return move_glyphs(columns, offset);
376 }
377
378 static void round_glyph_positions(text_shaper_line::column_vector& columns, float sub_pixel_width) noexcept
379 {
380 auto const rcp_sub_pixel_width = 1.0f / sub_pixel_width;
381 for (auto it : columns) {
382 it->position.x() = std::round(it->position.x() * rcp_sub_pixel_width) * sub_pixel_width;
383 }
384 }
385
386 static void
387 create_bounding_rectangles(text_shaper_line::column_vector& columns, float y, float ascender, float descender) noexcept
388 {
389 for (auto it = columns.begin(); it != columns.end(); ++it) {
390 auto const next_it = it + 1;
391 auto const char_it = *it;
392 if (next_it == columns.end()) {
393 char_it->rectangle = {
394 point2{char_it->position.x(), y - descender},
395 point2{char_it->position.x() + char_it->metrics.advance, y + ascender}};
396 } else {
397 auto const next_char_it = *next_it;
398
399 if (next_char_it->position.x() <= char_it->position.x()) {
400 // Somehow the next character is overlapping with the current character, use the advance instead.
401 char_it->rectangle = {
402 point2{char_it->position.x(), y - descender},
403 point2{char_it->position.x() + char_it->metrics.advance, y + ascender}};
404 } else {
405 char_it->rectangle = {
406 point2{char_it->position.x(), y - descender}, point2{next_char_it->position.x(), y + ascender}};
407 }
408 }
409 }
410 }
411};
412
413} // namespace hi::inline v1
@ rectangle
The gui_event has rectangle data.
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Definition font_font.hpp:31
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
Definition text_shaper_line.hpp:18
size_t line_nr
The line number of this line, counted from top to bottom.
Definition text_shaper_line.hpp:44
unicode_general_category last_category
Category of the last character on the line.
Definition text_shaper_line.hpp:71
std::pair< const_iterator, bool > get_nearest(point2 position) const noexcept
Get the character nearest to position.
Definition text_shaper_line.hpp:175
float width
The width of this line, excluding trailing white space, glyph morphing and kerning.
Definition text_shaper_line.hpp:62
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:88
float y
Position of the base-line of this line.
Definition text_shaper_line.hpp:48
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:77
column_vector columns
Iterators to the characters in the text.
Definition text_shaper_line.hpp:36
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:58
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 round(T... args)
T size(T... args)