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;
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),
94 auto const&
font =
find_font(style->family_id, style->variant);
95 _initial_line_metrics = (style->size * dpi_scale) *
font.
metrics;
97 _text.reserve(text.size());
98 for (
auto const& c : text) {
99 auto const clean_c = c ==
'\n' ?
grapheme{unicode_PS} : c;
101 auto& tmp = _text.emplace_back(clean_c, style, dpi_scale);
102 tmp.initialize_glyph(font);
108 [](text_shaper::char_const_reference it) {
109 return it.grapheme.starter();
113 _line_break_opportunities =
unicode_line_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
114 return c.grapheme.starter();
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);
122 _word_break_opportunities =
unicode_word_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
123 return c.grapheme.starter();
126 _sentence_break_opportunities =
unicode_sentence_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
127 return c.grapheme.starter();
133 [[nodiscard]] text_shaper(
134 std::string_view text,
135 text_style
const& style,
139 iso_15924 script = iso_15924{
"Zyyy"})
noexcept :
140 text_shaper(
to_gstring(text), style, dpi_scale, alignment, left_to_right, script)
144 [[nodiscard]]
bool empty() const noexcept
146 return _text.empty();
149 [[nodiscard]]
size_t size() const noexcept
154 [[nodiscard]] char_iterator
begin() noexcept
156 return _text.begin();
159 [[nodiscard]] char_const_iterator
begin() const noexcept
161 return _text.begin();
164 [[nodiscard]] char_const_iterator cbegin() const noexcept
166 return _text.cbegin();
169 [[nodiscard]] char_iterator
end() noexcept
174 [[nodiscard]] char_const_iterator
end() const noexcept
179 [[nodiscard]] char_const_iterator cend() const noexcept
184 auto const& lines() const noexcept
207 [[nodiscard]] aarectangle
208 bounding_rectangle(
float maximum_line_width,
float line_spacing = 1.0f,
float paragraph_spacing = 1.5f) noexcept
210 auto const rectangle = aarectangle{
212 constexpr auto baseline = 0.0f;
215 auto const lines = make_lines(rectangle, baseline,
sub_pixel_size, line_spacing, paragraph_spacing);
216 hi_assert(not lines.empty());
218 auto max_width = 0.0f;
219 for (
auto& line : lines) {
220 inplace_max(max_width, line.width);
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);
241 aarectangle rectangle,
243 extent2 sub_pixel_size,
244 float line_spacing = 1.0f,
245 float paragraph_spacing = 1.5f) noexcept
247 _rectangle = rectangle;
248 _lines = make_lines(rectangle, baseline,
sub_pixel_size, line_spacing, paragraph_spacing);
249 hi_assert(not _lines.empty());
264 return _text_direction;
274 return resolve(_alignment, _text_direction == unicode_bidi_class::L);
284 [[nodiscard]] char_const_iterator
get_it(
size_t index)
const noexcept
286 if (
static_cast<ptrdiff_t
>(index) < 0) {
288 }
else if (index >= size()) {
292 return begin() + index;
304 return get_it(cursor.index());
315 [[nodiscard]] char_const_iterator
get_it(
size_t column_nr,
size_t line_nr)
const noexcept
317 hi_assert(not _lines.empty());
319 if (
static_cast<ptrdiff_t
>(line_nr) < 0) {
321 }
else if (line_nr >= _lines.size()) {
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();
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;
333 if (
static_cast<ptrdiff_t
>(--line_nr) < 0) {
337 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].back() :
338 _lines[line_nr].front();
343 if (++line_nr >= _lines.size()) {
347 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].front() :
348 _lines[line_nr].back();
353 return _lines[line_nr][column_nr];
365 return get_it(column_row.first, column_row.second);
376 return {it->column_nr, it->line_nr};
378 hi_assert(not _lines.empty());
379 return {_lines.size() - 1, _lines.back().size()};
390 return get_column_line(get_it(index));
400 return get_column_line(cursor.index());
408 [[nodiscard]]
size_t get_index(text_shaper::char_const_iterator it)
const noexcept
428 return text_cursor{size() - 1,
true}.resize(size());
458 return get_before_cursor(get_index(it));
468 return get_after_cursor(get_index(it));
479 if (it->direction == unicode_bidi_class::L) {
480 return get_before_cursor(it);
482 return get_after_cursor(it);
485 return get_end_cursor();
497 if (it->direction == unicode_bidi_class::L) {
498 return get_after_cursor(it);
500 return get_before_cursor(it);
503 return get_end_cursor();
514 auto const it = get_it(cursor);
516 return (it->direction == unicode_bidi_class::L) == cursor.before();
518 hi_assert(begin() == end());
530 auto const it = get_it(cursor);
532 return (it->direction == unicode_bidi_class::L) == cursor.after();
534 hi_assert(begin() == end());
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());
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};
566 auto const index = cursor.index();
567 return {get_before_cursor(index), get_after_cursor(index)};
574 return get_selection_from_break(cursor, _word_break_opportunities);
581 return get_selection_from_break(cursor, _sentence_break_opportunities);
588 auto const first_index = [&]() {
589 auto i = cursor.index();
591 if (_text[i - 1].general_category == unicode_general_category::Zp) {
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) {
609 return {get_before_cursor(first_index), get_after_cursor(last_index)};
620 return {{}, get_end_cursor()};
628 [[nodiscard]] char_const_iterator
move_left_char(char_const_iterator it)
const noexcept
630 auto const[column_nr, line_nr] = get_column_line(it);
631 return get_it(column_nr - 1, line_nr);
639 [[nodiscard]] char_const_iterator
move_right_char(char_const_iterator it)
const noexcept
641 auto const[column_nr, line_nr] = get_column_line(it);
642 return get_it(column_nr + 1, line_nr);
647 auto it = get_it(cursor);
648 if (overwrite_mode) {
649 it = move_left_char(it);
650 return get_before_cursor(it);
653 if (is_on_left(cursor)) {
655 it = move_left_char(it);
658 return get_left_cursor(it);
662 [[nodiscard]] text_cursor move_right_char(text_cursor cursor,
bool overwrite_mode)
const noexcept
664 auto it = get_it(cursor);
665 if (overwrite_mode) {
666 it = move_right_char(it);
667 return get_before_cursor(it);
670 if (is_on_right(cursor)) {
672 it = move_right_char(it);
675 return get_right_cursor(it);
679 [[nodiscard]] text_cursor move_down_char(text_cursor cursor,
float& x)
const noexcept
685 auto [column_nr, line_nr] = get_column_line(cursor);
686 if (++line_nr == _lines.size()) {
687 return get_end_cursor();
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();
696 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
697 return get_before_cursor(new_char_it);
700 [[nodiscard]] text_cursor move_up_char(text_cursor cursor,
float& x)
const noexcept
706 auto [column_nr, line_nr] = get_column_line(cursor);
707 if (line_nr-- == 0) {
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();
717 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
718 return get_before_cursor(new_char_it);
721 [[nodiscard]] text_cursor move_left_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
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);
730 it = move_left_char(it);
732 return get_end_cursor();
735 [[nodiscard]] text_cursor move_right_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
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);
744 it = move_right_char(it);
746 return get_end_cursor();
749 [[nodiscard]] text_cursor move_begin_line(text_cursor cursor)
const noexcept
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);
756 [[nodiscard]] text_cursor move_end_line(text_cursor cursor)
const noexcept
758 auto const[column_nr, line_nr] = get_column_line(cursor);
759 auto const& line = _lines[line_nr];
762 while (it != line.first) {
764 if (not it->is_trailing_white_space) {
769 return get_after_cursor(it);
772 [[nodiscard]] text_cursor move_begin_sentence(text_cursor cursor)
const noexcept
774 if (cursor.after()) {
775 cursor = {cursor.index(),
false};
776 }
else if (cursor.index() != 0) {
777 cursor = {cursor.index() - 1,
false};
779 auto const[first, last] = select_sentence(cursor);
780 return first.before_neighbor(size());
783 [[nodiscard]] text_cursor move_end_sentence(text_cursor cursor)
const noexcept
785 if (cursor.before()) {
786 cursor = {cursor.index(),
true};
787 }
else if (cursor.index() != _text.size() - 1) {
788 cursor = {cursor.index() + 1,
true};
790 auto const[first, last] = select_sentence(cursor);
791 return last.before_neighbor(size());
794 [[nodiscard]] text_cursor move_begin_paragraph(text_cursor cursor)
const noexcept
796 if (cursor.after()) {
797 cursor = {cursor.index(),
false};
798 }
else if (cursor.index() != 0) {
799 cursor = {cursor.index() - 1,
false};
801 auto const[first, last] = select_paragraph(cursor);
802 return first.before_neighbor(size());
805 [[nodiscard]] text_cursor move_end_paragraph(text_cursor cursor)
const noexcept
807 if (cursor.before()) {
808 cursor = {cursor.index(),
true};
809 }
else if (cursor.index() != _text.size() - 1) {
810 cursor = {cursor.index() + 1,
true};
812 auto const[first, last] = select_paragraph(cursor);
813 return last.before_neighbor(size());
816 [[nodiscard]] text_cursor move_begin_document(text_cursor cursor)
const noexcept
821 [[nodiscard]] text_cursor move_end_document(text_cursor cursor)
const noexcept
827 return get_end_cursor();
847 unicode_break_vector _line_break_opportunities;
855 unicode_break_vector _word_break_opportunities;
859 unicode_break_vector _sentence_break_opportunities;
863 unicode_bidi_context _bidi_context;
881 font_metrics _initial_line_metrics;
885 aarectangle _rectangle;
888 layout_lines_vertical_spacing(text_shaper::line_vector& lines,
float line_spacing,
float paragraph_spacing)
noexcept
890 hi_assert(not lines.empty());
892 auto prev = lines.begin();
894 for (
auto it = prev + 1; it != lines.end(); ++it) {
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;
899 it->y =
prev->y - spacing * height;
904 static void layout_lines_vertical_alignment(
905 text_shaper::line_vector& lines,
906 vertical_alignment alignment,
910 float sub_pixel_height)
noexcept
912 hi_assert(not lines.empty());
915 auto adjustment = [&]() {
916 if (alignment == vertical_alignment::top) {
917 return -lines.front().y;
919 }
else if (alignment == vertical_alignment::bottom) {
920 return -lines.back().y;
923 auto const mp_index = lines.size() / 2;
924 if (lines.size() % 2 == 1) {
925 return -lines[mp_index].y;
928 return -std::midpoint(lines[mp_index - 1].y, lines[mp_index].y);
934 adjustment += baseline;
938 if (lines.back().y + adjustment < min_y) {
939 adjustment = min_y - lines.back().y;
941 if (lines.front().y + adjustment > max_y) {
942 adjustment = max_y - lines.front().y;
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;
959 bidi_algorithm(text_shaper::line_vector& lines, text_shaper::char_vector& text, unicode_bidi_context bidi_context)
noexcept
961 hi_assert(not lines.empty());
966 char_its.
reserve(text.size() + lines.size());
967 for (
auto const& line : lines) {
969 for (
auto it = line.first; it != line.last; ++it) {
970 char_its.push_back(it);
972 if (not is_Zp_or_Zl(line.last_category)) {
974 char_its.push_back(text.end());
978 auto const[char_its_last, paragraph_directions] =
unicode_bidi(
981 [&](text_shaper::char_const_iterator it) {
982 if (it != text.end()) {
983 return it->grapheme.starter();
988 [&](text_shaper::char_iterator it,
char32_t code_point) {
989 hi_axiom(it != text.end());
990 it->replace_glyph(code_point);
993 if (it != text.end()) {
994 it->direction = direction;
1000 char_its.erase(char_its_last, char_its.cend());
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) {
1011 hi_assert(par_it <= paragraph_directions.cend());
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()) {
1021 }
else if (char_it >= line_it->last) {
1023 hi_axiom(line_it->columns.size() <= narrow_cast<size_t>(
std::distance(line_it->first, line_it->last)));
1025 line_it->columns.clear();
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);
1034 char_it->line_nr = line_it->line_nr;
1035 char_it->column_nr = column_nr++;
1039 for (
auto& c : text) {
1068 if (std::exchange(height, lines.size()) > lines.size()) {
1074 if (std::exchange(height, lines.size()) > lines.size()) {
1090 auto const entry = stack.back();
1117 }
while (
not stack.empty());
1130 aarectangle rectangle,
1132 extent2 sub_pixel_size,
1134 float paragraph_spacing)
noexcept
1138 auto r = text_shaper::line_vector{};
1143 auto line_nr = 0
_uz;
1150 r.emplace_back(line_nr++, _text.begin(),
char_it,
char_eol, line_width, _initial_line_metrics);
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;
1161 layout_lines_vertical_spacing(r, line_spacing, paragraph_spacing);
1162 layout_lines_vertical_alignment(
1174 void position_glyphs(aarectangle rectangle, extent2 sub_pixel_size)
noexcept
1176 hi_assert(
not _lines.empty());
1179 bidi_algorithm(_lines, _text, _bidi_context);
1180 for (
auto& line : _lines) {
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()) {
1206 for (
auto i = std::ssize(_text) - 1; i >= 0; --i) {
1209 if (_word_break_opportunities[i + 1] != unicode_break_opportunity::no) {
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());
1218 bracket_type == unicode_bidi_paired_bracket_type::o ?
previous_script :
1219 bracket_type == unicode_bidi_paired_bracket_type::c ? iso_15924::common() :
1223 }
else if (c.script != iso_15924::inherited()) {
1230 for (
auto i = 0
_uz; i != _text.size(); ++i) {
1233 if (c.script == iso_15924::common()
or c.script == iso_15924::inherited()) {
1243 get_selection_from_break(text_cursor cursor, unicode_break_vector
const&
break_opportunities)
const noexcept
1245 if (_text.empty()) {
1253 auto i = cursor.index();
1260 auto i = cursor.index();
1271 get_line_metrics(text_shaper::char_const_iterator first, text_shaper::char_const_iterator last)
const noexcept
1273 auto metrics = _initial_line_metrics;
1274 for (
auto it = first;
it != last; ++
it) {
1277 if (is_visible(
it->general_category)) {
1278 inplace_max(metrics,
it->font_metrics());
1282 auto const last_category = (first != last) ? (last - 1)->general_category : unicode_general_category::Cn;
1283 return {metrics, last_category};
1294 if (lines.empty()) {
1298 auto line_it = lines.cbegin();