[TextServer] Fix system font fallback and caret/selection behavior for composite characters.

This commit is contained in:
bruvzg 2023-08-15 11:42:40 +03:00
parent c495eb5102
commit 5d3fcc5766
No known key found for this signature in database
GPG Key ID: 7960FCF39844EC38
19 changed files with 398 additions and 54 deletions

View File

@ -197,7 +197,7 @@
<member name="caret_force_displayed" type="bool" setter="set_caret_force_displayed" getter="is_caret_force_displayed" default="false"> <member name="caret_force_displayed" type="bool" setter="set_caret_force_displayed" getter="is_caret_force_displayed" default="false">
If [code]true[/code], the [LineEdit] will always show the caret, even if focus is lost. If [code]true[/code], the [LineEdit] will always show the caret, even if focus is lost.
</member> </member>
<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="true"> <member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="false">
Allow moving caret, selecting and removing the individual composite character components. Allow moving caret, selecting and removing the individual composite character components.
[b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components. [b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components.
</member> </member>

View File

@ -1100,7 +1100,7 @@
<member name="caret_draw_when_editable_disabled" type="bool" setter="set_draw_caret_when_editable_disabled" getter="is_drawing_caret_when_editable_disabled" default="false"> <member name="caret_draw_when_editable_disabled" type="bool" setter="set_draw_caret_when_editable_disabled" getter="is_drawing_caret_when_editable_disabled" default="false">
If [code]true[/code], caret will be visible when [member editable] is disabled. If [code]true[/code], caret will be visible when [member editable] is disabled.
</member> </member>
<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="true"> <member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="false">
Allow moving caret, selecting and removing the individual composite character components. Allow moving caret, selecting and removing the individual composite character components.
[b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components. [b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components.
</member> </member>

View File

@ -1139,6 +1139,14 @@
Clears text buffer (removes text and inline objects). Clears text buffer (removes text and inline objects).
</description> </description>
</method> </method>
<method name="shaped_text_closest_character_pos" qualifiers="const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
Returns composite character position closest to the [param pos].
</description>
</method>
<method name="shaped_text_draw" qualifiers="const"> <method name="shaped_text_draw" qualifiers="const">
<return type="void" /> <return type="void" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1189,6 +1197,13 @@
Returns shapes of the carets corresponding to the character offset [param position] in the text. Returned caret shape is 1 pixel wide rectangle. Returns shapes of the carets corresponding to the character offset [param position] in the text. Returned caret shape is 1 pixel wide rectangle.
</description> </description>
</method> </method>
<method name="shaped_text_get_character_breaks" qualifiers="const">
<return type="PackedInt32Array" />
<param index="0" name="shaped" type="RID" />
<description>
Returns array of the composite character boundaries.
</description>
</method>
<method name="shaped_text_get_custom_punctuation" qualifiers="const"> <method name="shaped_text_get_custom_punctuation" qualifiers="const">
<return type="String" /> <return type="String" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1432,7 +1447,7 @@
Returns [code]true[/code] if buffer is successfully shaped. Returns [code]true[/code] if buffer is successfully shaped.
</description> </description>
</method> </method>
<method name="shaped_text_next_grapheme_pos" qualifiers="const"> <method name="shaped_text_next_character_pos" qualifiers="const">
<return type="int" /> <return type="int" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" /> <param index="1" name="pos" type="int" />
@ -1440,6 +1455,14 @@
Returns composite character end position closest to the [param pos]. Returns composite character end position closest to the [param pos].
</description> </description>
</method> </method>
<method name="shaped_text_next_grapheme_pos" qualifiers="const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
Returns grapheme end position closest to the [param pos].
</description>
</method>
<method name="shaped_text_overrun_trim_to_width"> <method name="shaped_text_overrun_trim_to_width">
<return type="void" /> <return type="void" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1449,7 +1472,7 @@
Trims text if it exceeds the given width. Trims text if it exceeds the given width.
</description> </description>
</method> </method>
<method name="shaped_text_prev_grapheme_pos" qualifiers="const"> <method name="shaped_text_prev_character_pos" qualifiers="const">
<return type="int" /> <return type="int" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" /> <param index="1" name="pos" type="int" />
@ -1457,6 +1480,14 @@
Returns composite character start position closest to the [param pos]. Returns composite character start position closest to the [param pos].
</description> </description>
</method> </method>
<method name="shaped_text_prev_grapheme_pos" qualifiers="const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
Returns grapheme start position closest to the [param pos].
</description>
</method>
<method name="shaped_text_resize_object"> <method name="shaped_text_resize_object">
<return type="bool" /> <return type="bool" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1568,6 +1599,18 @@
[b]Note:[/b] Always returns [code]false[/code] if the server does not support the [constant FEATURE_UNICODE_SECURITY] feature. [b]Note:[/b] Always returns [code]false[/code] if the server does not support the [constant FEATURE_UNICODE_SECURITY] feature.
</description> </description>
</method> </method>
<method name="string_get_character_breaks" qualifiers="const">
<return type="PackedInt32Array" />
<param index="0" name="string" type="String" />
<param index="1" name="language" type="String" default="&quot;&quot;" />
<description>
Returns array of the composite character boundaries.
[codeblock]
var ts = TextServerManager.get_primary_interface()
print(ts.string_get_word_breaks("Test ❤️‍🔥 Test")) # Prints [1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14]
[/codeblock]
</description>
</method>
<method name="string_get_word_breaks" qualifiers="const"> <method name="string_get_word_breaks" qualifiers="const">
<return type="PackedInt32Array" /> <return type="PackedInt32Array" />
<param index="0" name="string" type="String" /> <param index="0" name="string" type="String" />

View File

@ -981,6 +981,13 @@
<description> <description>
</description> </description>
</method> </method>
<method name="_shaped_text_closest_character_pos" qualifiers="virtual const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
</description>
</method>
<method name="_shaped_text_draw" qualifiers="virtual const"> <method name="_shaped_text_draw" qualifiers="virtual const">
<return type="void" /> <return type="void" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1026,6 +1033,12 @@
<description> <description>
</description> </description>
</method> </method>
<method name="_shaped_text_get_character_breaks" qualifiers="virtual const">
<return type="PackedInt32Array" />
<param index="0" name="shaped" type="RID" />
<description>
</description>
</method>
<method name="_shaped_text_get_custom_punctuation" qualifiers="virtual const"> <method name="_shaped_text_get_custom_punctuation" qualifiers="virtual const">
<return type="String" /> <return type="String" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1229,6 +1242,13 @@
<description> <description>
</description> </description>
</method> </method>
<method name="_shaped_text_next_character_pos" qualifiers="virtual const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
</description>
</method>
<method name="_shaped_text_next_grapheme_pos" qualifiers="virtual const"> <method name="_shaped_text_next_grapheme_pos" qualifiers="virtual const">
<return type="int" /> <return type="int" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1244,6 +1264,13 @@
<description> <description>
</description> </description>
</method> </method>
<method name="_shaped_text_prev_character_pos" qualifiers="virtual const">
<return type="int" />
<param index="0" name="shaped" type="RID" />
<param index="1" name="pos" type="int" />
<description>
</description>
</method>
<method name="_shaped_text_prev_grapheme_pos" qualifiers="virtual const"> <method name="_shaped_text_prev_grapheme_pos" qualifiers="virtual const">
<return type="int" /> <return type="int" />
<param index="0" name="shaped" type="RID" /> <param index="0" name="shaped" type="RID" />
@ -1356,6 +1383,13 @@
<description> <description>
</description> </description>
</method> </method>
<method name="_string_get_character_breaks" qualifiers="virtual const">
<return type="PackedInt32Array" />
<param index="0" name="string" type="String" />
<param index="1" name="language" type="String" />
<description>
</description>
</method>
<method name="_string_get_word_breaks" qualifiers="virtual const"> <method name="_string_get_word_breaks" qualifiers="virtual const">
<return type="PackedInt32Array" /> <return type="PackedInt32Array" />
<param index="0" name="string" type="String" /> <param index="0" name="string" type="String" />

View File

@ -3782,6 +3782,7 @@ void TextServerAdvanced::invalidate(TextServerAdvanced::ShapedTextDataAdvanced *
p_shaped->script_iter = nullptr; p_shaped->script_iter = nullptr;
} }
p_shaped->break_ops_valid = false; p_shaped->break_ops_valid = false;
p_shaped->chars_valid = false;
p_shaped->js_ops_valid = false; p_shaped->js_ops_valid = false;
} }
} }
@ -4835,6 +4836,76 @@ int64_t TextServerAdvanced::_shaped_text_get_ellipsis_glyph_count(const RID &p_s
return sd->overrun_trim_data.ellipsis_glyph_buf.size(); return sd->overrun_trim_data.ellipsis_glyph_buf.size();
} }
void TextServerAdvanced::_update_chars(ShapedTextDataAdvanced *p_sd) const {
if (!p_sd->chars_valid) {
p_sd->chars.clear();
const UChar *data = p_sd->utf16.get_data();
UErrorCode err = U_ZERO_ERROR;
int prev = -1;
int i = 0;
Vector<ShapedTextDataAdvanced::Span> &spans = p_sd->spans;
if (p_sd->parent != RID()) {
ShapedTextDataAdvanced *parent_sd = shaped_owner.get_or_null(p_sd->parent);
ERR_FAIL_COND(!parent_sd->valid);
spans = parent_sd->spans;
}
while (i < spans.size()) {
if (spans[i].start > p_sd->end) {
break;
}
if (spans[i].end < p_sd->start) {
i++;
continue;
}
int r_start = MAX(0, spans[i].start - p_sd->start);
String language = spans[i].language;
while (i + 1 < spans.size() && language == spans[i + 1].language) {
i++;
}
int r_end = MIN(spans[i].end - p_sd->start, p_sd->text.size());
UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, (language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale().ascii().get_data() : language.ascii().get_data(), data + _convert_pos_inv(p_sd, r_start), _convert_pos_inv(p_sd, r_end - r_start), &err);
if (U_SUCCESS(err)) {
while (ubrk_next(bi) != UBRK_DONE) {
int pos = _convert_pos(p_sd, ubrk_current(bi)) + r_start + p_sd->start;
if (prev != pos) {
p_sd->chars.push_back(pos);
}
prev = pos;
}
ubrk_close(bi);
} else {
for (int j = r_start; j <= r_end; j++) {
if (prev != j) {
p_sd->chars.push_back(j + p_sd->start);
}
prev = j;
}
}
i++;
}
p_sd->chars_valid = true;
}
}
PackedInt32Array TextServerAdvanced::_shaped_text_get_character_breaks(const RID &p_shaped) const {
ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped);
ERR_FAIL_COND_V(!sd, PackedInt32Array());
MutexLock lock(sd->mutex);
if (!sd->valid) {
const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped);
}
_update_chars(sd);
return sd->chars;
}
bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) {
ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped);
ERR_FAIL_COND_V(!sd, false); ERR_FAIL_COND_V(!sd, false);
@ -5338,7 +5409,17 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star
// Try system fallback. // Try system fallback.
RID fdef = p_fonts[0]; RID fdef = p_fonts[0];
if (_font_is_allow_system_fallback(fdef)) { if (_font_is_allow_system_fallback(fdef)) {
String text = p_sd->text.substr(p_start, 1); _update_chars(p_sd);
int64_t next = p_end;
for (const int32_t &E : p_sd->chars) {
if (E > p_start) {
next = E;
break;
}
}
String text = p_sd->text.substr(p_start, next - p_start);
String font_name = _font_get_name(fdef); String font_name = _font_get_name(fdef);
BitField<FontStyle> font_style = _font_get_style(fdef); BitField<FontStyle> font_style = _font_get_style(fdef);
int font_weight = _font_get_weight(fdef); int font_weight = _font_get_weight(fdef);
@ -6602,6 +6683,30 @@ PackedInt32Array TextServerAdvanced::_string_get_word_breaks(const String &p_str
return ret; return ret;
} }
PackedInt32Array TextServerAdvanced::_string_get_character_breaks(const String &p_string, const String &p_language) const {
const String lang = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language;
// Convert to UTF-16.
Char16String utf16 = p_string.utf16();
PackedInt32Array ret;
UErrorCode err = U_ZERO_ERROR;
UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, lang.ascii().get_data(), (const UChar *)utf16.get_data(), utf16.length(), &err);
if (U_SUCCESS(err)) {
while (ubrk_next(bi) != UBRK_DONE) {
int pos = _convert_pos(p_string, utf16, ubrk_current(bi));
ret.push_back(pos);
}
ubrk_close(bi);
} else {
for (int i = 0; i <= p_string.size(); i++) {
ret.push_back(i);
}
}
return ret;
}
bool TextServerAdvanced::_is_valid_identifier(const String &p_string) const { bool TextServerAdvanced::_is_valid_identifier(const String &p_string) const {
#ifndef ICU_STATIC_DATA #ifndef ICU_STATIC_DATA
if (!icu_data_loaded) { if (!icu_data_loaded) {

View File

@ -509,9 +509,11 @@ class TextServerAdvanced : public TextServerExtension {
HashMap<int, bool> jstops; HashMap<int, bool> jstops;
HashMap<int, bool> breaks; HashMap<int, bool> breaks;
PackedInt32Array chars;
int break_inserts = 0; int break_inserts = 0;
bool break_ops_valid = false; bool break_ops_valid = false;
bool js_ops_valid = false; bool js_ops_valid = false;
bool chars_valid = false;
~ShapedTextDataAdvanced() { ~ShapedTextDataAdvanced() {
for (int i = 0; i < bidi_iter.size(); i++) { for (int i = 0; i < bidi_iter.size(); i++) {
@ -609,6 +611,7 @@ class TextServerAdvanced : public TextServerExtension {
mutable HashMap<SystemFontKey, SystemFontCache, SystemFontKeyHasher> system_fonts; mutable HashMap<SystemFontKey, SystemFontCache, SystemFontKeyHasher> system_fonts;
mutable HashMap<String, PackedByteArray> system_font_data; mutable HashMap<String, PackedByteArray> system_font_data;
void _update_chars(ShapedTextDataAdvanced *p_sd) const;
void _realign(ShapedTextDataAdvanced *p_sd) const; void _realign(ShapedTextDataAdvanced *p_sd) const;
int64_t _convert_pos(const String &p_utf32, const Char16String &p_utf16, int64_t p_pos) const; int64_t _convert_pos(const String &p_utf32, const Char16String &p_utf16, int64_t p_pos) const;
int64_t _convert_pos(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const; int64_t _convert_pos(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const;
@ -920,11 +923,14 @@ public:
MODBIND1RC(double, shaped_text_get_underline_position, const RID &); MODBIND1RC(double, shaped_text_get_underline_position, const RID &);
MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &); MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &);
MODBIND1RC(PackedInt32Array, shaped_text_get_character_breaks, const RID &);
MODBIND2RC(String, format_number, const String &, const String &); MODBIND2RC(String, format_number, const String &, const String &);
MODBIND2RC(String, parse_number, const String &, const String &); MODBIND2RC(String, parse_number, const String &, const String &);
MODBIND1RC(String, percent_sign, const String &); MODBIND1RC(String, percent_sign, const String &);
MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t); MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t);
MODBIND2RC(PackedInt32Array, string_get_character_breaks, const String &, const String &);
MODBIND2RC(int64_t, is_confusable, const String &, const PackedStringArray &); MODBIND2RC(int64_t, is_confusable, const String &, const PackedStringArray &);
MODBIND1RC(bool, spoof_check, const String &); MODBIND1RC(bool, spoof_check, const String &);

View File

@ -4079,6 +4079,20 @@ double TextServerFallback::_shaped_text_get_underline_thickness(const RID &p_sha
return sd->uthk; return sd->uthk;
} }
PackedInt32Array TextServerFallback::_shaped_text_get_character_breaks(const RID &p_shaped) const {
const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped);
ERR_FAIL_COND_V(!sd, PackedInt32Array());
MutexLock lock(sd->mutex);
PackedInt32Array ret;
ret.resize(sd->end - sd->start);
for (int i = sd->start; i < sd->end; i++) {
ret.write[i] = i;
}
return ret;
}
String TextServerFallback::_string_to_upper(const String &p_string, const String &p_language) const { String TextServerFallback::_string_to_upper(const String &p_string, const String &p_language) const {
return p_string.to_upper(); return p_string.to_upper();
} }

View File

@ -788,6 +788,8 @@ public:
MODBIND1RC(double, shaped_text_get_underline_position, const RID &); MODBIND1RC(double, shaped_text_get_underline_position, const RID &);
MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &); MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &);
MODBIND1RC(PackedInt32Array, shaped_text_get_character_breaks, const RID &);
MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t); MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t);
MODBIND2RC(String, string_to_upper, const String &, const String &); MODBIND2RC(String, string_to_upper, const String &, const String &);

View File

@ -79,7 +79,7 @@ void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
if (caret_mid_grapheme_enabled) { if (caret_mid_grapheme_enabled) {
set_caret_column(get_caret_column() - 1); set_caret_column(get_caret_column() - 1);
} else { } else {
set_caret_column(TS->shaped_text_prev_grapheme_pos(text_rid, get_caret_column())); set_caret_column(TS->shaped_text_prev_character_pos(text_rid, get_caret_column()));
} }
} }
@ -112,7 +112,7 @@ void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
if (caret_mid_grapheme_enabled) { if (caret_mid_grapheme_enabled) {
set_caret_column(get_caret_column() + 1); set_caret_column(get_caret_column() + 1);
} else { } else {
set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, get_caret_column())); set_caret_column(TS->shaped_text_next_character_pos(text_rid, get_caret_column()));
} }
} }
@ -211,7 +211,7 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) {
delete_char(); delete_char();
} else { } else {
int cc = caret_column; int cc = caret_column;
set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, caret_column)); set_caret_column(TS->shaped_text_next_character_pos(text_rid, caret_column));
delete_text(cc, caret_column); delete_text(cc, caret_column);
} }
} }
@ -1326,6 +1326,9 @@ void LineEdit::set_caret_at_pixel_pos(int p_x) {
} }
int ofs = ceil(TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset)); int ofs = ceil(TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset));
if (!caret_mid_grapheme_enabled) {
ofs = TS->shaped_text_closest_character_pos(text_rid, ofs);
}
set_caret_column(ofs); set_caret_column(ofs);
} }

