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