HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_shaper.hpp
1// Copyright Take Vos 2021-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 "text_shaper_line.hpp"
9#include "text_cursor.hpp"
10#include "text_style.hpp"
11#include "../layout/layout.hpp"
12#include "../font/font.hpp"
13#include "../geometry/geometry.hpp"
14#include "../unicode/unicode.hpp"
15#include "../units/units.hpp"
16#include "../macros.hpp"
17#include <vector>
18#include <tuple>
19#include <coroutine>
20
21hi_export_module(hikogui.text.text_shaper);
22
23hi_export namespace hi::inline v1 {
24
39public:
41 using char_iterator = char_vector::iterator;
42 using char_const_iterator = char_vector::const_iterator;
43 using char_reference = char_vector::reference;
44 using char_const_reference = char_vector::const_reference;
46 using line_iterator = line_vector::iterator;
47 using line_const_iterator = line_vector::const_iterator;
48
49 constexpr text_shaper() noexcept = default;
50 constexpr text_shaper(text_shaper const&) noexcept = default;
51 constexpr text_shaper(text_shaper&&) noexcept = default;
52 constexpr text_shaper& operator=(text_shaper const&) noexcept = default;
53 constexpr text_shaper& operator=(text_shaper&&) noexcept = default;
54
83 [[nodiscard]] text_shaper(
84 gstring const& text,
85 text_style const& style,
86 hi::pixel_density pixel_density,
87 hi::alignment alignment,
88 bool left_to_right,
89 iso_15924 script = iso_15924{"Zyyy"}) noexcept :
90 _bidi_context(left_to_right ? unicode_bidi_class::L : unicode_bidi_class::R),
91 _pixel_density(pixel_density),
92 _alignment(alignment),
93 _script(script)
94 {
95 auto const& font = find_font(style->family_id, style->variant);
96 _initial_line_metrics = style->size * _pixel_density * font.metrics;
97
98 _text.reserve(text.size());
99 for (auto const& c : text) {
100 auto const clean_c = c == '\n' ? grapheme{unicode_PS} : c;
101
102 auto& tmp = _text.emplace_back(clean_c, style, _pixel_density);
103 tmp.initialize_glyph(font);
104 }
105
106 _text_direction = unicode_bidi_direction(
107 _text.begin(),
108 _text.end(),
109 [](text_shaper::char_const_reference it) {
110 return it.grapheme.starter();
111 },
112 _bidi_context);
113
114 _line_break_opportunities = unicode_line_break(_text.begin(), _text.end(), [](auto const& c) -> decltype(auto) {
115 return c.grapheme.starter();
116 });
117
118 _line_break_widths.reserve(text.size());
119 for (auto const& c : _text) {
120 _line_break_widths.push_back(is_visible(c.general_category) ? c.width : -c.width);
121 }
122
123 _word_break_opportunities = unicode_word_break(_text.begin(), _text.end(), [](auto const& c) -> decltype(auto) {
124 return c.grapheme.starter();
125 });
126
127 _sentence_break_opportunities = unicode_sentence_break(_text.begin(), _text.end(), [](auto const& c) -> decltype(auto) {
128 return c.grapheme.starter();
129 });
130
131 resolve_script();
132 }
133
134 [[nodiscard]] text_shaper(
135 std::string_view text,
136 text_style const& style,
137 hi::pixel_density pixel_density,
138 hi::alignment alignment,
139 bool left_to_right,
140 iso_15924 script = iso_15924{"Zyyy"}) noexcept :
141 text_shaper(to_gstring(text), style, pixel_density, alignment, left_to_right, script)
142 {
143 }
144
145 [[nodiscard]] bool empty() const noexcept
146 {
147 return _text.empty();
148 }
149
150 [[nodiscard]] size_t size() const noexcept
151 {
152 return _text.size();
153 }
154
155 [[nodiscard]] char_iterator begin() noexcept
156 {
157 return _text.begin();
158 }
159
160 [[nodiscard]] char_const_iterator begin() const noexcept
161 {
162 return _text.begin();
163 }
164
165 [[nodiscard]] char_const_iterator cbegin() const noexcept
166 {
167 return _text.cbegin();
168 }
169
170 [[nodiscard]] char_iterator end() noexcept
171 {
172 return _text.end();
173 }
174
175 [[nodiscard]] char_const_iterator end() const noexcept
176 {
177 return _text.end();
178 }
179
180 [[nodiscard]] char_const_iterator cend() const noexcept
181 {
182 return _text.cend();
183 }
184
185 auto const& lines() const noexcept
186 {
187 return _lines;
188 }
189
208 [[nodiscard]] aarectangle
209 bounding_rectangle(float maximum_line_width, float line_spacing = 1.0f, float paragraph_spacing = 1.5f) noexcept
210 {
211 auto const rectangle = aarectangle{
213 constexpr auto baseline = 0.0f;
214 constexpr auto sub_pixel_size = extent2{1.0f, 1.0f};
215
216 auto const lines = make_lines(rectangle, baseline, sub_pixel_size, line_spacing, paragraph_spacing);
217 hi_assert(not lines.empty());
218
219 auto max_width = 0.0f;
220 for (auto& line : lines) {
221 inplace_max(max_width, line.width);
222 }
223
224 auto const max_y = lines.front().y + std::ceil(lines.front().metrics.ascender.in(pixels));
225 auto const min_y = lines.back().y - std::ceil(lines.back().metrics.descender.in(pixels));
226 return aarectangle{point2{0.0f, min_y}, point2{std::ceil(max_width), max_y}};
227 }
228
241 void layout(
242 aarectangle rectangle,
243 float baseline,
244 extent2 sub_pixel_size,
245 float line_spacing = 1.0f,
246 float paragraph_spacing = 1.5f) noexcept
247 {
248 _rectangle = rectangle;
249 _lines = make_lines(rectangle, baseline, sub_pixel_size, line_spacing, paragraph_spacing);
250 hi_assert(not _lines.empty());
251 position_glyphs(rectangle, sub_pixel_size);
252 }
253
256 [[nodiscard]] aarectangle rectangle() const noexcept
257 {
258 return _rectangle;
259 }
260
263 [[nodiscard]] unicode_bidi_class text_direction() const noexcept
264 {
265 return _text_direction;
266 }
267
273 [[nodiscard]] alignment resolved_alignment() const noexcept
274 {
275 return resolve(_alignment, _text_direction == unicode_bidi_class::L);
276 }
277
285 [[nodiscard]] char_const_iterator get_it(size_t index) const noexcept
286 {
287 if (static_cast<ptrdiff_t>(index) < 0) {
288 return begin();
289 } else if (index >= size()) {
290 return end();
291 }
292
293 return begin() + index;
294 }
295
303 [[nodiscard]] char_const_iterator get_it(text_cursor cursor) const noexcept
304 {
305 return get_it(cursor.index());
306 }
307
316 [[nodiscard]] char_const_iterator get_it(size_t column_nr, size_t line_nr) const noexcept
317 {
318 hi_assert(not _lines.empty());
319
320 if (static_cast<ptrdiff_t>(line_nr) < 0) {
321 return begin();
322 } else if (line_nr >= _lines.size()) {
323 return end();
324 }
325
326 auto const left_of_line = static_cast<ptrdiff_t>(column_nr) < 0;
327 auto const right_of_line = column_nr >= _lines[line_nr].size();
328
329 if (left_of_line or right_of_line) {
330 auto const ltr = _lines[line_nr].paragraph_direction == unicode_bidi_class::L;
331 auto const go_up = left_of_line == ltr;
332 if (go_up) {
333 // Go to line above.
334 if (static_cast<ptrdiff_t>(--line_nr) < 0) {
335 return begin();
336 } else {
337 // Go to end of line above.
338 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].back() :
339 _lines[line_nr].front();
340 }
341
342 } else {
343 // Go to the line below.
344 if (++line_nr >= _lines.size()) {
345 return end();
346 } else {
347 // Go to begin of line below.
348 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].front() :
349 _lines[line_nr].back();
350 }
351 }
352 }
353
354 return _lines[line_nr][column_nr];
355 }
356
364 [[nodiscard]] char_const_iterator get_it(std::pair<size_t, size_t> column_row) const noexcept
365 {
366 return get_it(column_row.first, column_row.second);
367 }
368
374 [[nodiscard]] std::pair<size_t, size_t> get_column_line(text_shaper::char_const_iterator it) const noexcept
375 {
376 if (it != end()) {
377 return {it->column_nr, it->line_nr};
378 } else {
379 hi_assert(not _lines.empty());
380 return {_lines.size() - 1, _lines.back().size()};
381 }
382 }
383
389 [[nodiscard]] std::pair<size_t, size_t> get_column_line(size_t index) const noexcept
390 {
391 return get_column_line(get_it(index));
392 }
393
399 [[nodiscard]] std::pair<size_t, size_t> get_column_line(text_cursor cursor) const noexcept
400 {
401 return get_column_line(cursor.index());
402 }
403
409 [[nodiscard]] size_t get_index(text_shaper::char_const_iterator it) const noexcept
410 {
411 return narrow_cast<size_t>(std::distance(begin(), it));
412 }
413
418 [[nodiscard]] text_cursor get_begin_cursor() const noexcept
419 {
420 return {};
421 }
422
427 [[nodiscard]] text_cursor get_end_cursor() const noexcept
428 {
429 return text_cursor{size() - 1, true}.resize(size());
430 }
431
437 [[nodiscard]] text_cursor get_before_cursor(size_t index) const noexcept
438 {
439 return text_cursor{index, false}.resize(size());
440 }
441
447 [[nodiscard]] text_cursor get_after_cursor(size_t index) const noexcept
448 {
449 return text_cursor{index, true}.resize(size());
450 }
451
457 [[nodiscard]] text_cursor get_before_cursor(text_shaper::char_const_iterator it) const noexcept
458 {
459 return get_before_cursor(get_index(it));
460 }
461
467 [[nodiscard]] text_cursor get_after_cursor(text_shaper::char_const_iterator it) const noexcept
468 {
469 return get_after_cursor(get_index(it));
470 }
471
477 [[nodiscard]] text_cursor get_left_cursor(text_shaper::char_const_iterator it) const noexcept
478 {
479 if (it != end()) {
480 if (it->direction == unicode_bidi_class::L) {
481 return get_before_cursor(it);
482 } else {
483 return get_after_cursor(it);
484 }
485 } else {
486 return get_end_cursor();
487 }
488 }
489
495 [[nodiscard]] text_cursor get_right_cursor(text_shaper::char_const_iterator it) const noexcept
496 {
497 if (it != end()) {
498 if (it->direction == unicode_bidi_class::L) {
499 return get_after_cursor(it);
500 } else {
501 return get_before_cursor(it);
502 }
503 } else {
504 return get_end_cursor();
505 }
506 }
507
513 [[nodiscard]] bool is_on_left(text_cursor cursor) const noexcept
514 {
515 auto const it = get_it(cursor);
516 if (it != end()) {
517 return (it->direction == unicode_bidi_class::L) == cursor.before();
518 } else {
519 hi_assert(begin() == end());
520 return true;
521 }
522 }
523
529 [[nodiscard]] bool is_on_right(text_cursor cursor) const noexcept
530 {
531 auto const it = get_it(cursor);
532 if (it != end()) {
533 return (it->direction == unicode_bidi_class::L) == cursor.after();
534 } else {
535 hi_assert(begin() == end());
536 return true;
537 }
538 }
539
545 [[nodiscard]] text_cursor get_nearest_cursor(point2 position) const noexcept
546 {
547 if (_text.empty()) {
548 return {};
549 }
550
551 auto const line_it = std::ranges::min_element(_lines, std::ranges::less{}, [position](auto const& line) {
552 return std::abs(line.y - position.y());
553 });
554
555 if (line_it != _lines.end()) {
556 auto const[char_it, after] = line_it->get_nearest(position);
557 return {narrow_cast<size_t>(std::distance(_text.begin(), char_it)), after};
558 } else {
559 return {};
560 }
561 }
562
566 {
567 auto const index = cursor.index();
568 return {get_before_cursor(index), get_after_cursor(index)};
569 }
570
574 {
575 return get_selection_from_break(cursor, _word_break_opportunities);
576 }
577
581 {
582 return get_selection_from_break(cursor, _sentence_break_opportunities);
583 }
584
588 {
589 auto const first_index = [&]() {
590 auto i = cursor.index();
591 while (i > 0) {
592 if (_text[i - 1].general_category == unicode_general_category::Zp) {
593 return i;
594 }
595 --i;
596 }
597 return i;
598 }();
599 auto const last_index = [&]() {
600 auto i = cursor.index();
601 while (i < _text.size()) {
602 if (_text[i].general_category == unicode_general_category::Zp) {
603 return i;
604 }
605 ++i;
606 }
607 return i;
608 }();
609
610 return {get_before_cursor(first_index), get_after_cursor(last_index)};
611 }
612
616 {
617 if (_text.empty()) {
618 return {{}, {}};
619 }
620
621 return {{}, get_end_cursor()};
622 }
623
629 [[nodiscard]] char_const_iterator move_left_char(char_const_iterator it) const noexcept
630 {
631 auto const[column_nr, line_nr] = get_column_line(it);
632 return get_it(column_nr - 1, line_nr);
633 }
634
640 [[nodiscard]] char_const_iterator move_right_char(char_const_iterator it) const noexcept
641 {
642 auto const[column_nr, line_nr] = get_column_line(it);
643 return get_it(column_nr + 1, line_nr);
644 }
645
646 [[nodiscard]] text_cursor move_left_char(text_cursor cursor, bool overwrite_mode) const noexcept
647 {
648 auto it = get_it(cursor);
649 if (overwrite_mode) {
650 it = move_left_char(it);
651 return get_before_cursor(it);
652
653 } else {
654 if (is_on_left(cursor)) {
655 // If the cursor is on the left side of a character, then move one character left.
656 it = move_left_char(it);
657 }
658
659 return get_left_cursor(it);
660 }
661 }
662
663 [[nodiscard]] text_cursor move_right_char(text_cursor cursor, bool overwrite_mode) const noexcept
664 {
665 auto it = get_it(cursor);
666 if (overwrite_mode) {
667 it = move_right_char(it);
668 return get_before_cursor(it);
669
670 } else {
671 if (is_on_right(cursor)) {
672 // If the cursor is on the left side of a character, then move one character left.
673 it = move_right_char(it);
674 }
675
676 return get_right_cursor(it);
677 }
678 }
679
680 [[nodiscard]] text_cursor move_down_char(text_cursor cursor, float& x) const noexcept
681 {
682 if (_text.empty()) {
683 return {};
684 }
685
686 auto [column_nr, line_nr] = get_column_line(cursor);
687 if (++line_nr == _lines.size()) {
688 return get_end_cursor();
689 }
690
691 if (std::isnan(x)) {
692 auto const char_it = get_it(cursor);
693 hi_assert(char_it != _text.end());
694 x = is_on_left(cursor) ? char_it->rectangle.left() : char_it->rectangle.right();
695 }
696
697 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
698 return get_before_cursor(new_char_it);
699 }
700
701 [[nodiscard]] text_cursor move_up_char(text_cursor cursor, float& x) const noexcept
702 {
703 if (_text.empty()) {
704 return {};
705 }
706
707 auto [column_nr, line_nr] = get_column_line(cursor);
708 if (line_nr-- == 0) {
709 return {};
710 }
711
712 if (std::isnan(x)) {
713 auto char_it = get_it(cursor);
714 hi_assert(char_it < _text.end());
715 x = is_on_left(cursor) ? char_it->rectangle.left() : char_it->rectangle.right();
716 }
717
718 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
719 return get_before_cursor(new_char_it);
720 }
721
722 [[nodiscard]] text_cursor move_left_word(text_cursor cursor, bool overwrite_mode) const noexcept
723 {
724 cursor = move_left_char(cursor, overwrite_mode).before_neighbor(size());
725 auto it = get_it(cursor);
726 while (it != end()) {
727 if (it->general_category != unicode_general_category::Zs and
728 _word_break_opportunities[get_index(it)] != unicode_break_opportunity::no) {
729 return get_before_cursor(it);
730 }
731 it = move_left_char(it);
732 }
733 return get_end_cursor();
734 }
735
736 [[nodiscard]] text_cursor move_right_word(text_cursor cursor, bool overwrite_mode) const noexcept
737 {
738 cursor = move_right_char(cursor, overwrite_mode).before_neighbor(size());
739 auto it = get_it(cursor);
740 while (it != end()) {
741 if (it->general_category != unicode_general_category::Zs and
742 _word_break_opportunities[get_index(it)] != unicode_break_opportunity::no) {
743 return get_before_cursor(it);
744 }
745 it = move_right_char(it);
746 }
747 return get_end_cursor();
748 }
749
750 [[nodiscard]] text_cursor move_begin_line(text_cursor cursor) const noexcept
751 {
752 auto const[column_nr, line_nr] = get_column_line(cursor);
753 auto const& line = _lines[line_nr];
754 return get_before_cursor(line.first);
755 }
756
757 [[nodiscard]] text_cursor move_end_line(text_cursor cursor) const noexcept
758 {
759 auto const[column_nr, line_nr] = get_column_line(cursor);
760 auto const& line = _lines[line_nr];
761
762 auto it = line.last;
763 while (it != line.first) {
764 --it;
765 if (not it->is_trailing_white_space) {
766 break;
767 }
768 }
769
770 return get_after_cursor(it);
771 }
772
773 [[nodiscard]] text_cursor move_begin_sentence(text_cursor cursor) const noexcept
774 {
775 if (cursor.after()) {
776 cursor = {cursor.index(), false};
777 } else if (cursor.index() != 0) {
778 cursor = {cursor.index() - 1, false};
779 }
780 auto const[first, last] = select_sentence(cursor);
781 return first.before_neighbor(size());
782 }
783
784 [[nodiscard]] text_cursor move_end_sentence(text_cursor cursor) const noexcept
785 {
786 if (cursor.before()) {
787 cursor = {cursor.index(), true};
788 } else if (cursor.index() != _text.size() - 1) {
789 cursor = {cursor.index() + 1, true};
790 }
791 auto const[first, last] = select_sentence(cursor);
792 return last.before_neighbor(size());
793 }
794
795 [[nodiscard]] text_cursor move_begin_paragraph(text_cursor cursor) const noexcept
796 {
797 if (cursor.after()) {
798 cursor = {cursor.index(), false};
799 } else if (cursor.index() != 0) {
800 cursor = {cursor.index() - 1, false};
801 }
802 auto const[first, last] = select_paragraph(cursor);
803 return first.before_neighbor(size());
804 }
805
806 [[nodiscard]] text_cursor move_end_paragraph(text_cursor cursor) const noexcept
807 {
808 if (cursor.before()) {
809 cursor = {cursor.index(), true};
810 } else if (cursor.index() != _text.size() - 1) {
811 cursor = {cursor.index() + 1, true};
812 }
813 auto const[first, last] = select_paragraph(cursor);
814 return last.before_neighbor(size());
815 }
816
817 [[nodiscard]] text_cursor move_begin_document(text_cursor cursor) const noexcept
818 {
819 return {};
820 }
821
822 [[nodiscard]] text_cursor move_end_document(text_cursor cursor) const noexcept
823 {
824 if (_text.empty()) {
825 return {};
826 }
827
828 return get_end_cursor();
829 }
830
831private:
834 hi::pixel_density _pixel_density;
835
842 char_vector _text;
843
844 hi::alignment _alignment;
845
848 unicode_break_vector _line_break_opportunities;
849
852 std::vector<float> _line_break_widths;
853
856 unicode_break_vector _word_break_opportunities;
857
860 unicode_break_vector _sentence_break_opportunities;
861
864 unicode_bidi_context _bidi_context;
865
868 unicode_bidi_class _text_direction;
869
872 iso_15924 _script;
873
878 line_vector _lines;
879
882 font_metrics_px _initial_line_metrics;
883
886 aarectangle _rectangle;
887
888 static void
889 layout_lines_vertical_spacing(text_shaper::line_vector& lines, float line_spacing, float paragraph_spacing) noexcept
890 {
891 hi_assert(not lines.empty());
892
893 auto prev = lines.begin();
894 prev->y = 0.0f;
895 for (auto it = prev + 1; it != lines.end(); ++it) {
896 auto const height =
897 prev->metrics.descender + std::max(prev->metrics.line_gap, it->metrics.line_gap) + it->metrics.ascender;
898 auto const spacing = prev->last_category == unicode_general_category::Zp ? paragraph_spacing : line_spacing;
899 // Lines advance downward on the y-axis.
900 it->y = prev->y - spacing * height.in(pixels);
901 prev = it;
902 }
903 }
904
905 static void layout_lines_vertical_alignment(
906 text_shaper::line_vector& lines,
907 vertical_alignment alignment,
908 float baseline,
909 float min_y,
910 float max_y,
911 float sub_pixel_height) noexcept
912 {
913 hi_assert(not lines.empty());
914
915 // Calculate the y-adjustment needed to position the base-line of the text to y=0
916 auto adjustment = [&]() {
917 if (alignment == vertical_alignment::top) {
918 return -lines.front().y;
919
920 } else if (alignment == vertical_alignment::bottom) {
921 return -lines.back().y;
922
923 } else {
924 auto const mp_index = lines.size() / 2;
925 if (lines.size() % 2 == 1) {
926 return -lines[mp_index].y;
927
928 } else {
929 return -std::midpoint(lines[mp_index - 1].y, lines[mp_index].y);
930 }
931 }
932 }();
933
934 // Add the base-line to the adjustment.
935 adjustment += baseline;
936
937 // Clamp the adjustment between min_y and max_y.
938 // The text may not fit, prioritize to show the top lines.
939 if (lines.back().y + adjustment < min_y) {
940 adjustment = min_y - lines.back().y;
941 }
942 if (lines.front().y + adjustment > max_y) {
943 adjustment = max_y - lines.front().y;
944 }
945
946 // Reposition the lines, and round to sub-pixel boundary.
947 auto const rcp_sub_pixel_height = 1.0f / sub_pixel_height;
948 for (auto& line : lines) {
949 line.y = std::round((line.y + adjustment) * rcp_sub_pixel_height) * sub_pixel_height;
950 }
951 }
952
959 static void
960 bidi_algorithm(text_shaper::line_vector& lines, text_shaper::char_vector& text, unicode_bidi_context bidi_context) noexcept
961 {
962 hi_assert(not lines.empty());
963
964 // Create a list of all character indices.
966 // Make room for implicit line-separators.
967 char_its.reserve(text.size() + lines.size());
968 for (auto const& line : lines) {
969 // Add all the characters of a line.
970 for (auto it = line.first; it != line.last; ++it) {
971 char_its.push_back(it);
972 }
973 if (not is_Zp_or_Zl(line.last_category)) {
974 // No explicit paragraph-separator or line-separator, at a virtual one.
975 char_its.push_back(text.end());
976 }
977 }
978
979 auto const[char_its_last, paragraph_directions] = unicode_bidi(
980 char_its.begin(),
981 char_its.end(),
982 [&](text_shaper::char_const_iterator it) {
983 if (it != text.end()) {
984 return it->grapheme.starter();
985 } else {
986 return unicode_LS;
987 }
988 },
989 [&](text_shaper::char_iterator it, char32_t code_point) {
990 hi_axiom(it != text.end());
991 it->replace_glyph(code_point);
992 },
993 [&](text_shaper::char_iterator it, unicode_bidi_class direction) {
994 if (it != text.end()) {
995 it->direction = direction;
996 }
997 },
998 bidi_context);
999
1000 // The unicode bidi algorithm may have deleted a few characters.
1001 char_its.erase(char_its_last, char_its.cend());
1002
1003 // Add the paragraph direction for each line.
1004 auto par_it = paragraph_directions.cbegin();
1005 for (auto& line : lines) {
1006 hi_axiom(par_it != paragraph_directions.cend());
1007 line.paragraph_direction = *par_it;
1008 if (line.last_category == unicode_general_category::Zp) {
1009 par_it++;
1010 }
1011 }
1012 hi_assert(par_it <= paragraph_directions.cend());
1013
1014 // Add the character indices for each line in display order.
1015 auto line_it = lines.begin();
1016 line_it->columns.clear();
1017 auto column_nr = 0_uz;
1018 for (auto const char_it : char_its) {
1019 if (char_it == text.end()) {
1020 // Ignore the virtual line separators.
1021 continue;
1022 } else if (char_it >= line_it->last) {
1023 // Skip to the next line.
1024 hi_axiom(line_it->columns.size() <= narrow_cast<size_t>(std::distance(line_it->first, line_it->last)));
1025 ++line_it;
1026 line_it->columns.clear();
1027 column_nr = 0_uz;
1028 }
1029 hi_axiom(line_it != lines.end());
1030 hi_axiom(char_it >= line_it->first);
1031 hi_axiom(char_it < line_it->last);
1032 line_it->columns.push_back(char_it);
1033
1034 // Assign line_nr and column_nr, for quick back referencing.
1035 char_it->line_nr = line_it->line_nr;
1036 char_it->column_nr = column_nr++;
1037 }
1038
1039 // All of the characters in the text must be positioned.
1040 for (auto& c : text) {
1041 hi_axiom(c.line_nr != std::numeric_limits<size_t>::max() and c.column_nr != std::numeric_limits<size_t>::max());
1042 }
1043 }
1044
1045 [[nodiscard]] static generator<std::pair<std::vector<size_t>, float>>
1046 get_widths(unicode_break_vector const& opportunities, std::vector<float> const& widths, hi::pixel_density pixel_density) noexcept
1047 {
1048 struct entry_type {
1049 size_t min_height;
1050 size_t max_height;
1051 float min_width;
1052 float max_width;
1053 };
1054
1055 auto stack = std::vector<entry_type>{};
1056
1057 auto const a4_one_column = (au::milli(au::meters)(172.0f) * pixel_density.ppi).in(pixels);
1058 auto const a4_two_column = (au::milli(au::meters)(88.0f) * pixel_density.ppi).in(pixels);
1059
1060 // Max-width first.
1061 auto [max_width, max_lines] = detail::unicode_LB_maximum_width(opportunities, widths);
1062 auto height = max_lines.size();
1063 co_yield {std::move(max_lines), max_width};
1064
1065 if (max_width >= a4_two_column) {
1066 // If this is wide text, then only try a few sizes.
1067 if (max_width > a4_one_column) {
1068 auto [width, lines] = detail::unicode_LB_width(opportunities, widths, a4_one_column);
1069 if (std::exchange(height, lines.size()) > lines.size()) {
1070 co_yield {std::move(lines), width};
1071 }
1072 }
1073
1074 auto [width, lines] = detail::unicode_LB_width(opportunities, widths, a4_two_column);
1075 if (std::exchange(height, lines.size()) > lines.size()) {
1076 co_yield {std::move(lines), width};
1077 }
1078
1079 } else {
1080 // With small text we try every size that changes the number of lines.
1081 auto [min_width, min_lines] = detail::unicode_LB_minimum_width(opportunities, widths);
1082 if (min_lines.size() >= height) {
1083 // There are no multiple sizes.
1084 co_return;
1085 }
1086
1087 stack.emplace_back(min_lines.size(), height, min_width, max_width);
1088 co_yield {std::move(min_lines), min_width};
1089
1090 do {
1091 auto const entry = stack.back();
1092 stack.pop_back();
1093
1094 if (entry.max_height > entry.max_height + 1 and entry.max_width >= entry.min_width + 2.0f) {
1095 // There lines between the current two sizes; split in two.
1096 auto const half_width = (entry.min_width + entry.max_width) * 0.5f;
1097
1098 auto [split_width, split_lines] = detail::unicode_LB_width(opportunities, widths, half_width);
1099 auto const split_height = split_lines.size();
1100
1101 if (split_height == entry.min_height) {
1102 // We didn't find a proper split, need to try the upper half. Use `half_width` to split right down the
1103 // middle.
1104 stack.emplace_back(split_height, entry.max_height, half_width, entry.max_width);
1105
1106 } else if (split_height == entry.max_height) {
1107 // We didn't find a proper split, need to try the lower half. Use `half_width` to split right down the
1108 // middle.
1109 stack.emplace_back(entry.min_height, split_height, entry.min_width, half_width);
1110
1111 } else {
1112 // Split through the middle, use the split_width for faster searching.
1113 co_yield {std::move(split_lines), split_width};
1114 stack.emplace_back(entry.min_height, split_height, entry.min_width, split_width);
1115 stack.emplace_back(split_height, entry.max_height, split_width, entry.max_width);
1116 }
1117 }
1118 } while (not stack.empty());
1119 }
1120 }
1121
1130 [[nodiscard]] line_vector make_lines(
1131 aarectangle rectangle,
1132 float baseline,
1133 extent2 sub_pixel_size,
1134 float line_spacing,
1135 float paragraph_spacing) noexcept
1136 {
1137 auto const line_sizes = unicode_line_break(_line_break_opportunities, _line_break_widths, rectangle.width());
1138
1139 auto r = text_shaper::line_vector{};
1140 r.reserve(line_sizes.size());
1141
1142 auto char_it = _text.begin();
1143 auto width_it = _line_break_widths.begin();
1144 auto line_nr = 0_uz;
1145 for (auto const line_size : line_sizes) {
1146 hi_axiom(line_size > 0);
1147 auto const char_eol = char_it + line_size;
1148 auto const width_eol = width_it + line_size;
1149
1150 auto const line_width = detail::unicode_LB_width(width_it, width_eol);
1151 r.emplace_back(line_nr++, _text.begin(), char_it, char_eol, line_width, _initial_line_metrics);
1152
1153 char_it = char_eol;
1154 width_it = width_eol;
1155 }
1156
1157 if (r.empty() or is_Zp_or_Zl(r.back().last_category)) {
1158 r.emplace_back(line_nr++, _text.begin(), _text.end(), _text.end(), 0.0f, _initial_line_metrics);
1159 r.back().paragraph_direction = _text_direction;
1160 }
1161
1162 layout_lines_vertical_spacing(r, line_spacing, paragraph_spacing);
1163 layout_lines_vertical_alignment(
1164 r, _alignment.vertical(), baseline, rectangle.bottom(), rectangle.top(), sub_pixel_size.height());
1165
1166 return r;
1167 }
1168
1175 void position_glyphs(aarectangle rectangle, extent2 sub_pixel_size) noexcept
1176 {
1177 hi_assert(not _lines.empty());
1178
1179 // The bidi algorithm will reorder the characters on each line, and mirror the brackets in the text when needed.
1180 bidi_algorithm(_lines, _text, _bidi_context);
1181 for (auto& line : _lines) {
1182 // Position the glyphs on each line. Possibly morph glyphs to handle ligatures and calculate the bounding rectangles.
1183 line.layout(_alignment.horizontal(), rectangle.left(), rectangle.right(), sub_pixel_size.width());
1184 }
1185 }
1186
1189 void resolve_script() noexcept
1190 {
1191 // Find the first script in the text if no script is found use the text_shaper's default script.
1192 auto first_script = _script;
1193 for (auto& c : _text) {
1194 auto const script = ucd_get_script(c.grapheme.starter());
1195 if (script != iso_15924::wildcard() or script == iso_15924::uncoded() or script == iso_15924::common() or
1196 script == iso_15924::inherited()) {
1197 first_script = script;
1198 break;
1199 }
1200 }
1201
1202 // Backward pass: fix start of words and open-brackets.
1203 // After this pass unknown-script is no longer in the text.
1204 // Close brackets will not be fixed, those will be fixed in the last forward pass.
1205 auto word_script = iso_15924::common();
1206 auto previous_script = first_script;
1207 for (auto i = std::ssize(_text) - 1; i >= 0; --i) {
1208 auto& c = _text[i];
1209
1210 if (_word_break_opportunities[i + 1] != unicode_break_opportunity::no) {
1211 word_script = iso_15924::common();
1212 }
1213
1214 c.script = ucd_get_script(c.grapheme.starter());
1215 if (c.script == iso_15924::uncoded() or c.script == iso_15924::common()) {
1216 auto const bracket_type = ucd_get_bidi_paired_bracket_type(c.grapheme.starter());
1217 // clang-format off
1218 c.script =
1219 bracket_type == unicode_bidi_paired_bracket_type::o ? previous_script :
1220 bracket_type == unicode_bidi_paired_bracket_type::c ? iso_15924::common() :
1221 word_script;
1222 // clang-format on
1223
1224 } else if (c.script != iso_15924::inherited()) {
1225 previous_script = word_script = c.script;
1226 }
1227 }
1228
1229 // Forward pass: fix all common and inherited with previous or first script.
1230 previous_script = first_script;
1231 for (auto i = 0_uz; i != _text.size(); ++i) {
1232 auto& c = _text[i];
1233
1234 if (c.script == iso_15924::common() or c.script == iso_15924::inherited()) {
1235 c.script = previous_script;
1236
1237 } else {
1238 previous_script = c.script;
1239 }
1240 }
1241 }
1242
1244 get_selection_from_break(text_cursor cursor, unicode_break_vector const& break_opportunities) const noexcept
1245 {
1246 if (_text.empty()) {
1247 return {{}, {}};
1248 }
1249
1250 // In the algorithm below we search before and after the character that the cursor is at.
1251 // We do not use the before/after differentiation.
1252
1253 auto const first_index = [&]() {
1254 auto i = cursor.index();
1255 while (break_opportunities[i] == unicode_break_opportunity::no) {
1256 --i;
1257 }
1258 return i;
1259 }();
1260 auto const last_index = [&]() {
1261 auto i = cursor.index();
1262 while (break_opportunities[i + 1] == unicode_break_opportunity::no) {
1263 ++i;
1264 }
1265 return i;
1266 }();
1267
1268 return {get_before_cursor(first_index), get_after_cursor(last_index)};
1269 }
1270
1272 get_line_metrics(text_shaper::char_const_iterator first, text_shaper::char_const_iterator last) const noexcept
1273 {
1274 auto metrics = _initial_line_metrics;
1275 for (auto it = first; it != last; ++it) {
1276 // Only calculate line metrics based on visible characters.
1277 // For example a paragraph separator is seldom available in a font.
1278 if (is_visible(it->general_category)) {
1279 inplace_max(metrics, it->font_metrics());
1280 }
1281 }
1282
1283 auto const last_category = (first != last) ? (last - 1)->general_category : unicode_general_category::Cn;
1284 return {metrics, last_category};
1285 }
1286
1293 [[nodiscard]] float get_text_height(std::vector<size_t> const& lines) const noexcept
1294 {
1295 if (lines.empty()) {
1296 return 0.0f;
1297 }
1298
1299 auto line_it = lines.cbegin();
1300 auto char_it_first = _text.begin();
1301 auto char_it_last = char_it_first + *line_it++;
1302
1303 // Add the x-height of the first line.
1304 auto [previous_metrics, previous_category] = get_line_metrics(char_it_first, char_it_last);
1305 auto total_height = previous_metrics.x_height;
1306
1307 for (; line_it != lines.cend(); ++line_it) {
1308 char_it_first = std::exchange(char_it_last, char_it_last + *line_it);
1309
1310 // Advance to the base-line of the next line.
1311 auto [current_metrics, current_category] = get_line_metrics(char_it_first, char_it_last);
1312 auto const line_height = previous_metrics.descender + std::max(previous_metrics.line_gap, current_metrics.line_gap) +
1313 current_metrics.ascender;
1314
1315 auto const spacing = previous_category == unicode_general_category::Zp ? previous_metrics.paragraph_spacing :
1316 previous_metrics.line_spacing;
1317 total_height = total_height + spacing * line_height;
1318
1319 previous_metrics = std::move(current_metrics);
1320 previous_category = std::move(current_category);
1321 }
1322
1323 return total_height.in(pixels);
1324 }
1325};
1326
1327} // namespace hi::inline v1
@ rectangle
The gui_event has rectangle data.
unicode_bidi_class
Bidirectional class Unicode Standard Annex #9: https://unicode.org/reports/tr9/.
Definition ucd_bidi_classes.hpp:861
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
constexpr gstring to_gstring(std::u32string_view rhs, unicode_normalize_config config=unicode_normalize_config::NFC()) noexcept
Convert a UTF-32 string-view to a grapheme-string.
Definition gstring.hpp:263
unicode_break_vector unicode_line_break(It first, ItEnd last, CodePointFunc const &code_point_func) noexcept
The unicode line break algorithm UAX #14.
Definition unicode_line_break.hpp:633
unicode_break_vector unicode_sentence_break(It first, ItEnd last, CodePointFunc const &code_point_func) noexcept
The unicode word break algorithm UAX#29.
Definition unicode_sentence_break.hpp:258
constexpr extent2 sub_pixel_size(subpixel_orientation orientation) noexcept
Get the size of a sub-pixel based on the sub-pixel orientation.
Definition subpixel_orientation.hpp:41
constexpr std::pair< It, std::vector< unicode_bidi_class > > unicode_bidi(It first, It last, GetCodePoint get_code_point, SetCodePoint set_code_point, SetTextDirection set_text_direction, unicode_bidi_context const &context={})
Reorder a given range of characters based on the unicode_bidi algorithm.
Definition unicode_bidi.hpp:1219
constexpr unicode_bidi_class unicode_bidi_direction(It first, It last, GetCodePoint get_code_point, unicode_bidi_context const &context={})
Get the unicode bidi direction for the first paragraph and context.
Definition unicode_bidi.hpp:1259
hi_export font const & find_font(font_family_id family_id, font_variant variant=font_variant{}) noexcept
Find a font closest to the variant.
Definition font_book.hpp:415
unicode_break_vector unicode_word_break(It first, ItEnd last, CodePointFunc const &code_point_func) noexcept
The unicode word break algorithm UAX#29.
Definition unicode_word_break.hpp:240
Definition font_font.hpp:31
font_metrics_em metrics
The metrics of a font.
Definition font_font.hpp:65
Horizontal/Vertical alignment combination.
Definition alignment.hpp:244
A high-level geometric point Part of the high-level vec, point, mat and color types.
Definition point2.hpp:28
A cursor-position in text.
Definition text_cursor.hpp:26
constexpr text_cursor & resize(size_t size) &noexcept
Set the text size.
Definition text_cursor.hpp:49
Text shaper.
Definition text_shaper.hpp:38
text_cursor get_before_cursor(text_shaper::char_const_iterator it) const noexcept
Get the cursor before the character in logical order.
Definition text_shaper.hpp:457
std::pair< text_cursor, text_cursor > select_paragraph(text_cursor cursor) const noexcept
Get the selection for a paragraph at the cursor.
Definition text_shaper.hpp:587
std::pair< size_t, size_t > get_column_line(size_t index) const noexcept
Get the column and line of a character.
Definition text_shaper.hpp:389
char_const_iterator get_it(text_cursor cursor) const noexcept
Get the character at the cursor.
Definition text_shaper.hpp:303
char_const_iterator get_it(size_t column_nr, size_t line_nr) const noexcept
Get the character at column and row in display order.
Definition text_shaper.hpp:316
std::pair< text_cursor, text_cursor > select_sentence(text_cursor cursor) const noexcept
Get the selection for the sentence at the cursor.
Definition text_shaper.hpp:580
bool is_on_left(text_cursor cursor) const noexcept
Check if the cursor is on the left side of the character in display order.
Definition text_shaper.hpp:513
bool is_on_right(text_cursor cursor) const noexcept
Check if the cursor is on the right side of the character in display order.
Definition text_shaper.hpp:529
aarectangle bounding_rectangle(float maximum_line_width, float line_spacing=1.0f, float paragraph_spacing=1.5f) noexcept
Get bounding rectangle.
Definition text_shaper.hpp:209
text_cursor get_after_cursor(text_shaper::char_const_iterator it) const noexcept
Get the cursor after the character in logical order.
Definition text_shaper.hpp:467
unicode_bidi_class text_direction() const noexcept
Get the text-direction as a whole.
Definition text_shaper.hpp:263
aarectangle rectangle() const noexcept
The rectangle used when laying out the text.
Definition text_shaper.hpp:256
std::pair< text_cursor, text_cursor > select_char(text_cursor cursor) const noexcept
Get the selection for the character at the cursor.
Definition text_shaper.hpp:565
std::pair< size_t, size_t > get_column_line(text_shaper::char_const_iterator it) const noexcept
Get the column and line of a character.
Definition text_shaper.hpp:374
text_cursor get_left_cursor(text_shaper::char_const_iterator it) const noexcept
Get the cursor left of the character in display order.
Definition text_shaper.hpp:477
std::pair< text_cursor, text_cursor > select_word(text_cursor cursor) const noexcept
Get the selection for the word at the cursor.
Definition text_shaper.hpp:573
size_t get_index(text_shaper::char_const_iterator it) const noexcept
Get the index of the character in logical order.
Definition text_shaper.hpp:409
char_const_iterator get_it(size_t index) const noexcept
Get the character at index in logical order.
Definition text_shaper.hpp:285
text_cursor get_end_cursor() const noexcept
Get the cursor at the end of the document.
Definition text_shaper.hpp:427
std::pair< text_cursor, text_cursor > select_document(text_cursor cursor) const noexcept
Get the selection for a paragraph at the cursor.
Definition text_shaper.hpp:615
char_const_iterator move_right_char(char_const_iterator it) const noexcept
Get the character to the right.
Definition text_shaper.hpp:640
void layout(aarectangle rectangle, float baseline, extent2 sub_pixel_size, float line_spacing=1.0f, float paragraph_spacing=1.5f) noexcept
Layout the lines of the text.
Definition text_shaper.hpp:241
char_const_iterator get_it(std::pair< size_t, size_t > column_row) const noexcept
Get the character at column and row in display order.
Definition text_shaper.hpp:364
text_cursor get_begin_cursor() const noexcept
Get the cursor at the beginning of the document.
Definition text_shaper.hpp:418
text_cursor get_after_cursor(size_t index) const noexcept
Get the cursor after the character in logical order.
Definition text_shaper.hpp:447
text_shaper(gstring const &text, text_style const &style, hi::pixel_density pixel_density, hi::alignment alignment, bool left_to_right, iso_15924 script=iso_15924{"Zyyy"}) noexcept
Construct a text_shaper with a text and alignment.
Definition text_shaper.hpp:83
alignment resolved_alignment() const noexcept
Get the resolved alignment of the text.
Definition text_shaper.hpp:273
text_cursor get_nearest_cursor(point2 position) const noexcept
find the nearest character.
Definition text_shaper.hpp:545
text_cursor get_before_cursor(size_t index) const noexcept
Get the cursor before the character in logical order.
Definition text_shaper.hpp:437
std::pair< size_t, size_t > get_column_line(text_cursor cursor) const noexcept
Get the column and line of a character.
Definition text_shaper.hpp:399
text_cursor get_right_cursor(text_shaper::char_const_iterator it) const noexcept
Get the cursor right of the character in display order.
Definition text_shaper.hpp:495
char_const_iterator move_left_char(char_const_iterator it) const noexcept
Get the character to the left.
Definition text_shaper.hpp:629
Definition text_style.hpp:183
A grapheme-cluster, what a user thinks a character is.
Definition grapheme.hpp:167
Definition pixel_density.hpp:16
T begin(T... args)
T ceil(T... args)
T distance(T... args)
T end(T... args)
T isnan(T... args)
T lowest(T... args)
T max(T... args)
T move(T... args)
T prev(T... args)
T reserve(T... args)
T round(T... args)
T size(T... args)