41 using char_iterator = char_vector::iterator;
42 using char_const_iterator = char_vector::const_iterator;
43 using char_reference = char_vector::reference;
44 using char_const_reference = char_vector::const_reference;
46 using line_iterator = line_vector::iterator;
47 using line_const_iterator = line_vector::const_iterator;
89 iso_15924 script = iso_15924{
"Zyyy"})
noexcept :
90 _bidi_context(left_to_right ? unicode_bidi_class::L : unicode_bidi_class::R),
91 _pixel_density(pixel_density),
92 _alignment(alignment),
95 auto const&
font =
find_font(style->family_id, style->variant);
96 _initial_line_metrics = style->size * _pixel_density *
font.
metrics;
98 _text.reserve(text.
size());
99 for (
auto const& c : text) {
100 auto const clean_c = c ==
'\n' ?
grapheme{unicode_PS} : c;
102 auto& tmp = _text.emplace_back(clean_c, style, _pixel_density);
103 tmp.initialize_glyph(font);
109 [](text_shaper::char_const_reference it) {
110 return it.grapheme.starter();
114 _line_break_opportunities =
unicode_line_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
115 return c.grapheme.starter();
118 _line_break_widths.reserve(text.
size());
119 for (
auto const& c : _text) {
120 _line_break_widths.push_back(is_visible(c.general_category) ? c.width : -c.width);
123 _word_break_opportunities =
unicode_word_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
124 return c.grapheme.starter();
127 _sentence_break_opportunities =
unicode_sentence_break(_text.begin(), _text.end(), [](
auto const& c) ->
decltype(
auto) {
128 return c.grapheme.starter();
134 [[nodiscard]] text_shaper(
135 std::string_view text,
136 text_style
const& style,
140 iso_15924 script = iso_15924{
"Zyyy"})
noexcept :
141 text_shaper(
to_gstring(text), style, pixel_density, alignment, left_to_right, script)
145 [[nodiscard]]
bool empty() const noexcept
147 return _text.empty();
150 [[nodiscard]]
size_t size() const noexcept
155 [[nodiscard]] char_iterator
begin() noexcept
157 return _text.begin();
160 [[nodiscard]] char_const_iterator
begin() const noexcept
162 return _text.begin();
165 [[nodiscard]] char_const_iterator cbegin() const noexcept
167 return _text.cbegin();
170 [[nodiscard]] char_iterator
end() noexcept
175 [[nodiscard]] char_const_iterator
end() const noexcept
180 [[nodiscard]] char_const_iterator cend() const noexcept
185 auto const& lines() const noexcept
208 [[nodiscard]] aarectangle
209 bounding_rectangle(
float maximum_line_width,
float line_spacing = 1.0f,
float paragraph_spacing = 1.5f) noexcept
211 auto const rectangle = aarectangle{
213 constexpr auto baseline = 0.0f;
216 auto const lines = make_lines(rectangle, baseline,
sub_pixel_size, line_spacing, paragraph_spacing);
217 hi_assert(not lines.empty());
219 auto max_width = 0.0f;
220 for (
auto& line : lines) {
221 inplace_max(max_width, line.width);
224 auto const max_y = lines.front().y +
std::ceil(lines.front().metrics.ascender.in(pixels));
225 auto const min_y = lines.back().y -
std::ceil(lines.back().metrics.descender.in(pixels));
242 aarectangle rectangle,
244 extent2 sub_pixel_size,
245 float line_spacing = 1.0f,
246 float paragraph_spacing = 1.5f) noexcept
248 _rectangle = rectangle;
249 _lines = make_lines(rectangle, baseline,
sub_pixel_size, line_spacing, paragraph_spacing);
250 hi_assert(not _lines.empty());
265 return _text_direction;
275 return resolve(_alignment, _text_direction == unicode_bidi_class::L);
285 [[nodiscard]] char_const_iterator
get_it(
size_t index)
const noexcept
287 if (
static_cast<ptrdiff_t
>(index) < 0) {
289 }
else if (index >= size()) {
293 return begin() + index;
305 return get_it(cursor.index());
316 [[nodiscard]] char_const_iterator
get_it(
size_t column_nr,
size_t line_nr)
const noexcept
318 hi_assert(not _lines.empty());
320 if (
static_cast<ptrdiff_t
>(line_nr) < 0) {
322 }
else if (line_nr >= _lines.size()) {
326 auto const left_of_line =
static_cast<ptrdiff_t
>(column_nr) < 0;
327 auto const right_of_line = column_nr >= _lines[line_nr].size();
329 if (left_of_line or right_of_line) {
330 auto const ltr = _lines[line_nr].paragraph_direction == unicode_bidi_class::L;
331 auto const go_up = left_of_line == ltr;
334 if (
static_cast<ptrdiff_t
>(--line_nr) < 0) {
338 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].back() :
339 _lines[line_nr].front();
344 if (++line_nr >= _lines.size()) {
348 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].front() :
349 _lines[line_nr].back();
354 return _lines[line_nr][column_nr];
366 return get_it(column_row.first, column_row.second);
377 return {it->column_nr, it->line_nr};
379 hi_assert(not _lines.empty());
380 return {_lines.size() - 1, _lines.back().size()};
391 return get_column_line(get_it(index));
401 return get_column_line(cursor.index());
409 [[nodiscard]]
size_t get_index(text_shaper::char_const_iterator it)
const noexcept
429 return text_cursor{size() - 1,
true}.resize(size());
459 return get_before_cursor(get_index(it));
469 return get_after_cursor(get_index(it));
480 if (it->direction == unicode_bidi_class::L) {
481 return get_before_cursor(it);
483 return get_after_cursor(it);
486 return get_end_cursor();
498 if (it->direction == unicode_bidi_class::L) {
499 return get_after_cursor(it);
501 return get_before_cursor(it);
504 return get_end_cursor();
515 auto const it = get_it(cursor);
517 return (it->direction == unicode_bidi_class::L) == cursor.before();
519 hi_assert(begin() == end());
531 auto const it = get_it(cursor);
533 return (it->direction == unicode_bidi_class::L) == cursor.after();
535 hi_assert(begin() == end());
551 auto const line_it = std::ranges::min_element(_lines, std::ranges::less{}, [position](
auto const& line) {
552 return std::abs(line.y - position.y());
555 if (line_it != _lines.end()) {
556 auto const[char_it, after] = line_it->get_nearest(position);
557 return {narrow_cast<size_t>(
std::distance(_text.begin(), char_it)), after};
567 auto const index = cursor.index();
568 return {get_before_cursor(index), get_after_cursor(index)};
575 return get_selection_from_break(cursor, _word_break_opportunities);
582 return get_selection_from_break(cursor, _sentence_break_opportunities);
589 auto const first_index = [&]() {
590 auto i = cursor.index();
592 if (_text[i - 1].general_category == unicode_general_category::Zp) {
599 auto const last_index = [&]() {
600 auto i = cursor.index();
601 while (i < _text.size()) {
602 if (_text[i].general_category == unicode_general_category::Zp) {
610 return {get_before_cursor(first_index), get_after_cursor(last_index)};
621 return {{}, get_end_cursor()};
629 [[nodiscard]] char_const_iterator
move_left_char(char_const_iterator it)
const noexcept
631 auto const[column_nr, line_nr] = get_column_line(it);
632 return get_it(column_nr - 1, line_nr);
640 [[nodiscard]] char_const_iterator
move_right_char(char_const_iterator it)
const noexcept
642 auto const[column_nr, line_nr] = get_column_line(it);
643 return get_it(column_nr + 1, line_nr);
648 auto it = get_it(cursor);
649 if (overwrite_mode) {
650 it = move_left_char(it);
651 return get_before_cursor(it);
654 if (is_on_left(cursor)) {
656 it = move_left_char(it);
659 return get_left_cursor(it);
663 [[nodiscard]] text_cursor move_right_char(text_cursor cursor,
bool overwrite_mode)
const noexcept
665 auto it = get_it(cursor);
666 if (overwrite_mode) {
667 it = move_right_char(it);
668 return get_before_cursor(it);
671 if (is_on_right(cursor)) {
673 it = move_right_char(it);
676 return get_right_cursor(it);
680 [[nodiscard]] text_cursor move_down_char(text_cursor cursor,
float& x)
const noexcept
686 auto [column_nr, line_nr] = get_column_line(cursor);
687 if (++line_nr == _lines.size()) {
688 return get_end_cursor();
692 auto const char_it = get_it(cursor);
693 hi_assert(char_it != _text.end());
694 x = is_on_left(cursor) ? char_it->rectangle.left() : char_it->rectangle.right();
697 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
698 return get_before_cursor(new_char_it);
701 [[nodiscard]] text_cursor move_up_char(text_cursor cursor,
float& x)
const noexcept
707 auto [column_nr, line_nr] = get_column_line(cursor);
708 if (line_nr-- == 0) {
713 auto char_it = get_it(cursor);
714 hi_assert(char_it < _text.end());
715 x = is_on_left(cursor) ? char_it->rectangle.left() : char_it->rectangle.right();
718 auto const[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
719 return get_before_cursor(new_char_it);
722 [[nodiscard]] text_cursor move_left_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
724 cursor = move_left_char(cursor, overwrite_mode).before_neighbor(size());
725 auto it = get_it(cursor);
726 while (it !=
end()) {
727 if (it->general_category != unicode_general_category::Zs and
728 _word_break_opportunities[get_index(it)] != unicode_break_opportunity::no) {
729 return get_before_cursor(it);
731 it = move_left_char(it);
733 return get_end_cursor();
736 [[nodiscard]] text_cursor move_right_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
738 cursor = move_right_char(cursor, overwrite_mode).before_neighbor(size());
739 auto it = get_it(cursor);
740 while (it !=
end()) {
741 if (it->general_category != unicode_general_category::Zs and
742 _word_break_opportunities[get_index(it)] != unicode_break_opportunity::no) {
743 return get_before_cursor(it);
745 it = move_right_char(it);
747 return get_end_cursor();
750 [[nodiscard]] text_cursor move_begin_line(text_cursor cursor)
const noexcept
752 auto const[column_nr, line_nr] = get_column_line(cursor);
753 auto const& line = _lines[line_nr];
754 return get_before_cursor(line.first);
757 [[nodiscard]] text_cursor move_end_line(text_cursor cursor)
const noexcept
759 auto const[column_nr, line_nr] = get_column_line(cursor);
760 auto const& line = _lines[line_nr];
763 while (it != line.first) {
765 if (not it->is_trailing_white_space) {
770 return get_after_cursor(it);
773 [[nodiscard]] text_cursor move_begin_sentence(text_cursor cursor)
const noexcept
775 if (cursor.after()) {
776 cursor = {cursor.index(),
false};
777 }
else if (cursor.index() != 0) {
778 cursor = {cursor.index() - 1,
false};
780 auto const[first, last] = select_sentence(cursor);
781 return first.before_neighbor(size());
784 [[nodiscard]] text_cursor move_end_sentence(text_cursor cursor)
const noexcept
786 if (cursor.before()) {
787 cursor = {cursor.index(),
true};
788 }
else if (cursor.index() != _text.size() - 1) {
789 cursor = {cursor.index() + 1,
true};
791 auto const[first, last] = select_sentence(cursor);
792 return last.before_neighbor(size());
795 [[nodiscard]] text_cursor move_begin_paragraph(text_cursor cursor)
const noexcept
797 if (cursor.after()) {
798 cursor = {cursor.index(),
false};
799 }
else if (cursor.index() != 0) {
800 cursor = {cursor.index() - 1,
false};
802 auto const[first, last] = select_paragraph(cursor);
803 return first.before_neighbor(size());
806 [[nodiscard]] text_cursor move_end_paragraph(text_cursor cursor)
const noexcept
808 if (cursor.before()) {
809 cursor = {cursor.index(),
true};
810 }
else if (cursor.index() != _text.size() - 1) {
811 cursor = {cursor.index() + 1,
true};
813 auto const[first, last] = select_paragraph(cursor);
814 return last.before_neighbor(size());
817 [[nodiscard]] text_cursor move_begin_document(text_cursor cursor)
const noexcept
822 [[nodiscard]] text_cursor move_end_document(text_cursor cursor)
const noexcept
828 return get_end_cursor();
848 unicode_break_vector _line_break_opportunities;
856 unicode_break_vector _word_break_opportunities;
860 unicode_break_vector _sentence_break_opportunities;
864 unicode_bidi_context _bidi_context;
882 font_metrics_px _initial_line_metrics;
886 aarectangle _rectangle;
889 layout_lines_vertical_spacing(text_shaper::line_vector& lines,
float line_spacing,
float paragraph_spacing)
noexcept
891 hi_assert(not lines.empty());
893 auto prev = lines.begin();
895 for (
auto it = prev + 1; it != lines.end(); ++it) {
897 prev->metrics.descender +
std::max(
prev->metrics.line_gap, it->metrics.line_gap) + it->metrics.ascender;
898 auto const spacing =
prev->last_category == unicode_general_category::Zp ? paragraph_spacing : line_spacing;
900 it->y =
prev->y - spacing * height.in(pixels);
905 static void layout_lines_vertical_alignment(
906 text_shaper::line_vector& lines,
907 vertical_alignment alignment,
911 float sub_pixel_height)
noexcept
913 hi_assert(not lines.empty());
916 auto adjustment = [&]() {
917 if (alignment == vertical_alignment::top) {
918 return -lines.front().y;
920 }
else if (alignment == vertical_alignment::bottom) {
921 return -lines.back().y;
924 auto const mp_index = lines.size() / 2;
925 if (lines.size() % 2 == 1) {
926 return -lines[mp_index].y;
929 return -std::midpoint(lines[mp_index - 1].y, lines[mp_index].y);
935 adjustment += baseline;
939 if (lines.back().y + adjustment < min_y) {
940 adjustment = min_y - lines.back().y;
942 if (lines.front().y + adjustment > max_y) {
943 adjustment = max_y - lines.front().y;
947 auto const rcp_sub_pixel_height = 1.0f / sub_pixel_height;
948 for (
auto& line : lines) {
949 line.y =
std::round((line.y + adjustment) * rcp_sub_pixel_height) * sub_pixel_height;
960 bidi_algorithm(text_shaper::line_vector& lines, text_shaper::char_vector& text, unicode_bidi_context bidi_context)
noexcept
962 hi_assert(not lines.empty());
968 for (
auto const& line : lines) {
970 for (
auto it = line.first; it != line.last; ++it) {
971 char_its.push_back(it);
973 if (not is_Zp_or_Zl(line.last_category)) {
975 char_its.push_back(text.
end());
979 auto const[char_its_last, paragraph_directions] =
unicode_bidi(
982 [&](text_shaper::char_const_iterator it) {
983 if (it != text.end()) {
984 return it->grapheme.starter();
989 [&](text_shaper::char_iterator it,
char32_t code_point) {
990 hi_axiom(it != text.
end());
991 it->replace_glyph(code_point);
994 if (it != text.
end()) {
995 it->direction = direction;
1001 char_its.erase(char_its_last, char_its.cend());
1004 auto par_it = paragraph_directions.cbegin();
1005 for (
auto& line : lines) {
1006 hi_axiom(par_it != paragraph_directions.cend());
1007 line.paragraph_direction = *par_it;
1008 if (line.last_category == unicode_general_category::Zp) {
1012 hi_assert(par_it <= paragraph_directions.cend());
1015 auto line_it = lines.begin();
1016 line_it->columns.clear();
1017 auto column_nr = 0_uz;
1018 for (
auto const char_it : char_its) {
1019 if (char_it == text.
end()) {
1022 }
else if (char_it >= line_it->last) {
1024 hi_axiom(line_it->columns.size() <= narrow_cast<size_t>(
std::distance(line_it->first, line_it->last)));
1026 line_it->columns.clear();
1029 hi_axiom(line_it != lines.end());
1030 hi_axiom(char_it >= line_it->first);
1031 hi_axiom(char_it < line_it->last);
1032 line_it->columns.push_back(char_it);
1035 char_it->line_nr = line_it->line_nr;
1036 char_it->column_nr = column_nr++;
1040 for (
auto& c : text) {
1045 [[nodiscard]]
static generator<std::pair<std::vector<size_t>,
float>>
1057 auto const a4_one_column = (au::milli(au::meters)(172.0f) * pixel_density.ppi).in(pixels);
1058 auto const a4_two_column = (au::milli(au::meters)(88.0f) * pixel_density.ppi).in(pixels);
1061 auto [max_width, max_lines] = detail::unicode_LB_maximum_width(opportunities, widths);
1062 auto height = max_lines.size();
1063 co_yield {
std::move(max_lines), max_width};
1065 if (max_width >= a4_two_column) {
1067 if (max_width > a4_one_column) {
1068 auto [width, lines] = detail::unicode_LB_width(opportunities, widths, a4_one_column);
1069 if (std::exchange(height, lines.size()) > lines.size()) {
1074 auto [width, lines] = detail::unicode_LB_width(opportunities, widths, a4_two_column);
1075 if (std::exchange(height, lines.size()) > lines.size()) {
1081 auto [min_width, min_lines] = detail::unicode_LB_minimum_width(opportunities, widths);
1082 if (min_lines.size() >= height) {
1087 stack.emplace_back(min_lines.size(), height, min_width, max_width);
1088 co_yield {
std::move(min_lines), min_width};
1091 auto const entry = stack.back();
1094 if (entry.max_height > entry.max_height + 1 and entry.max_width >= entry.min_width + 2.0f) {
1096 auto const half_width = (entry.min_width + entry.max_width) * 0.5f;
1098 auto [split_width, split_lines] = detail::unicode_LB_width(opportunities, widths, half_width);
1099 auto const split_height = split_lines.size();
1101 if (split_height == entry.min_height) {
1104 stack.emplace_back(split_height, entry.max_height, half_width, entry.max_width);
1106 }
else if (split_height == entry.max_height) {
1109 stack.emplace_back(entry.min_height, split_height, entry.min_width, half_width);
1113 co_yield {
std::move(split_lines), split_width};
1114 stack.emplace_back(entry.min_height, split_height, entry.min_width, split_width);
1115 stack.emplace_back(split_height, entry.max_height, split_width, entry.max_width);
1118 }
while (not stack.empty());
1130 [[nodiscard]] line_vector make_lines(
1131 aarectangle rectangle,
1133 extent2 sub_pixel_size,
1135 float paragraph_spacing)
noexcept
1139 auto r = text_shaper::line_vector{};
1140 r.reserve(line_sizes.size());
1142 auto char_it = _text.begin();
1143 auto width_it = _line_break_widths.
begin();
1144 auto line_nr = 0_uz;
1145 for (
auto const line_size : line_sizes) {
1146 hi_axiom(line_size > 0);
1147 auto const char_eol = char_it + line_size;
1148 auto const width_eol = width_it + line_size;
1150 auto const line_width = detail::unicode_LB_width(width_it, width_eol);
1151 r.emplace_back(line_nr++, _text.begin(), char_it, char_eol, line_width, _initial_line_metrics);
1154 width_it = width_eol;
1157 if (r.empty() or is_Zp_or_Zl(r.back().last_category)) {
1158 r.emplace_back(line_nr++, _text.begin(), _text.end(), _text.end(), 0.0f, _initial_line_metrics);
1159 r.back().paragraph_direction = _text_direction;
1162 layout_lines_vertical_spacing(r, line_spacing, paragraph_spacing);
1163 layout_lines_vertical_alignment(
1175 void position_glyphs(aarectangle rectangle, extent2 sub_pixel_size)
noexcept
1177 hi_assert(not _lines.empty());
1180 bidi_algorithm(_lines, _text, _bidi_context);
1181 for (
auto& line : _lines) {
1189 void resolve_script() noexcept
1192 auto first_script = _script;
1193 for (
auto& c : _text) {
1194 auto const script = ucd_get_script(c.grapheme.starter());
1195 if (script != iso_15924::wildcard() or script == iso_15924::uncoded() or script == iso_15924::common() or
1196 script == iso_15924::inherited()) {
1197 first_script = script;
1205 auto word_script = iso_15924::common();
1206 auto previous_script = first_script;
1207 for (
auto i = std::ssize(_text) - 1; i >= 0; --i) {
1210 if (_word_break_opportunities[i + 1] != unicode_break_opportunity::no) {
1211 word_script = iso_15924::common();
1214 c.script = ucd_get_script(c.grapheme.starter());
1215 if (c.script == iso_15924::uncoded() or c.script == iso_15924::common()) {
1216 auto const bracket_type = ucd_get_bidi_paired_bracket_type(c.grapheme.starter());
1219 bracket_type == unicode_bidi_paired_bracket_type::o ? previous_script :
1220 bracket_type == unicode_bidi_paired_bracket_type::c ? iso_15924::common() :
1224 }
else if (c.script != iso_15924::inherited()) {
1225 previous_script = word_script = c.script;
1230 previous_script = first_script;
1231 for (
auto i = 0_uz; i != _text.size(); ++i) {
1234 if (c.script == iso_15924::common() or c.script == iso_15924::inherited()) {
1235 c.script = previous_script;
1238 previous_script = c.script;
1244 get_selection_from_break(text_cursor cursor, unicode_break_vector
const& break_opportunities)
const noexcept
1246 if (_text.empty()) {
1253 auto const first_index = [&]() {
1254 auto i = cursor.index();
1255 while (break_opportunities[i] == unicode_break_opportunity::no) {
1260 auto const last_index = [&]() {
1261 auto i = cursor.index();
1262 while (break_opportunities[i + 1] == unicode_break_opportunity::no) {
1268 return {get_before_cursor(first_index), get_after_cursor(last_index)};
1272 get_line_metrics(text_shaper::char_const_iterator first, text_shaper::char_const_iterator last)
const noexcept
1274 auto metrics = _initial_line_metrics;
1275 for (
auto it = first; it != last; ++it) {
1278 if (is_visible(it->general_category)) {
1279 inplace_max(metrics, it->font_metrics());
1283 auto const last_category = (first != last) ? (last - 1)->general_category : unicode_general_category::Cn;
1284 return {metrics, last_category};
1295 if (lines.empty()) {
1299 auto line_it = lines.cbegin();
1300 auto char_it_first = _text.begin();
1301 auto char_it_last = char_it_first + *line_it++;
1304 auto [previous_metrics, previous_category] = get_line_metrics(char_it_first, char_it_last);
1305 auto total_height = previous_metrics.x_height;
1307 for (; line_it != lines.cend(); ++line_it) {
1308 char_it_first = std::exchange(char_it_last, char_it_last + *line_it);
1311 auto [current_metrics, current_category] = get_line_metrics(char_it_first, char_it_last);
1312 auto const line_height = previous_metrics.descender +
std::max(previous_metrics.line_gap, current_metrics.line_gap) +
1313 current_metrics.ascender;
1315 auto const spacing = previous_category == unicode_general_category::Zp ? previous_metrics.paragraph_spacing :
1316 previous_metrics.line_spacing;
1317 total_height = total_height + spacing * line_height;
1319 previous_metrics =
std::move(current_metrics);
1320 previous_category =
std::move(current_category);
1323 return total_height.in(pixels);