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;
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;
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),
92 _initial_line_metrics = (style->size * dpi_scale) *
font.
metrics;
94 _text.reserve(text.size());
95 for (hilet& c : text) {
96 hilet clean_c = c ==
'\n' ?
grapheme{unicode_PS} : c;
98 auto& tmp = _text.emplace_back(clean_c, style, dpi_scale);
99 tmp.initialize_glyph(font);
105 [](text_shaper::char_const_reference it) {
106 return it.grapheme.starter();
110 _line_break_opportunities =
unicode_line_break(_text.begin(), _text.end(), [](hilet& c) ->
decltype(
auto) {
111 return c.grapheme.starter();
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);
119 _word_break_opportunities =
unicode_word_break(_text.begin(), _text.end(), [](hilet& c) ->
decltype(
auto) {
120 return c.grapheme.starter();
123 _sentence_break_opportunities =
unicode_sentence_break(_text.begin(), _text.end(), [](hilet& c) ->
decltype(
auto) {
124 return c.grapheme.starter();
130 [[nodiscard]] text_shaper(
131 std::string_view text,
132 text_style
const& style,
134 hi::alignment alignment,
136 iso_15924 script = iso_15924{
"Zyyy"})
noexcept :
137 text_shaper(
to_gstring(text), style, dpi_scale, alignment, left_to_right, script)
141 [[nodiscard]]
bool empty() const noexcept
143 return _text.empty();
146 [[nodiscard]]
size_t size() const noexcept
151 [[nodiscard]] char_iterator
begin() noexcept
153 return _text.begin();
156 [[nodiscard]] char_const_iterator
begin() const noexcept
158 return _text.begin();
161 [[nodiscard]] char_const_iterator cbegin() const noexcept
163 return _text.cbegin();
166 [[nodiscard]] char_iterator
end() noexcept
171 [[nodiscard]] char_const_iterator
end() const noexcept
176 [[nodiscard]] char_const_iterator cend() const noexcept
181 auto const& lines() const noexcept
204 [[nodiscard]] aarectangle
205 bounding_rectangle(
float maximum_line_width,
float line_spacing = 1.0f,
float paragraph_spacing = 1.5f) noexcept
209 constexpr auto baseline = 0.0f;
213 hi_assert(not lines.empty());
215 auto max_width = 0.0f;
216 for (
auto& line : lines) {
217 inplace_max(max_width, line.width);
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);
241 float line_spacing = 1.0f,
242 float paragraph_spacing = 1.5f) noexcept
246 hi_assert(not _lines.empty());
261 return _text_direction;
271 return resolve(_alignment, _text_direction == unicode_bidi_class::L);
281 [[nodiscard]] char_const_iterator
get_it(
size_t index)
const noexcept
283 if (
static_cast<ptrdiff_t
>(index) < 0) {
285 }
else if (index >= size()) {
289 return begin() + index;
301 return get_it(cursor.index());
312 [[nodiscard]] char_const_iterator
get_it(
size_t column_nr,
size_t line_nr)
const noexcept
314 hi_assert(not _lines.empty());
316 if (
static_cast<ptrdiff_t
>(line_nr) < 0) {
318 }
else if (line_nr >= _lines.size()) {
322 hilet left_of_line =
static_cast<ptrdiff_t
>(column_nr) < 0;
323 hilet right_of_line = column_nr >= _lines[line_nr].size();
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;
330 if (
static_cast<ptrdiff_t
>(--line_nr) < 0) {
334 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].back() :
335 _lines[line_nr].front();
340 if (++line_nr >= _lines.size()) {
344 return _lines[line_nr].paragraph_direction == unicode_bidi_class::L ? _lines[line_nr].front() :
345 _lines[line_nr].back();
350 return _lines[line_nr][column_nr];
362 return get_it(column_row.first, column_row.second);
373 return {it->column_nr, it->line_nr};
375 hi_assert(not _lines.empty());
376 return {_lines.size() - 1, _lines.back().size()};
405 [[nodiscard]]
size_t get_index(text_shaper::char_const_iterator it)
const noexcept
476 if (it->direction == unicode_bidi_class::L) {
494 if (it->direction == unicode_bidi_class::L) {
511 hilet it =
get_it(cursor);
513 return (it->direction == unicode_bidi_class::L) == cursor.before();
515 hi_assert(begin() == end());
527 hilet it =
get_it(cursor);
529 return (it->direction == unicode_bidi_class::L) == cursor.after();
531 hi_assert(begin() == end());
547 hilet line_it = std::ranges::min_element(_lines, std::ranges::less{}, [position](hilet& line) {
548 return std::abs(line.y - position.y());
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};
563 hilet index = cursor.index();
571 return get_selection_from_break(cursor, _word_break_opportunities);
578 return get_selection_from_break(cursor, _sentence_break_opportunities);
585 hilet first_index = [&]() {
586 auto i = cursor.index();
588 if (_text[i - 1].general_category == unicode_general_category::Zp) {
595 hilet last_index = [&]() {
596 auto i = cursor.index();
597 while (i < _text.size()) {
598 if (_text[i].general_category == unicode_general_category::Zp) {
625 [[nodiscard]] char_const_iterator
move_left_char(char_const_iterator it)
const noexcept
628 return get_it(column_nr - 1, line_nr);
636 [[nodiscard]] char_const_iterator
move_right_char(char_const_iterator it)
const noexcept
639 return get_it(column_nr + 1, line_nr);
644 auto it = get_it(cursor);
645 if (overwrite_mode) {
646 it = move_left_char(it);
647 return get_before_cursor(it);
650 if (is_on_left(cursor)) {
652 it = move_left_char(it);
655 return get_left_cursor(it);
659 [[nodiscard]] text_cursor move_right_char(text_cursor cursor,
bool overwrite_mode)
const noexcept
661 auto it = get_it(cursor);
662 if (overwrite_mode) {
663 it = move_right_char(it);
664 return get_before_cursor(it);
667 if (is_on_right(cursor)) {
669 it = move_right_char(it);
672 return get_right_cursor(it);
676 [[nodiscard]] text_cursor move_down_char(text_cursor cursor,
float& x)
const noexcept
682 auto [column_nr, line_nr] = get_column_line(cursor);
683 if (++line_nr == _lines.size()) {
684 return get_end_cursor();
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();
693 hilet[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
694 return get_before_cursor(new_char_it);
697 [[nodiscard]] text_cursor move_up_char(text_cursor cursor,
float& x)
const noexcept
703 auto [column_nr, line_nr] = get_column_line(cursor);
704 if (line_nr-- == 0) {
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();
714 hilet[new_char_it, after] = _lines[line_nr].get_nearest(point2{x, 0.0f});
715 return get_before_cursor(new_char_it);
718 [[nodiscard]] text_cursor move_left_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
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);
727 it = move_left_char(it);
729 return get_end_cursor();
732 [[nodiscard]] text_cursor move_right_word(text_cursor cursor,
bool overwrite_mode)
const noexcept
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);
741 it = move_right_char(it);
743 return get_end_cursor();
746 [[nodiscard]] text_cursor move_begin_line(text_cursor cursor)
const noexcept
748 hilet[column_nr, line_nr] = get_column_line(cursor);
749 hilet& line = _lines[line_nr];
750 return get_before_cursor(line.first);
753 [[nodiscard]] text_cursor move_end_line(text_cursor cursor)
const noexcept
755 hilet[column_nr, line_nr] = get_column_line(cursor);
756 hilet& line = _lines[line_nr];
759 while (it != line.first) {
761 if (not it->is_trailing_white_space) {
766 return get_after_cursor(it);
769 [[nodiscard]] text_cursor move_begin_sentence(text_cursor cursor)
const noexcept
771 if (cursor.after()) {
772 cursor = {cursor.index(),
false};
773 }
else if (cursor.index() != 0) {
774 cursor = {cursor.index() - 1,
false};
776 hilet[first, last] = select_sentence(cursor);
777 return first.before_neighbor(size());
780 [[nodiscard]] text_cursor move_end_sentence(text_cursor cursor)
const noexcept
782 if (cursor.before()) {
783 cursor = {cursor.index(),
true};
784 }
else if (cursor.index() != _text.size() - 1) {
785 cursor = {cursor.index() + 1,
true};
787 hilet[first, last] = select_sentence(cursor);
788 return last.before_neighbor(size());
791 [[nodiscard]] text_cursor move_begin_paragraph(text_cursor cursor)
const noexcept
793 if (cursor.after()) {
794 cursor = {cursor.index(),
false};
795 }
else if (cursor.index() != 0) {
796 cursor = {cursor.index() - 1,
false};
798 hilet[first, last] = select_paragraph(cursor);
799 return first.before_neighbor(size());
802 [[nodiscard]] text_cursor move_end_paragraph(text_cursor cursor)
const noexcept
804 if (cursor.before()) {
805 cursor = {cursor.index(),
true};
806 }
else if (cursor.index() != _text.size() - 1) {
807 cursor = {cursor.index() + 1,
true};
809 hilet[first, last] = select_paragraph(cursor);
810 return last.before_neighbor(size());
813 [[nodiscard]] text_cursor move_begin_document(text_cursor cursor)
const noexcept
818 [[nodiscard]] text_cursor move_end_document(text_cursor cursor)
const noexcept
824 return get_end_cursor();
840 hi::alignment _alignment;
844 unicode_break_vector _line_break_opportunities;
848 std::vector<float> _line_break_widths;
852 unicode_break_vector _word_break_opportunities;
856 unicode_break_vector _sentence_break_opportunities;
860 unicode_bidi_context _bidi_context;
878 font_metrics _initial_line_metrics;
882 aarectangle _rectangle;
885 layout_lines_vertical_spacing(text_shaper::line_vector& lines,
float line_spacing,
float paragraph_spacing)
noexcept
887 hi_assert(not lines.empty());
889 auto prev = lines.begin();
891 for (
auto it = prev + 1; it != lines.end(); ++it) {
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;
896 it->y =
prev->y - spacing * height;
901 static void layout_lines_vertical_alignment(
902 text_shaper::line_vector& lines,
903 vertical_alignment alignment,
907 float sub_pixel_height)
noexcept
909 hi_assert(not lines.empty());
912 auto adjustment = [&]() {
913 if (alignment == vertical_alignment::top) {
914 return -lines.front().y;
916 }
else if (alignment == vertical_alignment::bottom) {
917 return -lines.back().y;
920 hilet mp_index = lines.size() / 2;
921 if (lines.size() % 2 == 1) {
922 return -lines[mp_index].y;
925 return -std::midpoint(lines[mp_index - 1].y, lines[mp_index].y);
931 adjustment += baseline;
935 if (lines.back().y + adjustment < min_y) {
936 adjustment = min_y - lines.back().y;
938 if (lines.front().y + adjustment > max_y) {
939 adjustment = max_y - lines.front().y;
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;
956 bidi_algorithm(text_shaper::line_vector& lines, text_shaper::char_vector& text, unicode_bidi_context bidi_context)
noexcept
958 hi_assert(not lines.empty());
961 auto char_its = std::vector<text_shaper::char_iterator>{};
963 char_its.reserve(text.size() + lines.size());
964 for (hilet& line : lines) {
966 for (
auto it = line.first; it != line.last; ++it) {
967 char_its.push_back(it);
969 if (not is_Zp_or_Zl(line.last_category)) {
971 char_its.push_back(text.end());
975 hilet[char_its_last, paragraph_directions] =
unicode_bidi(
978 [&](text_shaper::char_const_iterator it) {
979 if (it != text.end()) {
980 return it->grapheme.starter();
985 [&](text_shaper::char_iterator it,
char32_t code_point) {
986 hi_axiom(it != text.end());
987 it->replace_glyph(code_point);
990 if (it != text.end()) {
991 it->direction = direction;
997 char_its.erase(char_its_last, char_its.cend());
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) {
1008 hi_assert(par_it <= paragraph_directions.cend());
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()) {
1018 }
else if (char_it >= line_it->last) {
1020 hi_axiom(line_it->columns.size() <= narrow_cast<size_t>(
std::distance(line_it->first, line_it->last)));
1022 line_it->columns.clear();
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);
1031 char_it->line_nr = line_it->line_nr;
1032 char_it->column_nr = column_nr++;
1036 for (
auto& c : text) {
1041 [[nodiscard]]
static generator<std::pair<std::vector<size_t>,
float>>
1042 get_widths(unicode_break_vector
const& opportunities, std::vector<float>
const& widths,
float dpi_scale)
noexcept
1051 auto stack = std::vector<entry_type>{};
1053 hilet a4_one_column = 172.0f * 2.83465f * dpi_scale;
1054 hilet a4_two_column = 88.0f * 2.83465f * dpi_scale;
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};
1061 if (max_width >= a4_two_column) {
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()) {
1070 auto [width, lines] = detail::unicode_LB_width(opportunities, widths, a4_two_column);
1071 if (std::exchange(height, lines.size()) > lines.size()) {
1077 auto [min_width, min_lines] = detail::unicode_LB_minimum_width(opportunities, widths);
1078 if (min_lines.size() >= height) {
1083 stack.emplace_back(min_lines.size(), height, min_width, max_width);
1084 co_yield {
std::move(min_lines), min_width};
1087 hilet entry = stack.back();
1090 if (entry.max_height > entry.max_height + 1 and entry.max_width >= entry.min_width + 2.0f) {
1092 hilet half_width = (entry.min_width + entry.max_width) * 0.5f;
1094 auto [split_width, split_lines] = detail::unicode_LB_width(opportunities, widths, half_width);
1095 hilet split_height = split_lines.size();
1097 if (split_height == entry.min_height) {
1100 stack.emplace_back(split_height, entry.max_height, half_width, entry.max_width);
1102 }
else if (split_height == entry.max_height) {
1105 stack.emplace_back(entry.min_height, split_height, entry.min_width, half_width);
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);
1114 }
while (not stack.empty());
1126 [[nodiscard]] line_vector make_lines(
1127 aarectangle rectangle,
1129 extent2 sub_pixel_size,
1131 float paragraph_spacing)
noexcept
1135 auto r = text_shaper::line_vector{};
1136 r.reserve(line_sizes.size());
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;
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);
1150 width_it = width_eol;
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;
1158 layout_lines_vertical_spacing(r, line_spacing, paragraph_spacing);
1159 layout_lines_vertical_alignment(
1171 void position_glyphs(aarectangle rectangle, extent2 sub_pixel_size)
noexcept
1173 hi_assert(not _lines.empty());
1176 bidi_algorithm(_lines, _text, _bidi_context);
1177 for (
auto& line : _lines) {
1185 void resolve_script() noexcept
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;
1201 auto word_script = iso_15924::common();
1202 auto previous_script = first_script;
1203 for (
auto i = std::ssize(_text) - 1; i >= 0; --i) {
1206 if (_word_break_opportunities[i + 1] != unicode_break_opportunity::no) {
1207 word_script = iso_15924::common();
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());
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() :
1220 }
else if (c.script != iso_15924::inherited()) {
1221 previous_script = word_script = c.script;
1226 previous_script = first_script;
1227 for (
auto i = 0_uz; i != _text.size(); ++i) {
1230 if (c.script == iso_15924::common() or c.script == iso_15924::inherited()) {
1231 c.script = previous_script;
1234 previous_script = c.script;
1239 [[nodiscard]] std::pair<text_cursor, text_cursor>
1240 get_selection_from_break(text_cursor cursor, unicode_break_vector
const& break_opportunities)
const noexcept
1242 if (_text.empty()) {
1249 hilet first_index = [&]() {
1250 auto i = cursor.index();
1251 while (break_opportunities[i] == unicode_break_opportunity::no) {
1256 hilet last_index = [&]() {
1257 auto i = cursor.index();
1258 while (break_opportunities[i + 1] == unicode_break_opportunity::no) {
1264 return {get_before_cursor(first_index), get_after_cursor(last_index)};
1267 [[nodiscard]] std::pair<font_metrics, unicode_general_category>
1268 get_line_metrics(text_shaper::char_const_iterator first, text_shaper::char_const_iterator last)
const noexcept
1270 auto metrics = _initial_line_metrics;
1271 for (
auto it = first; it != last; ++it) {
1274 if (is_visible(it->general_category)) {
1275 inplace_max(metrics, it->font_metrics());
1279 hilet last_category = (first != last) ? (last - 1)->general_category : unicode_general_category::Cn;
1280 return {metrics, last_category};
1289 [[nodiscard]]
float get_text_height(std::vector<size_t>
const& lines)
const noexcept
1291 if (lines.empty()) {
1295 auto line_it = lines.cbegin();
1296 auto char_it_first = _text.begin();
1297 auto char_it_last = char_it_first + *line_it++;
1300 auto [previous_metrics, previous_category] = get_line_metrics(char_it_first, char_it_last);
1301 auto total_height = previous_metrics.x_height;
1303 for (; line_it != lines.cend(); ++line_it) {
1304 char_it_first = std::exchange(char_it_last, char_it_last + *line_it);
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;
1311 hilet spacing = previous_category == unicode_general_category::Zp ? previous_metrics.paragraph_spacing :
1312 previous_metrics.line_spacing;
1313 total_height += spacing * line_height;
1315 previous_metrics =
std::move(current_metrics);
1316 previous_category =
std::move(current_category);
1319 return total_height;