Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TextEdit] Add support for optional wrapped line indentation. #88546

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/classes/EditorSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,9 @@
<member name="text_editor/behavior/indent/auto_indent" type="bool" setter="" getter="">
If [code]true[/code], automatically indents code when pressing the [kbd]Enter[/kbd] key based on blocks above the new line.
</member>
<member name="text_editor/behavior/indent/indent_wrapped_lines" type="bool" setter="" getter="">
If [code]true[/code], all wrapped lines are indented to the same amount as the unwrapped line.
</member>
<member name="text_editor/behavior/indent/size" type="int" setter="" getter="">
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.
</member>
Expand Down
3 changes: 3 additions & 0 deletions doc/classes/TextEdit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,9 @@
<member name="highlight_current_line" type="bool" setter="set_highlight_current_line" getter="is_highlight_current_line_enabled" default="false">
If [code]true[/code], the line containing the cursor is highlighted.
</member>
<member name="indent_wrapped_lines" type="bool" setter="set_indent_wrapped_lines" getter="is_indent_wrapped_lines" default="false">
If [code]true[/code], all wrapped lines are indented to the same amount as the unwrapped line.
</member>
<member name="language" type="String" setter="set_language" getter="get_language" default="&quot;&quot;">
Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
</member>
Expand Down
3 changes: 3 additions & 0 deletions doc/classes/TextServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,9 @@
<constant name="BREAK_TRIM_EDGE_SPACES" value="16" enum="LineBreakFlag" is_bitfield="true">
Remove edge spaces from the broken line segments.
</constant>
<constant name="BREAK_TRIM_INDENT" value="32" enum="LineBreakFlag" is_bitfield="true">
Subtract first line indentation width from all lines after the first one.
</constant>
<constant name="VC_CHARS_BEFORE_SHAPING" value="0" enum="VisibleCharactersBehavior">
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.
</constant>
Expand Down
1 change: 1 addition & 0 deletions editor/code_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
1 change: 1 addition & 0 deletions editor/editor_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> 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);
Expand Down
69 changes: 61 additions & 8 deletions scene/gui/text_edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TextServer::LineBreakFlag> 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) {
Expand Down Expand Up @@ -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<TextServer::LineBreakFlag> 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<float> tabs;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<TextServer::LineBreakFlag> 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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}

Expand Down
7 changes: 7 additions & 0 deletions scene/gui/text_edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,17 @@ 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();

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<Font> &p_font);
void set_font_size(int p_font_size);
void set_direction_and_language(TextServer::Direction p_direction, const String &p_language);
Expand Down Expand Up @@ -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;
Expand Down
44 changes: 42 additions & 2 deletions servers/text_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<TextServer *>(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;
Expand Down Expand Up @@ -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<TextServer *>(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;
Expand All @@ -910,13 +938,19 @@ 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;
} else {
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;
}
}
Expand All @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions servers/text_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading