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