View File

@ -113,7 +113,7 @@ private:
PopupMenu *menu_dir = nullptr; PopupMenu *menu_dir = nullptr;
PopupMenu *menu_ctl = nullptr; PopupMenu *menu_ctl = nullptr;
bool caret_mid_grapheme_enabled = true; bool caret_mid_grapheme_enabled = false;
int caret_column = 0; int caret_column = 0;
float scroll_offset = 0.0; float scroll_offset = 0.0;

View File

@ -1585,6 +1585,7 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V
} }
} else { } else {
char_pos = TS->shaped_text_hit_test_position(rid, p_click.x - rect.position.x); char_pos = TS->shaped_text_hit_test_position(rid, p_click.x - rect.position.x);
char_pos = TS->shaped_text_closest_character_pos(rid, char_pos);
} }
} }
line_clicked = true; line_clicked = true;

View File

@ -2374,7 +2374,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
if (caret_mid_grapheme_enabled) { if (caret_mid_grapheme_enabled) {
set_caret_column(get_caret_column(i) - 1, i == 0, i); set_caret_column(get_caret_column(i) - 1, i == 0, i);
} else { } else {
set_caret_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); set_caret_column(TS->shaped_text_prev_character_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i);
} }
} }
} }
@ -2433,7 +2433,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
if (caret_mid_grapheme_enabled) { if (caret_mid_grapheme_enabled) {
set_caret_column(get_caret_column(i) + 1, i == 0, i); set_caret_column(get_caret_column(i) + 1, i == 0, i);
} else { } else {
set_caret_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); set_caret_column(TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i);
} }
} }
} }
@ -2815,7 +2815,7 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
if (caret_mid_grapheme_enabled) { if (caret_mid_grapheme_enabled) {
next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0;
} else { } else {
next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0;
} }
// Remove overlapping carets. // Remove overlapping carets.
@ -4331,6 +4331,9 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_
colx = TS->shaped_text_get_size(text_rid).x - colx; colx = TS->shaped_text_get_size(text_rid).x - colx;
} }
col = TS->shaped_text_hit_test_position(text_rid, colx); col = TS->shaped_text_hit_test_position(text_rid, colx);
if (!caret_mid_grapheme_enabled) {
col = TS->shaped_text_closest_character_pos(text_rid, col);
}
return Point2i(col, row); return Point2i(col, row);
} }
@ -7023,7 +7026,11 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con
if (is_layout_rtl()) { if (is_layout_rtl()) {
p_px = TS->shaped_text_get_size(text_rid).x - p_px; p_px = TS->shaped_text_get_size(text_rid).x - p_px;
} }
return TS->shaped_text_hit_test_position(text_rid, p_px); int ofs = TS->shaped_text_hit_test_position(text_rid, p_px);
if (!caret_mid_grapheme_enabled) {
ofs = TS->shaped_text_closest_character_pos(text_rid, ofs);
}
return ofs;
} }
/* Caret */ /* Caret */

View File

@ -423,7 +423,7 @@ private:
bool move_caret_on_right_click = true; bool move_caret_on_right_click = true;
bool caret_mid_grapheme_enabled = true; bool caret_mid_grapheme_enabled = false;
bool multi_carets_enabled = true; bool multi_carets_enabled = true;

View File

@ -303,6 +303,11 @@ void TextServerExtension::_bind_methods() {
GDVIRTUAL_BIND(_shaped_text_next_grapheme_pos, "shaped", "pos"); GDVIRTUAL_BIND(_shaped_text_next_grapheme_pos, "shaped", "pos");
GDVIRTUAL_BIND(_shaped_text_prev_grapheme_pos, "shaped", "pos"); GDVIRTUAL_BIND(_shaped_text_prev_grapheme_pos, "shaped", "pos");
GDVIRTUAL_BIND(_shaped_text_get_character_breaks, "shaped");
GDVIRTUAL_BIND(_shaped_text_next_character_pos, "shaped", "pos");
GDVIRTUAL_BIND(_shaped_text_prev_character_pos, "shaped", "pos");
GDVIRTUAL_BIND(_shaped_text_closest_character_pos, "shaped", "pos");
GDVIRTUAL_BIND(_format_number, "string", "language"); GDVIRTUAL_BIND(_format_number, "string", "language");
GDVIRTUAL_BIND(_parse_number, "string", "language"); GDVIRTUAL_BIND(_parse_number, "string", "language");
GDVIRTUAL_BIND(_percent_sign, "language"); GDVIRTUAL_BIND(_percent_sign, "language");
@ -311,6 +316,7 @@ void TextServerExtension::_bind_methods() {
GDVIRTUAL_BIND(_is_valid_identifier, "string"); GDVIRTUAL_BIND(_is_valid_identifier, "string");
GDVIRTUAL_BIND(_string_get_word_breaks, "string", "language", "chars_per_line"); GDVIRTUAL_BIND(_string_get_word_breaks, "string", "language", "chars_per_line");
GDVIRTUAL_BIND(_string_get_character_breaks, "string", "language");
GDVIRTUAL_BIND(_is_confusable, "string", "dict"); GDVIRTUAL_BIND(_is_confusable, "string", "dict");
GDVIRTUAL_BIND(_spoof_check, "string"); GDVIRTUAL_BIND(_spoof_check, "string");
@ -1333,6 +1339,38 @@ int64_t TextServerExtension::shaped_text_prev_grapheme_pos(const RID &p_shaped,
return TextServer::shaped_text_prev_grapheme_pos(p_shaped, p_pos); return TextServer::shaped_text_prev_grapheme_pos(p_shaped, p_pos);
} }
PackedInt32Array TextServerExtension::shaped_text_get_character_breaks(const RID &p_shaped) const {
PackedInt32Array ret;
if (GDVIRTUAL_CALL(_shaped_text_get_character_breaks, p_shaped, ret)) {
return ret;
}
return PackedInt32Array();
}
int64_t TextServerExtension::shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const {
int64_t ret;
if (GDVIRTUAL_CALL(_shaped_text_next_character_pos, p_shaped, p_pos, ret)) {
return ret;
}
return TextServer::shaped_text_next_character_pos(p_shaped, p_pos);
}
int64_t TextServerExtension::shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const {
int64_t ret;
if (GDVIRTUAL_CALL(_shaped_text_prev_character_pos, p_shaped, p_pos, ret)) {
return ret;
}
return TextServer::shaped_text_prev_character_pos(p_shaped, p_pos);
}
int64_t TextServerExtension::shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const {
int64_t ret;
if (GDVIRTUAL_CALL(_shaped_text_closest_character_pos, p_shaped, p_pos, ret)) {
return ret;
}
return TextServer::shaped_text_closest_character_pos(p_shaped, p_pos);
}
String TextServerExtension::format_number(const String &p_string, const String &p_language) const { String TextServerExtension::format_number(const String &p_string, const String &p_language) const {
String ret; String ret;
if (GDVIRTUAL_CALL(_format_number, p_string, p_language, ret)) { if (GDVIRTUAL_CALL(_format_number, p_string, p_language, ret)) {
@ -1399,6 +1437,14 @@ PackedInt32Array TextServerExtension::string_get_word_breaks(const String &p_str
return ret; return ret;
} }
PackedInt32Array TextServerExtension::string_get_character_breaks(const String &p_string, const String &p_language) const {
PackedInt32Array ret;
if (GDVIRTUAL_CALL(_string_get_character_breaks, p_string, p_language, ret)) {
return ret;
}
return TextServer::string_get_character_breaks(p_string, p_language);
}
int64_t TextServerExtension::is_confusable(const String &p_string, const PackedStringArray &p_dict) const { int64_t TextServerExtension::is_confusable(const String &p_string, const PackedStringArray &p_dict) const {
int64_t ret; int64_t ret;
if (GDVIRTUAL_CALL(_is_confusable, p_string, p_dict, ret)) { if (GDVIRTUAL_CALL(_is_confusable, p_string, p_dict, ret)) {

View File

@ -505,6 +505,15 @@ public:
GDVIRTUAL2RC(int64_t, _shaped_text_next_grapheme_pos, RID, int64_t); GDVIRTUAL2RC(int64_t, _shaped_text_next_grapheme_pos, RID, int64_t);
GDVIRTUAL2RC(int64_t, _shaped_text_prev_grapheme_pos, RID, int64_t); GDVIRTUAL2RC(int64_t, _shaped_text_prev_grapheme_pos, RID, int64_t);
virtual PackedInt32Array shaped_text_get_character_breaks(const RID &p_shaped) const override;
virtual int64_t shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const override;
virtual int64_t shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const override;
virtual int64_t shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const override;
GDVIRTUAL1RC(PackedInt32Array, _shaped_text_get_character_breaks, RID);
GDVIRTUAL2RC(int64_t, _shaped_text_next_character_pos, RID, int64_t);
GDVIRTUAL2RC(int64_t, _shaped_text_prev_character_pos, RID, int64_t);
GDVIRTUAL2RC(int64_t, _shaped_text_closest_character_pos, RID, int64_t);
virtual String format_number(const String &p_string, const String &p_language = "") const override; virtual String format_number(const String &p_string, const String &p_language = "") const override;
virtual String parse_number(const String &p_string, const String &p_language = "") const override; virtual String parse_number(const String &p_string, const String &p_language = "") const override;
virtual String percent_sign(const String &p_language = "") const override; virtual String percent_sign(const String &p_language = "") const override;
@ -518,6 +527,9 @@ public:
virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const override; virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const override;
GDVIRTUAL3RC(PackedInt32Array, _string_get_word_breaks, const String &, const String &, int64_t); GDVIRTUAL3RC(PackedInt32Array, _string_get_word_breaks, const String &, const String &, int64_t);
virtual PackedInt32Array string_get_character_breaks(const String &p_string, const String &p_language = "") const override;
GDVIRTUAL2RC(PackedInt32Array, _string_get_character_breaks, const String &, const String &);
virtual bool is_valid_identifier(const String &p_string) const override; virtual bool is_valid_identifier(const String &p_string) const override;
GDVIRTUAL1RC(bool, _is_valid_identifier, const String &); GDVIRTUAL1RC(bool, _is_valid_identifier, const String &);

View File

@ -448,6 +448,11 @@ void TextServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("shaped_text_next_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_next_grapheme_pos); ClassDB::bind_method(D_METHOD("shaped_text_next_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_next_grapheme_pos);
ClassDB::bind_method(D_METHOD("shaped_text_prev_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_prev_grapheme_pos); ClassDB::bind_method(D_METHOD("shaped_text_prev_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_prev_grapheme_pos);
ClassDB::bind_method(D_METHOD("shaped_text_get_character_breaks", "shaped"), &TextServer::shaped_text_get_character_breaks);
ClassDB::bind_method(D_METHOD("shaped_text_next_character_pos", "shaped", "pos"), &TextServer::shaped_text_next_character_pos);
ClassDB::bind_method(D_METHOD("shaped_text_prev_character_pos", "shaped", "pos"), &TextServer::shaped_text_prev_character_pos);
ClassDB::bind_method(D_METHOD("shaped_text_closest_character_pos", "shaped", "pos"), &TextServer::shaped_text_closest_character_pos);
ClassDB::bind_method(D_METHOD("shaped_text_draw", "shaped", "canvas", "pos", "clip_l", "clip_r", "color"), &TextServer::shaped_text_draw, DEFVAL(-1), DEFVAL(-1), DEFVAL(Color(1, 1, 1))); ClassDB::bind_method(D_METHOD("shaped_text_draw", "shaped", "canvas", "pos", "clip_l", "clip_r", "color"), &TextServer::shaped_text_draw, DEFVAL(-1), DEFVAL(-1), DEFVAL(Color(1, 1, 1)));
ClassDB::bind_method(D_METHOD("shaped_text_draw_outline", "shaped", "canvas", "pos", "clip_l", "clip_r", "outline_size", "color"), &TextServer::shaped_text_draw_outline, DEFVAL(-1), DEFVAL(-1), DEFVAL(1), DEFVAL(Color(1, 1, 1))); ClassDB::bind_method(D_METHOD("shaped_text_draw_outline", "shaped", "canvas", "pos", "clip_l", "clip_r", "outline_size", "color"), &TextServer::shaped_text_draw_outline, DEFVAL(-1), DEFVAL(-1), DEFVAL(1), DEFVAL(Color(1, 1, 1)));
@ -458,6 +463,7 @@ void TextServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("percent_sign", "language"), &TextServer::percent_sign, DEFVAL("")); ClassDB::bind_method(D_METHOD("percent_sign", "language"), &TextServer::percent_sign, DEFVAL(""));
ClassDB::bind_method(D_METHOD("string_get_word_breaks", "string", "language", "chars_per_line"), &TextServer::string_get_word_breaks, DEFVAL(""), DEFVAL(0)); ClassDB::bind_method(D_METHOD("string_get_word_breaks", "string", "language", "chars_per_line"), &TextServer::string_get_word_breaks, DEFVAL(""), DEFVAL(0));
ClassDB::bind_method(D_METHOD("string_get_character_breaks", "string", "language"), &TextServer::string_get_character_breaks, DEFVAL(""));
ClassDB::bind_method(D_METHOD("is_confusable", "string", "dict"), &TextServer::is_confusable); ClassDB::bind_method(D_METHOD("is_confusable", "string", "dict"), &TextServer::is_confusable);
ClassDB::bind_method(D_METHOD("spoof_check", "string"), &TextServer::spoof_check); ClassDB::bind_method(D_METHOD("spoof_check", "string"), &TextServer::spoof_check);
@ -1424,6 +1430,57 @@ int64_t TextServer::shaped_text_prev_grapheme_pos(const RID &p_shaped, int64_t p
return p_pos; return p_pos;
} }
int64_t TextServer::shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const {
const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped);
int64_t prev = 0;
for (const int32_t &E : chars) {
if (E >= p_pos) {
return prev;
}
prev = E;
}
return prev;
}
int64_t TextServer::shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const {
const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped);
int64_t prev = 0;
for (const int32_t &E : chars) {
if (E > p_pos) {
return E;
}
prev = E;
}
return prev;
}
int64_t TextServer::shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const {
const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped);
int64_t prev = 0;
for (const int32_t &E : chars) {
if (E == p_pos) {
return E;
} else if (E > p_pos) {
if ((E - p_pos) < (p_pos - prev)) {
return E;
} else {
return prev;
}
}
prev = E;
}
return prev;
}
PackedInt32Array TextServer::string_get_character_breaks(const String &p_string, const String &p_language) const {
PackedInt32Array ret;
ret.resize(p_string.size());
for (int i = 0; i <= p_string.size(); i++) {
ret.write[i] = i;
}
return ret;
}
void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l, double p_clip_r, const Color &p_color) const { void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l, double p_clip_r, const Color &p_color) const {
TextServer::Orientation orientation = shaped_text_get_orientation(p_shaped); TextServer::Orientation orientation = shaped_text_get_orientation(p_shaped);
bool hex_codes = shaped_text_get_preserve_control(p_shaped) || shaped_text_get_preserve_invalid(p_shaped); bool hex_codes = shaped_text_get_preserve_control(p_shaped) || shaped_text_get_preserve_invalid(p_shaped);

View File

@ -491,6 +491,11 @@ public:
virtual int64_t shaped_text_next_grapheme_pos(const RID &p_shaped, int64_t p_pos) const; virtual int64_t shaped_text_next_grapheme_pos(const RID &p_shaped, int64_t p_pos) const;
virtual int64_t shaped_text_prev_grapheme_pos(const RID &p_shaped, int64_t p_pos) const; virtual int64_t shaped_text_prev_grapheme_pos(const RID &p_shaped, int64_t p_pos) const;
virtual PackedInt32Array shaped_text_get_character_breaks(const RID &p_shaped) const = 0;
virtual int64_t shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const;
virtual int64_t shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const;
virtual int64_t shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const;
// The pen position is always placed on the baseline and moveing left to right. // The pen position is always placed on the baseline and moveing left to right.
virtual void shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, const Color &p_color = Color(1, 1, 1)) const; virtual void shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, const Color &p_color = Color(1, 1, 1)) const;
virtual void shaped_text_draw_outline(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, int64_t p_outline_size = 1, const Color &p_color = Color(1, 1, 1)) const; virtual void shaped_text_draw_outline(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, int64_t p_outline_size = 1, const Color &p_color = Color(1, 1, 1)) const;
@ -502,6 +507,7 @@ public:
// String functions. // String functions.
virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const = 0; virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const = 0;
virtual PackedInt32Array string_get_character_breaks(const String &p_string, const String &p_language = "") const;
virtual int64_t is_confusable(const String &p_string, const PackedStringArray &p_dict) const { return -1; }; virtual int64_t is_confusable(const String &p_string, const PackedStringArray &p_dict) const { return -1; };
virtual bool spoof_check(const String &p_string) const { return false; }; virtual bool spoof_check(const String &p_string) const { return false; };

View File

@ -2297,34 +2297,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args);
text_edit->set_caret_mid_grapheme_enabled(false);
CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled());
text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
text_edit->undo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "ffi some test text.ffi some test text.");
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
SEND_GUI_ACTION("ui_text_delete");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == " some test text. some test text.");
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 0);
CHECK_FALSE(text_edit->has_selection(0));
CHECK(text_edit->get_caret_line(1) == 0);
CHECK(text_edit->get_caret_column(1) == 16);
CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
} }
SUBCASE("[TextEdit] ui_text_caret_word_left") { SUBCASE("[TextEdit] ui_text_caret_word_left") {
@ -3335,18 +3307,6 @@ TEST_CASE("[SceneTree][TextEdit] caret") {
SEND_GUI_ACTION("ui_text_caret_left"); SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_caret_column() == 2); CHECK(text_edit->get_caret_column() == 2);
text_edit->set_caret_mid_grapheme_enabled(false);
CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled());
SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_caret_column() == 0);
SEND_GUI_ACTION("ui_text_caret_right");
CHECK(text_edit->get_caret_column() == 3);
SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_caret_column() == 0);
text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");

