From dad4aae386c927fb6dbae3e7ed45fb11082c9180 Mon Sep 17 00:00:00 2001 From: bruvzg <7645683+bruvzg@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:41:12 +0200 Subject: [PATCH] [TextEdit] Add support for optional wrapped line indentation. --- doc/classes/EditorSettings.xml | 3 ++ doc/classes/TextEdit.xml | 3 ++ doc/classes/TextServer.xml | 3 ++ editor/code_editor.cpp | 1 + editor/editor_settings.cpp | 1 + scene/gui/text_edit.cpp | 69 ++++++++++++++++++++++++++++++---- scene/gui/text_edit.h | 7 ++++ servers/text_server.cpp | 44 +++++++++++++++++++++- servers/text_server.h | 1 + 9 files changed, 122 insertions(+), 10 deletions(-) diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 0e4dcacc56c4..758768929135 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -992,6 +992,9 @@ If [code]true[/code], automatically indents code when pressing the [kbd]Enter[/kbd] key based on blocks above the new line. + + If [code]true[/code], all wrapped lines are indented to the same amount as the unwrapped line. + When using tab indentation, determines the length of each tab. When using space indentation, determines how many spaces are inserted when pressing [kbd]Tab[/kbd] and when automatic indentation is performed. diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 001cf06db466..a29849c12303 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -1154,6 +1154,9 @@ If [code]true[/code], the line containing the cursor is highlighted. + + If [code]true[/code], all wrapped lines are indented to the same amount as the unwrapped line. + Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead. diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index d4b341b7000f..9a17fb3def57 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -1837,6 +1837,9 @@ Remove edge spaces from the broken line segments. + + Subtract first line indentation width from all lines after the first one. + Trims text before the shaping. e.g, increasing [member Label.visible_characters] or [member RichTextLabel.visible_characters] value is visually identical to typing the text. diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index d3872349e9b6..56287d4a03d4 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -1039,6 +1039,7 @@ void CodeTextEditor::update_editor_settings() { set_indent_using_spaces(EDITOR_GET("text_editor/behavior/indent/type")); text_editor->set_indent_size(EDITOR_GET("text_editor/behavior/indent/size")); text_editor->set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent")); + text_editor->set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines")); // Completion text_editor->set_auto_brace_completion_enabled(EDITOR_GET("text_editor/completion/auto_brace_complete")); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 853a4cd410e9..e6e760bcd359 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -631,6 +631,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "text_editor/behavior/indent/type", 0, "Tabs,Spaces") EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "text_editor/behavior/indent/size", 4, "1,64,1") // size of 0 crashes. _initial_set("text_editor/behavior/indent/auto_indent", true); + _initial_set("text_editor/behavior/indent/indent_wrapped_lines", true); // Behavior: Files _initial_set("text_editor/behavior/files/trim_trailing_whitespace_on_save", false); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 79a5f2b55724..ca4724890189 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -74,6 +74,18 @@ int TextEdit::Text::get_tab_size() const { return tab_size; } +void TextEdit::Text::set_indent_wrapped_lines(bool p_enabled) { + if (indent_wrapped_lines == p_enabled) { + return; + } + indent_wrapped_lines = p_enabled; + tab_size_dirty = true; +} + +bool TextEdit::Text::is_indent_wrapped_lines() const { + return indent_wrapped_lines; +} + void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, const String &p_language) { if (direction == p_direction && language == p_language) { return; @@ -185,9 +197,14 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan text.write[p_line].data_buf->clear(); } + BitField flags = brk_flags; + if (indent_wrapped_lines) { + flags.set_flag(TextServer::BREAK_TRIM_INDENT); + } + text.write[p_line].data_buf->set_width(width); text.write[p_line].data_buf->set_direction((TextServer::Direction)direction); - text.write[p_line].data_buf->set_break_flags(brk_flags); + text.write[p_line].data_buf->set_break_flags(flags); text.write[p_line].data_buf->set_preserve_control(draw_control_chars); if (p_ime_text.length() > 0) { if (p_text_changed) { @@ -251,8 +268,12 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan void TextEdit::Text::invalidate_all_lines() { for (int i = 0; i < text.size(); i++) { + BitField flags = brk_flags; + if (indent_wrapped_lines) { + flags.set_flag(TextServer::BREAK_TRIM_INDENT); + } text.write[i].data_buf->set_width(width); - text.write[i].data_buf->set_break_flags(brk_flags); + text.write[i].data_buf->set_break_flags(flags); if (tab_size_dirty) { if (tab_size > 0) { Vector tabs; @@ -1075,9 +1096,12 @@ void TextEdit::_notification(int p_what) { // Draw line. RID rid = ldata->get_line_rid(line_wrap_index); float text_height = TS->shaped_text_get_size(rid).y; + float wrap_indent = (text.is_indent_wrapped_lines() && line_wrap_index > 0) ? get_indent_level(line) * theme_cache.font->get_char_size(' ', theme_cache.font_size).width : 0.0; if (rtl) { - char_margin = size.width - char_margin - TS->shaped_text_get_size(rid).x; + char_margin = size.width - char_margin - (TS->shaped_text_get_size(rid).x + wrap_indent); + } else { + char_margin += wrap_indent; } // Draw selections. @@ -2935,7 +2959,11 @@ void TextEdit::_update_placeholder() { // Placeholder is generally smaller then text documents, and updates less so this should be fast enough for now. placeholder_data_buf->clear(); placeholder_data_buf->set_width(text.get_width()); - placeholder_data_buf->set_break_flags(text.get_brk_flags()); + BitField flags = text.get_brk_flags(); + if (text.is_indent_wrapped_lines()) { + flags.set_flag(TextServer::BREAK_TRIM_INDENT); + } + placeholder_data_buf->set_break_flags(flags); if (text_direction == Control::TEXT_DIRECTION_INHERITED) { placeholder_data_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); } else { @@ -3331,6 +3359,20 @@ int TextEdit::get_tab_size() const { return text.get_tab_size(); } +void TextEdit::set_indent_wrapped_lines(bool p_enabled) { + if (text.is_indent_wrapped_lines() == p_enabled) { + return; + } + text.set_indent_wrapped_lines(p_enabled); + text.invalidate_all_lines(); + _update_placeholder(); + queue_redraw(); +} + +bool TextEdit::is_indent_wrapped_lines() const { + return text.is_indent_wrapped_lines(); +} + // User controls void TextEdit::set_overtype_mode_enabled(const bool p_enabled) { if (overtype_mode == p_enabled) { @@ -4336,8 +4378,11 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ } RID text_rid = text.get_line_data(row)->get_line_rid(wrap_index); + float wrap_indent = (text.is_indent_wrapped_lines() && wrap_index > 0) ? get_indent_level(row) * theme_cache.font->get_char_size(' ', theme_cache.font_size).width : 0.0; if (is_layout_rtl()) { - colx = TS->shaped_text_get_size(text_rid).x - colx; + colx = TS->shaped_text_get_size(text_rid).x - colx + wrap_indent; + } else { + colx -= wrap_indent; } col = TS->shaped_text_hit_test_position(text_rid, colx); if (!caret_mid_grapheme_enabled) { @@ -6073,6 +6118,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tab_size", "size"), &TextEdit::set_tab_size); ClassDB::bind_method(D_METHOD("get_tab_size"), &TextEdit::get_tab_size); + ClassDB::bind_method(D_METHOD("set_indent_wrapped_lines", "enabled"), &TextEdit::set_indent_wrapped_lines); + ClassDB::bind_method(D_METHOD("is_indent_wrapped_lines"), &TextEdit::is_indent_wrapped_lines); + // User controls ClassDB::bind_method(D_METHOD("set_overtype_mode_enabled", "enabled"), &TextEdit::set_overtype_mode_enabled); ClassDB::bind_method(D_METHOD("is_overtype_mode_enabled"), &TextEdit::is_overtype_mode_enabled); @@ -6451,6 +6499,7 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "middle_mouse_paste_enabled"), "set_middle_mouse_paste_enabled", "is_middle_mouse_paste_enabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "wrap_mode", PROPERTY_HINT_ENUM, "None,Boundary"), "set_line_wrapping_mode", "get_line_wrapping_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Arbitrary:1,Word:2,Word (Smart):3"), "set_autowrap_mode", "get_autowrap_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_wrapped_lines"), "set_indent_wrapped_lines", "is_indent_wrapped_lines"); ADD_GROUP("Scroll", "scroll_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_smooth"), "set_smooth_scroll_enabled", "is_smooth_scroll_enabled"); @@ -7075,8 +7124,11 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con p_wrap_index = MIN(p_wrap_index, text.get_line_data(p_line)->get_line_count() - 1); RID text_rid = text.get_line_data(p_line)->get_line_rid(p_wrap_index); + float wrap_indent = (text.is_indent_wrapped_lines() && p_wrap_index > 0) ? get_indent_level(p_line) * theme_cache.font->get_char_size(' ', theme_cache.font_size).width : 0.0; 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 + wrap_indent; + } else { + p_px -= wrap_indent; } int ofs = TS->shaped_text_hit_test_position(text_rid, p_px); if (!caret_mid_grapheme_enabled) { @@ -7125,11 +7177,12 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column } RID text_rid = text.get_line_data(p_line)->get_line_rid(row); + float wrap_indent = (text.is_indent_wrapped_lines() && row > 0) ? get_indent_level(p_line) * theme_cache.font->get_char_size(' ', theme_cache.font_size).width : 0.0; CaretInfo ts_caret = TS->shaped_text_get_carets(text_rid, p_column); if ((ts_caret.l_caret != Rect2() && (ts_caret.l_dir == TextServer::DIRECTION_AUTO || ts_caret.l_dir == (TextServer::Direction)input_direction)) || (ts_caret.t_caret == Rect2())) { - return ts_caret.l_caret.position.x; + return ts_caret.l_caret.position.x + (is_layout_rtl() ? -wrap_indent : wrap_indent); } else { - return ts_caret.t_caret.position.x; + return ts_caret.t_caret.position.x + (is_layout_rtl() ? -wrap_indent : wrap_indent); } } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index d49be860a9c7..b8e30c7900d9 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -181,6 +181,7 @@ class TextEdit : public Control { int tab_size = 4; int gutter_count = 0; + bool indent_wrapped_lines = false; void _calculate_line_height(); void _calculate_max_line_width(); @@ -188,6 +189,9 @@ class TextEdit : public Control { public: void set_tab_size(int p_tab_size); int get_tab_size() const; + void set_indent_wrapped_lines(bool p_enabled); + bool is_indent_wrapped_lines() const; + void set_font(const Ref &p_font); void set_font_size(int p_font_size); void set_direction_and_language(TextServer::Direction p_direction, const String &p_language); @@ -720,6 +724,9 @@ class TextEdit : public Control { void set_tab_size(const int p_size); int get_tab_size() const; + void set_indent_wrapped_lines(bool p_enabled); + bool is_indent_wrapped_lines() const; + // User controls void set_overtype_mode_enabled(const bool p_enabled); bool is_overtype_mode_enabled() const; diff --git a/servers/text_server.cpp b/servers/text_server.cpp index b67a698615f4..d1dadbc8399a 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -535,6 +535,7 @@ void TextServer::_bind_methods() { BIND_BITFIELD_FLAG(BREAK_GRAPHEME_BOUND); BIND_BITFIELD_FLAG(BREAK_ADAPTIVE); BIND_BITFIELD_FLAG(BREAK_TRIM_EDGE_SPACES); + BIND_BITFIELD_FLAG(BREAK_TRIM_INDENT); /* VisibleCharactersBehavior */ BIND_ENUM_CONSTANT(VC_CHARS_BEFORE_SHAPING); @@ -750,13 +751,28 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks_adv(const RID &p_shaped int l_size = shaped_text_get_glyph_count(p_shaped); const Glyph *l_gl = const_cast(this)->shaped_text_sort_logical(p_shaped); + double indent = 0.0; + if (p_break_flags.has_flag(BREAK_TRIM_INDENT)) { + for (int i = 0; i < l_size; i++) { + if ((l_gl[i].flags & GRAPHEME_IS_TAB) == GRAPHEME_IS_TAB || (l_gl[i].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE) { + indent += l_gl[i].advance * l_gl[i].repeat; + } else { + break; + } + } + } + for (int i = 0; i < l_size; i++) { + double l_width = p_width[chunk]; + if (l_width > indent) { + l_width -= indent; + } if (l_gl[i].start < p_start) { prev_safe_break = i + 1; continue; } if (l_gl[i].count > 0) { - if ((p_width[chunk] > 0) && (width + l_gl[i].advance > p_width[chunk]) && (last_safe_break >= 0)) { + if ((l_width > 0) && (width + l_gl[i].advance > l_width) && (last_safe_break >= 0)) { if (p_break_flags.has_flag(BREAK_TRIM_EDGE_SPACES)) { int start_pos = prev_safe_break; int end_pos = last_safe_break; @@ -891,13 +907,25 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks(const RID &p_shaped, do int l_size = shaped_text_get_glyph_count(p_shaped); const Glyph *l_gl = const_cast(this)->shaped_text_sort_logical(p_shaped); + double indent = 0.0; + if (p_break_flags.has_flag(BREAK_TRIM_INDENT)) { + for (int i = 0; i < l_size; i++) { + if ((l_gl[i].flags & GRAPHEME_IS_TAB) == GRAPHEME_IS_TAB || (l_gl[i].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE) { + indent += l_gl[i].advance * l_gl[i].repeat; + } else { + break; + } + } + } + + double l_width = p_width; for (int i = 0; i < l_size; i++) { if (l_gl[i].start < p_start) { prev_safe_break = i + 1; continue; } if (l_gl[i].count > 0) { - if ((p_width > 0) && (width + l_gl[i].advance * l_gl[i].repeat > p_width) && (last_safe_break >= 0)) { + if ((l_width > 0) && (width + l_gl[i].advance * l_gl[i].repeat > l_width) && (last_safe_break >= 0)) { if (p_break_flags.has_flag(BREAK_TRIM_EDGE_SPACES)) { int start_pos = prev_safe_break; int end_pos = last_safe_break; @@ -910,6 +938,9 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks(const RID &p_shaped, do if (last_end <= l_gl[start_pos].start) { lines.push_back(l_gl[start_pos].start); lines.push_back(l_gl[end_pos].end); + if (p_width > indent) { + l_width = p_width - indent; + } last_end = l_gl[end_pos].end; } trim_next = true; @@ -917,6 +948,9 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks(const RID &p_shaped, do if (last_end <= line_start) { lines.push_back(line_start); lines.push_back(l_gl[last_safe_break].end); + if (p_width > indent) { + l_width = p_width - indent; + } last_end = l_gl[last_safe_break].end; } } @@ -943,12 +977,18 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks(const RID &p_shaped, do if (last_end <= l_gl[start_pos].start) { lines.push_back(l_gl[start_pos].start); lines.push_back(l_gl[end_pos].end); + if (p_width > indent) { + l_width = p_width - indent; + } last_end = l_gl[end_pos].end; } } else { if (last_end <= line_start) { lines.push_back(line_start); lines.push_back(l_gl[i].end); + if (p_width > indent) { + l_width = p_width - indent; + } last_end = l_gl[i].end; } } diff --git a/servers/text_server.h b/servers/text_server.h index c2cc44464663..dfd6140fde10 100644 --- a/servers/text_server.h +++ b/servers/text_server.h @@ -108,6 +108,7 @@ class TextServer : public RefCounted { BREAK_GRAPHEME_BOUND = 1 << 2, BREAK_ADAPTIVE = 1 << 3, BREAK_TRIM_EDGE_SPACES = 1 << 4, + BREAK_TRIM_INDENT = 1 << 5, }; enum OverrunBehavior {