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