View File

@ -636,6 +636,54 @@ TEST_SUITE("[TextServer]") {
CHECK(breaks[17] == 42); CHECK(breaks[17] == 42);
} }
} }
if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) {
String text2 = U"U+2764 U+FE0F U+200D U+1F525 ; 13.1 # ❤️‍🔥";
PackedInt32Array breaks = ts->string_get_character_breaks(text2, "en");
CHECK(breaks.size() == 39);
if (breaks.size() == 39) {
CHECK(breaks[0] == 1);
CHECK(breaks[1] == 2);
CHECK(breaks[2] == 3);
CHECK(breaks[3] == 4);
CHECK(breaks[4] == 5);
CHECK(breaks[5] == 6);
CHECK(breaks[6] == 7);
CHECK(breaks[7] == 8);
CHECK(breaks[8] == 9);
CHECK(breaks[9] == 10);
CHECK(breaks[10] == 11);
CHECK(breaks[11] == 12);
CHECK(breaks[12] == 13);
CHECK(breaks[13] == 14);
CHECK(breaks[14] == 15);
CHECK(breaks[15] == 16);
CHECK(breaks[16] == 17);
CHECK(breaks[17] == 18);
CHECK(breaks[18] == 19);
CHECK(breaks[19] == 20);
CHECK(breaks[20] == 21);
CHECK(breaks[21] == 22);
CHECK(breaks[22] == 23);
CHECK(breaks[23] == 24);
CHECK(breaks[24] == 25);
CHECK(breaks[25] == 26);
CHECK(breaks[26] == 27);
CHECK(breaks[27] == 28);
CHECK(breaks[28] == 29);
CHECK(breaks[29] == 30);
CHECK(breaks[30] == 31);
CHECK(breaks[31] == 32);
CHECK(breaks[32] == 33);
CHECK(breaks[33] == 34);
CHECK(breaks[34] == 35);
CHECK(breaks[35] == 36);
CHECK(breaks[36] == 37);
CHECK(breaks[37] == 38);
CHECK(breaks[38] == 42);
}
}
} }
} }
} }