소스 검색

Expose and cleanup TextEdit line wrap API

Paulb23 4 년 전
부모
커밋
7e70f9e0b9

+ 46 - 2
doc/classes/TextEdit.xml

@@ -213,6 +213,35 @@
 			<description>
 			</description>
 		</method>
+		<method name="get_line_wrap_count" qualifiers="const">
+			<return type="int">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<description>
+				Returns the number of the time given line is wrapped.
+			</description>
+		</method>
+		<method name="get_line_wrap_index_at_column" qualifiers="const">
+			<return type="int">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<argument index="1" name="column" type="int">
+			</argument>
+			<description>
+				Returns the wrap index of the given line column.
+			</description>
+		</method>
+		<method name="get_line_wrapped_text" qualifiers="const">
+			<return type="PackedStringArray">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<description>
+				Returns an array of [String] repersenting each wrapped index.
+			</description>
+		</method>
 		<method name="get_menu" qualifiers="const">
 			<return type="PopupMenu" />
 			<description>
@@ -346,6 +375,15 @@
 				Returns whether the menu is visible. Use this instead of [code]get_menu().visible[/code] to improve performance (so the creation of the menu is avoided).
 			</description>
 		</method>
+		<method name="is_line_wrapped" qualifiers="const">
+			<return type="bool">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<description>
+				Returns if the given line is wrapped.
+			</description>
+		</method>
 		<method name="is_selection_active" qualifiers="const">
 			<return type="bool" />
 			<description>
@@ -686,8 +724,8 @@
 		<member name="virtual_keyboard_enabled" type="bool" setter="set_virtual_keyboard_enabled" getter="is_virtual_keyboard_enabled" default="true">
 			If [code]true[/code], the native virtual keyboard is shown when focused on platforms that support it.
 		</member>
-		<member name="wrap_enabled" type="bool" setter="set_wrap_enabled" getter="is_wrap_enabled" default="false">
-			If [code]true[/code], enables text wrapping when it goes beyond the edge of what is visible.
+		<member name="wrap_mode" type="int" setter="set_line_wrapping_mode" getter="get_line_wrapping_mode" enum="TextEdit.LineWrappingMode" default="0">
+			Sets the line wrapping mode to use.
 		</member>
 	</members>
 	<signals>
@@ -748,6 +786,12 @@
 		</constant>
 		<constant name="SELECTION_MODE_LINE" value="4" enum="SelectionMode">
 		</constant>
+		<constant name="LINE_WRAPPING_NONE" value="0" enum="LineWrappingMode">
+			Line wrapping is disabled.
+		</constant>
+		<constant name="LINE_WRAPPING_BOUNDARY" value="1" enum="LineWrappingMode">
+			Line wrapping occurs at the control boundary, beyond what would normally be visible.
+		</constant>
 		<constant name="GUTTER_TYPE_STRING" value="0" enum="GutterType">
 		</constant>
 		<constant name="GUTTER_TYPE_ICON" value="1" enum="GutterType">

+ 1 - 1
editor/code_editor.cpp

@@ -950,7 +950,7 @@ void CodeTextEditor::update_editor_settings() {
 	text_editor->set_draw_bookmarks_gutter(EditorSettings::get_singleton()->get("text_editor/appearance/show_bookmark_gutter"));
 	text_editor->set_line_folding_enabled(EditorSettings::get_singleton()->get("text_editor/appearance/code_folding"));
 	text_editor->set_draw_fold_gutter(EditorSettings::get_singleton()->get("text_editor/appearance/code_folding"));
-	text_editor->set_wrap_enabled(EditorSettings::get_singleton()->get("text_editor/appearance/word_wrap"));
+	text_editor->set_line_wrapping_mode((TextEdit::LineWrappingMode)EditorSettings::get_singleton()->get("text_editor/appearance/word_wrap").operator int());
 	text_editor->set_scroll_pass_end_of_file(EditorSettings::get_singleton()->get("text_editor/cursor/scroll_past_end_of_file"));
 	text_editor->set_caret_type((TextEdit::CaretType)EditorSettings::get_singleton()->get("text_editor/cursor/type").operator int());
 	text_editor->set_caret_blink_enabled(EditorSettings::get_singleton()->get("text_editor/cursor/caret_blink"));

+ 2 - 2
editor/editor_properties.cpp

@@ -121,7 +121,7 @@ void EditorPropertyMultilineText::_open_big_text() {
 	if (!big_text_dialog) {
 		big_text = memnew(TextEdit);
 		big_text->connect("text_changed", callable_mp(this, &EditorPropertyMultilineText::_big_text_changed));
-		big_text->set_wrap_enabled(true);
+		big_text->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
 		big_text_dialog = memnew(AcceptDialog);
 		big_text_dialog->add_child(big_text);
 		big_text_dialog->set_title(TTR("Edit Text:"));
@@ -166,7 +166,7 @@ EditorPropertyMultilineText::EditorPropertyMultilineText() {
 	set_bottom_editor(hb);
 	text = memnew(TextEdit);
 	text->connect("text_changed", callable_mp(this, &EditorPropertyMultilineText::_text_changed));
-	text->set_wrap_enabled(true);
+	text->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
 	add_focusable(text);
 	hb->add_child(text);
 	text->set_h_size_flags(SIZE_EXPAND_FILL);

+ 3 - 1
editor/editor_settings.cpp

@@ -525,7 +525,9 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 	_initial_set("text_editor/appearance/show_bookmark_gutter", true);
 	_initial_set("text_editor/appearance/show_info_gutter", true);
 	_initial_set("text_editor/appearance/code_folding", true);
-	_initial_set("text_editor/appearance/word_wrap", false);
+	_initial_set("text_editor/appearance/word_wrap", 0);
+	hints["text_editor/appearance/word_wrap"] = PropertyInfo(Variant::INT, "text_editor/appearance/word_wrap", PROPERTY_HINT_ENUM, "None,Boundary");
+
 	_initial_set("text_editor/appearance/show_line_length_guidelines", true);
 	_initial_set("text_editor/appearance/line_length_guideline_soft_column", 80);
 	hints["text_editor/appearance/line_length_guideline_soft_column"] = PropertyInfo(Variant::INT, "text_editor/appearance/line_length_guideline_soft_column", PROPERTY_HINT_RANGE, "20, 160, 1");

+ 1 - 1
editor/plugin_config_dialog.cpp

@@ -282,7 +282,7 @@ PluginConfigDialog::PluginConfigDialog() {
 
 	desc_edit = memnew(TextEdit);
 	desc_edit->set_custom_minimum_size(Size2(400, 80) * EDSCALE);
-	desc_edit->set_wrap_enabled(true);
+	desc_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
 	grid->add_child(desc_edit);
 
 	// Author

+ 1 - 1
editor/plugins/version_control_editor_plugin.cpp

@@ -484,7 +484,7 @@ VersionControlEditorPlugin::VersionControlEditorPlugin() {
 	commit_message->set_h_grow_direction(Control::GrowDirection::GROW_DIRECTION_BEGIN);
 	commit_message->set_v_grow_direction(Control::GrowDirection::GROW_DIRECTION_END);
 	commit_message->set_custom_minimum_size(Size2(200, 100));
-	commit_message->set_wrap_enabled(true);
+	commit_message->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
 	commit_message->connect("text_changed", callable_mp(this, &VersionControlEditorPlugin::_update_commit_button));
 	commit_message->connect("gui_input", callable_mp(this, &VersionControlEditorPlugin::_commit_message_gui_input));
 	commit_box_vbc->add_child(commit_message);

+ 4 - 4
scene/gui/code_edit.cpp

@@ -295,8 +295,8 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 
 			if (mb->get_button_index() == MOUSE_BUTTON_LEFT) {
 				if (is_line_folded(line)) {
-					int wrap_index = get_line_wrap_index_at_col(line, col);
-					if (wrap_index == times_line_wraps(line)) {
+					int wrap_index = get_line_wrap_index_at_column(line, col);
+					if (wrap_index == get_line_wrap_count(line)) {
 						int eol_icon_width = cache.folded_eol_icon->get_width();
 						int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll();
 						if (mpos.x > left_margin && mpos.x <= left_margin + eol_icon_width + 3) {
@@ -531,8 +531,8 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const {
 	_get_mouse_pos(p_pos, line, col);
 
 	if (is_line_folded(line)) {
-		int wrap_index = get_line_wrap_index_at_col(line, col);
-		if (wrap_index == times_line_wraps(line)) {
+		int wrap_index = get_line_wrap_index_at_column(line, col);
+		if (wrap_index == get_line_wrap_count(line)) {
 			int eol_icon_width = cache.folded_eol_icon->get_width();
 			int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll();
 			if (p_pos.x > left_margin && p_pos.x <= left_margin + eol_icon_width + 3) {

+ 161 - 145
scene/gui/text_edit.cpp

@@ -295,7 +295,7 @@ void TextEdit::_update_scrollbars() {
 		v_scroll->hide();
 	}
 
-	if (total_width > visible_width && !is_wrap_enabled()) {
+	if (total_width > visible_width && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) {
 		h_scroll->show();
 		h_scroll->set_max(total_width);
 		h_scroll->set_page(visible_width);
@@ -495,11 +495,11 @@ void TextEdit::_notification(int p_what) {
 			if (text_changed_dirty) {
 				MessageQueue::get_singleton()->push_call(this, "_text_changed_emit");
 			}
-			_update_wrap_at(true);
+			_update_wrap_at_column(true);
 		} break;
 		case NOTIFICATION_RESIZED: {
 			_update_scrollbars();
-			_update_wrap_at();
+			_update_wrap_at_column();
 		} break;
 		case NOTIFICATION_VISIBILITY_CHANGED: {
 			if (is_visible()) {
@@ -511,7 +511,7 @@ void TextEdit::_notification(int p_what) {
 		case NOTIFICATION_TRANSLATION_CHANGED:
 		case NOTIFICATION_THEME_CHANGED: {
 			_update_caches();
-			_update_wrap_at(true);
+			_update_wrap_at_column(true);
 		} break;
 		case NOTIFICATION_WM_WINDOW_FOCUS_IN: {
 			window_has_focus = true;
@@ -748,7 +748,7 @@ void TextEdit::_notification(int p_what) {
 
 			int first_visible_line = get_first_visible_line() - 1;
 			int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0);
-			draw_amount += times_line_wraps(first_visible_line + 1);
+			draw_amount += get_line_wrap_count(first_visible_line + 1);
 
 			// minimap
 			if (draw_minimap) {
@@ -769,7 +769,7 @@ void TextEdit::_notification(int p_what) {
 					minimap_line -= num_lines_from_rows(first_visible_line, 0, -num_lines_before, wi);
 					minimap_line -= (minimap_line > 0 && smooth_scroll_enabled ? 1 : 0);
 				}
-				int minimap_draw_amount = minimap_visible_lines + times_line_wraps(minimap_line + 1);
+				int minimap_draw_amount = minimap_visible_lines + get_line_wrap_count(minimap_line + 1);
 
 				// draw the minimap
 				Color viewport_color = (cache.background_color.get_v() < 0.5) ? Color(1, 1, 1, 0.1) : Color(0, 0, 0, 0.1);
@@ -805,8 +805,8 @@ void TextEdit::_notification(int p_what) {
 						current_color = cache.font_readonly_color;
 					}
 
-					Vector<String> wrap_rows = get_wrap_rows_text(minimap_line);
-					int line_wrap_amount = times_line_wraps(minimap_line);
+					Vector<String> wrap_rows = get_line_wrapped_text(minimap_line);
+					int line_wrap_amount = get_line_wrap_count(minimap_line);
 					int last_wrap_column = 0;
 
 					for (int line_wrap_index = 0; line_wrap_index < line_wrap_amount + 1; line_wrap_index++) {
@@ -819,7 +819,7 @@ void TextEdit::_notification(int p_what) {
 
 						const String &str = wrap_rows[line_wrap_index];
 						int indent_px = line_wrap_index != 0 ? get_indent_level(minimap_line) : 0;
-						if (indent_px >= wrap_at) {
+						if (indent_px >= wrap_at_column) {
 							indent_px = 0;
 						}
 						indent_px = minimap_char_size.x * indent_px;
@@ -947,8 +947,8 @@ void TextEdit::_notification(int p_what) {
 
 				const Ref<TextParagraph> ldata = text.get_line_data(line);
 
-				Vector<String> wrap_rows = get_wrap_rows_text(line);
-				int line_wrap_amount = times_line_wraps(line);
+				Vector<String> wrap_rows = get_line_wrapped_text(line);
+				int line_wrap_amount = get_line_wrap_count(line);
 
 				for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) {
 					if (line_wrap_index != 0) {
@@ -1659,8 +1659,8 @@ void TextEdit::_move_caret_up(bool p_select) {
 		set_caret_column(0);
 	} else {
 		int new_line = caret.line - num_lines_from(caret.line - 1, -1);
-		if (line_wraps(new_line)) {
-			set_caret_line(new_line, true, false, times_line_wraps(new_line));
+		if (is_line_wrapped(new_line)) {
+			set_caret_line(new_line, true, false, get_line_wrap_count(new_line));
 		} else {
 			set_caret_line(new_line, true, false);
 		}
@@ -1679,7 +1679,7 @@ void TextEdit::_move_caret_down(bool p_select) {
 	}
 
 	int cur_wrap_index = get_caret_wrap_index();
-	if (cur_wrap_index < times_line_wraps(caret.line)) {
+	if (cur_wrap_index < get_line_wrap_count(caret.line)) {
 		set_caret_line(caret.line, true, false, cur_wrap_index + 1);
 	} else if (caret.line == get_last_unhidden_line()) {
 		set_caret_column(text[caret.line].length());
@@ -1701,7 +1701,7 @@ void TextEdit::_move_caret_to_line_start(bool p_select) {
 	}
 
 	// Move caret column to start of wrapped row and then to start of text.
-	Vector<String> rows = get_wrap_rows_text(caret.line);
+	Vector<String> rows = get_line_wrapped_text(caret.line);
 	int wi = get_caret_wrap_index();
 	int row_start_col = 0;
 	for (int i = 0; i < wi; i++) {
@@ -1740,7 +1740,7 @@ void TextEdit::_move_caret_to_line_end(bool p_select) {
 	}
 
 	// Move caret column to end of wrapped row and then to end of text.
-	Vector<String> rows = get_wrap_rows_text(caret.line);
+	Vector<String> rows = get_line_wrapped_text(caret.line);
 	int wi = get_caret_wrap_index();
 	int row_end_col = -1;
 	for (int i = 0; i < wi + 1; i++) {
@@ -1929,7 +1929,7 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co
 	int row = first_vis_line + Math::floor(rows);
 	int wrap_index = 0;
 
-	if (is_wrap_enabled() || is_hiding_enabled()) {
+	if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || is_hiding_enabled()) {
 		int f_ofs = num_lines_from_rows(first_vis_line, caret.wrap_ofs, rows + (1 * SGN(rows)), wrap_index) - 1;
 		if (rows < 0) {
 			row = first_vis_line - f_ofs;
@@ -1951,9 +1951,9 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co
 		int colx = p_mouse.x - (cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding);
 		colx += caret.x_ofs;
 		col = get_char_pos_for_line(colx, row, wrap_index);
-		if (is_wrap_enabled() && wrap_index < times_line_wraps(row)) {
+		if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && wrap_index < get_line_wrap_count(row)) {
 			// Move back one if we are at the end of the row.
-			Vector<String> rows2 = get_wrap_rows_text(row);
+			Vector<String> rows2 = get_line_wrapped_text(row);
 			int row_end_col = 0;
 			for (int i = 0; i < wrap_index + 1; i++) {
 				row_end_col += rows2[i].length();
@@ -1985,7 +1985,7 @@ void TextEdit::_get_minimap_mouse_row(const Point2i &p_mouse, int &r_row) const
 	int visible_rows = get_visible_rows() + 1;
 	int first_visible_line = get_first_visible_line() - 1;
 	int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0);
-	draw_amount += times_line_wraps(first_visible_line + 1);
+	draw_amount += get_line_wrap_count(first_visible_line + 1);
 	int minimap_line_height = (minimap_char_size.y + minimap_line_spacing);
 
 	// calculate viewport size and y offset
@@ -2007,7 +2007,7 @@ void TextEdit::_get_minimap_mouse_row(const Point2i &p_mouse, int &r_row) const
 	int row = minimap_line + Math::floor(rows);
 	int wrap_index = 0;
 
-	if (is_wrap_enabled() || is_hiding_enabled()) {
+	if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || is_hiding_enabled()) {
 		int f_ofs = num_lines_from_rows(minimap_line, caret.wrap_ofs, rows + (1 * SGN(rows)), wrap_index) - 1;
 		if (rows < 0) {
 			row = minimap_line - f_ofs;
@@ -2894,7 +2894,7 @@ int TextEdit::_get_minimap_visible_rows() const {
 int TextEdit::get_total_visible_rows() const {
 	// Returns the total amount of rows we need in the editor.
 	// This skips hidden lines and counts each wrapping of a line.
-	if (!is_hiding_enabled() && !is_wrap_enabled()) {
+	if (!is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) {
 		return text.size();
 	}
 
@@ -2902,35 +2902,12 @@ int TextEdit::get_total_visible_rows() const {
 	for (int i = 0; i < text.size(); i++) {
 		if (!text.is_hidden(i)) {
 			total_rows++;
-			total_rows += times_line_wraps(i);
+			total_rows += get_line_wrap_count(i);
 		}
 	}
 	return total_rows;
 }
 
-void TextEdit::_update_wrap_at(bool p_force) {
-	int new_wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding;
-	if (draw_minimap) {
-		new_wrap_at -= minimap_width;
-	}
-	if (v_scroll->is_visible_in_tree()) {
-		new_wrap_at -= v_scroll->get_combined_minimum_size().width;
-	}
-	new_wrap_at -= wrap_right_offset; // Give it a little more space.
-
-	if ((wrap_at != new_wrap_at) || p_force) {
-		wrap_at = new_wrap_at;
-		if (wrap_enabled) {
-			text.set_width(wrap_at);
-		} else {
-			text.set_width(-1);
-		}
-		text.invalidate_all_lines();
-	}
-
-	_update_caret_wrap_offset();
-}
-
 void TextEdit::adjust_viewport_to_caret() {
 	// Make sure Caret is visible on the screen.
 	scrolling = false;
@@ -2958,7 +2935,7 @@ void TextEdit::adjust_viewport_to_caret() {
 	}
 	visible_width -= 20; // Give it a little more space.
 
-	if (!is_wrap_enabled()) {
+	if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) {
 		// Adjust x offset.
 		Vector2i caret_pos;
 
@@ -3007,7 +2984,7 @@ void TextEdit::center_viewport_to_caret() {
 	}
 	visible_width -= 20; // Give it a little more space.
 
-	if (is_wrap_enabled()) {
+	if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE) {
 		// Center x offset.
 
 		Vector2i caret_pos;
@@ -3045,74 +3022,6 @@ void TextEdit::center_viewport_to_caret() {
 	update();
 }
 
-void TextEdit::_update_caret_wrap_offset() {
-	int first_vis_line = get_first_visible_line();
-	if (line_wraps(first_vis_line)) {
-		caret.wrap_ofs = MIN(caret.wrap_ofs, times_line_wraps(first_vis_line));
-	} else {
-		caret.wrap_ofs = 0;
-	}
-	set_line_as_first_visible(caret.line_ofs, caret.wrap_ofs);
-}
-
-bool TextEdit::line_wraps(int line) const {
-	ERR_FAIL_INDEX_V(line, text.size(), 0);
-	if (!is_wrap_enabled()) {
-		return false;
-	}
-	return text.get_line_wrap_amount(line) > 0;
-}
-
-int TextEdit::times_line_wraps(int line) const {
-	ERR_FAIL_INDEX_V(line, text.size(), 0);
-
-	if (!line_wraps(line)) {
-		return 0;
-	}
-
-	return text.get_line_wrap_amount(line);
-}
-
-Vector<String> TextEdit::get_wrap_rows_text(int p_line) const {
-	ERR_FAIL_INDEX_V(p_line, text.size(), Vector<String>());
-
-	Vector<String> lines;
-	if (!line_wraps(p_line)) {
-		lines.push_back(text[p_line]);
-		return lines;
-	}
-
-	const String &line_text = text[p_line];
-	Vector<Vector2i> line_ranges = text.get_line_wrap_ranges(p_line);
-	for (int i = 0; i < line_ranges.size(); i++) {
-		lines.push_back(line_text.substr(line_ranges[i].x, line_ranges[i].y - line_ranges[i].x));
-	}
-
-	return lines;
-}
-
-int TextEdit::get_line_wrap_index_at_col(int p_line, int p_column) const {
-	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
-
-	if (!line_wraps(p_line)) {
-		return 0;
-	}
-
-	// Loop through wraps in the line text until we get to the column.
-	int wrap_index = 0;
-	int col = 0;
-	Vector<String> rows = get_wrap_rows_text(p_line);
-	for (int i = 0; i < rows.size(); i++) {
-		wrap_index = i;
-		String s = rows[wrap_index];
-		col += s.length();
-		if (col > p_column) {
-			break;
-		}
-	}
-	return wrap_index;
-}
-
 TextEdit::SelectionMode TextEdit::get_selection_mode() const {
 	return selection.selecting_mode;
 }
@@ -3158,14 +3067,14 @@ void TextEdit::_scroll_moved(double p_to_val) {
 		for (n_line = 0; n_line < text.size(); n_line++) {
 			if (!is_line_hidden(n_line)) {
 				sc++;
-				sc += times_line_wraps(n_line);
+				sc += get_line_wrap_count(n_line);
 				if (sc > v_scroll_i) {
 					break;
 				}
 			}
 		}
 		n_line = MIN(n_line, text.size() - 1);
-		int line_wrap_amount = times_line_wraps(n_line);
+		int line_wrap_amount = get_line_wrap_count(n_line);
 		int wi = line_wrap_amount - (sc - v_scroll_i - 1);
 		wi = CLAMP(wi, 0, line_wrap_amount);
 
@@ -3460,17 +3369,6 @@ bool TextEdit::is_readonly() const {
 	return readonly;
 }
 
-void TextEdit::set_wrap_enabled(bool p_wrap_enabled) {
-	if (wrap_enabled != p_wrap_enabled) {
-		wrap_enabled = p_wrap_enabled;
-		_update_wrap_at(true);
-	}
-}
-
-bool TextEdit::is_wrap_enabled() const {
-	return wrap_enabled;
-}
-
 void TextEdit::_update_caches() {
 	/* Caret */
 	caret_color = get_theme_color(SNAME("caret_color"));
@@ -3661,8 +3559,8 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_
 	caret.line = p_line;
 
 	int n_col = get_char_pos_for_line(caret.last_fit_x, p_line, p_wrap_index);
-	if (n_col != 0 && is_wrap_enabled() && p_wrap_index < times_line_wraps(p_line)) {
-		Vector<String> rows = get_wrap_rows_text(p_line);
+	if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) {
+		Vector<String> rows = get_line_wrapped_text(p_line);
 		int row_end_col = 0;
 		for (int i = 0; i < p_wrap_index + 1; i++) {
 			row_end_col += rows[i].length();
@@ -3720,7 +3618,77 @@ int TextEdit::get_caret_column() const {
 }
 
 int TextEdit::get_caret_wrap_index() const {
-	return get_line_wrap_index_at_col(caret.line, caret.column);
+	return get_line_wrap_index_at_column(caret.line, caret.column);
+}
+
+/* line wrapping. */
+void TextEdit::set_line_wrapping_mode(LineWrappingMode p_wrapping_mode) {
+	if (line_wrapping_mode != p_wrapping_mode) {
+		line_wrapping_mode = p_wrapping_mode;
+		_update_wrap_at_column(true);
+	}
+}
+
+TextEdit::LineWrappingMode TextEdit::get_line_wrapping_mode() const {
+	return line_wrapping_mode;
+}
+
+bool TextEdit::is_line_wrapped(int p_line) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
+	if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) {
+		return false;
+	}
+	return text.get_line_wrap_amount(p_line) > 0;
+}
+
+int TextEdit::get_line_wrap_count(int p_line) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
+
+	if (!is_line_wrapped(p_line)) {
+		return 0;
+	}
+
+	return text.get_line_wrap_amount(p_line);
+}
+
+int TextEdit::get_line_wrap_index_at_column(int p_line, int p_column) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
+
+	if (!is_line_wrapped(p_line)) {
+		return 0;
+	}
+
+	/* Loop through wraps in the line text until we get to the column. */
+	int wrap_index = 0;
+	int col = 0;
+	Vector<String> lines = get_line_wrapped_text(p_line);
+	for (int i = 0; i < lines.size(); i++) {
+		wrap_index = i;
+		String s = lines[wrap_index];
+		col += s.length();
+		if (col > p_column) {
+			break;
+		}
+	}
+	return wrap_index;
+}
+
+Vector<String> TextEdit::get_line_wrapped_text(int p_line) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), Vector<String>());
+
+	Vector<String> lines;
+	if (!is_line_wrapped(p_line)) {
+		lines.push_back(text[p_line]);
+		return lines;
+	}
+
+	const String &line_text = text[p_line];
+	Vector<Vector2i> line_ranges = text.get_line_wrap_ranges(p_line);
+	for (int i = 0; i < line_ranges.size(); i++) {
+		lines.push_back(line_text.substr(line_ranges[i].x, line_ranges[i].y - line_ranges[i].x));
+	}
+
+	return lines;
 }
 
 /* Syntax Highlighting. */
@@ -4384,7 +4352,7 @@ int TextEdit::num_lines_from_rows(int p_line_from, int p_wrap_index_from, int vi
 	wrap_index = 0;
 	ERR_FAIL_INDEX_V(p_line_from, text.size(), ABS(visible_amount));
 
-	if (!is_hiding_enabled() && !is_wrap_enabled()) {
+	if (!is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) {
 		return ABS(visible_amount);
 	}
 
@@ -4400,22 +4368,22 @@ int TextEdit::num_lines_from_rows(int p_line_from, int p_wrap_index_from, int vi
 			num_total++;
 			if (!is_line_hidden(i)) {
 				num_visible++;
-				num_visible += times_line_wraps(i);
+				num_visible += get_line_wrap_count(i);
 			}
 			if (num_visible >= visible_amount) {
 				break;
 			}
 		}
-		wrap_index = times_line_wraps(MIN(i, text.size() - 1)) - MAX(0, num_visible - visible_amount);
+		wrap_index = get_line_wrap_count(MIN(i, text.size() - 1)) - MAX(0, num_visible - visible_amount);
 	} else {
 		visible_amount = ABS(visible_amount);
 		int i;
-		num_visible -= times_line_wraps(p_line_from) - p_wrap_index_from;
+		num_visible -= get_line_wrap_count(p_line_from) - p_wrap_index_from;
 		for (i = p_line_from; i >= 0; i--) {
 			num_total++;
 			if (!is_line_hidden(i)) {
 				num_visible++;
-				num_visible += times_line_wraps(i);
+				num_visible += get_line_wrap_count(i);
 			}
 			if (num_visible >= visible_amount) {
 				break;
@@ -4709,7 +4677,7 @@ void TextEdit::tag_saved_version() {
 }
 
 double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const {
-	if (!is_wrap_enabled() && !is_hiding_enabled()) {
+	if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE && !is_hiding_enabled()) {
 		return p_line;
 	}
 
@@ -4719,7 +4687,7 @@ double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const {
 	for (int i = 0; i < to; i++) {
 		if (!text.is_hidden(i)) {
 			new_line_scroll_pos++;
-			new_line_scroll_pos += times_line_wraps(i);
+			new_line_scroll_pos += get_line_wrap_count(i);
 		}
 	}
 	new_line_scroll_pos += p_wrap_index;
@@ -4918,7 +4886,7 @@ void TextEdit::insert_at(const String &p_text, int at) {
 void TextEdit::set_draw_minimap(bool p_draw) {
 	if (draw_minimap != p_draw) {
 		draw_minimap = p_draw;
-		_update_wrap_at();
+		_update_wrap_at_column();
 	}
 	update();
 }
@@ -4930,7 +4898,7 @@ bool TextEdit::is_drawing_minimap() const {
 void TextEdit::set_minimap_width(int p_minimap_width) {
 	if (minimap_width != p_minimap_width) {
 		minimap_width = p_minimap_width;
-		_update_wrap_at();
+		_update_wrap_at_column();
 	}
 	update();
 }
@@ -5197,7 +5165,6 @@ void TextEdit::_get_property_list(List<PropertyInfo> *p_list) const {
 void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_gui_input"), &TextEdit::_gui_input);
 	ClassDB::bind_method(D_METHOD("_text_changed_emit"), &TextEdit::_text_changed_emit);
-	ClassDB::bind_method(D_METHOD("_update_wrap_at", "force"), &TextEdit::_update_wrap_at, DEFVAL(false));
 
 	BIND_ENUM_CONSTANT(SEARCH_MATCH_CASE);
 	BIND_ENUM_CONSTANT(SEARCH_WHOLE_WORDS);
@@ -5248,8 +5215,6 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_readonly", "enable"), &TextEdit::set_readonly);
 	ClassDB::bind_method(D_METHOD("is_readonly"), &TextEdit::is_readonly);
 
-	ClassDB::bind_method(D_METHOD("set_wrap_enabled", "enable"), &TextEdit::set_wrap_enabled);
-	ClassDB::bind_method(D_METHOD("is_wrap_enabled"), &TextEdit::is_wrap_enabled);
 	ClassDB::bind_method(D_METHOD("set_context_menu_enabled", "enable"), &TextEdit::set_context_menu_enabled);
 	ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &TextEdit::is_context_menu_enabled);
 	ClassDB::bind_method(D_METHOD("set_shortcut_keys_enabled", "enable"), &TextEdit::set_shortcut_keys_enabled);
@@ -5339,6 +5304,22 @@ void TextEdit::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("get_caret_wrap_index"), &TextEdit::get_caret_wrap_index);
 
+	/* line wrapping. */
+	BIND_ENUM_CONSTANT(LINE_WRAPPING_NONE);
+	BIND_ENUM_CONSTANT(LINE_WRAPPING_BOUNDARY);
+
+	// internal.
+	ClassDB::bind_method(D_METHOD("_update_wrap_at_column", "force"), &TextEdit::_update_wrap_at_column, DEFVAL(false));
+
+	ClassDB::bind_method(D_METHOD("set_line_wrapping_mode", "mode"), &TextEdit::set_line_wrapping_mode);
+	ClassDB::bind_method(D_METHOD("get_line_wrapping_mode"), &TextEdit::get_line_wrapping_mode);
+
+	ClassDB::bind_method(D_METHOD("is_line_wrapped", "line"), &TextEdit::is_line_wrapped);
+	ClassDB::bind_method(D_METHOD("get_line_wrap_count", "line"), &TextEdit::get_line_wrap_count);
+	ClassDB::bind_method(D_METHOD("get_line_wrap_index_at_column", "line", "column"), &TextEdit::get_line_wrap_index_at_column);
+
+	ClassDB::bind_method(D_METHOD("get_line_wrapped_text", "line"), &TextEdit::get_line_wrapped_text);
+
 	/* Syntax Highlighting. */
 	ClassDB::bind_method(D_METHOD("set_syntax_highlighter", "syntax_highlighter"), &TextEdit::set_syntax_highlighter);
 	ClassDB::bind_method(D_METHOD("get_syntax_highlighter"), &TextEdit::get_syntax_highlighter);
@@ -5420,7 +5401,7 @@ void TextEdit::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "smooth_scrolling"), "set_smooth_scroll_enable", "is_smooth_scroll_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "v_scroll_speed"), "set_v_scroll_speed", "get_v_scroll_speed");
-	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "wrap_enabled"), "set_wrap_enabled", "is_wrap_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::FLOAT, "scroll_vertical"), "set_v_scroll", "get_v_scroll");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_horizontal"), "set_h_scroll", "get_h_scroll");
 
@@ -5716,6 +5697,41 @@ void TextEdit::_toggle_draw_caret() {
 	}
 }
 
+/* Line Wrapping */
+void TextEdit::_update_wrap_at_column(bool p_force) {
+	int new_wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding;
+	if (draw_minimap) {
+		new_wrap_at -= minimap_width;
+	}
+	if (v_scroll->is_visible_in_tree()) {
+		new_wrap_at -= v_scroll->get_combined_minimum_size().width;
+	}
+	/* Give it a little more space. */
+	new_wrap_at -= wrap_right_offset;
+
+	if ((wrap_at_column != new_wrap_at) || p_force) {
+		wrap_at_column = new_wrap_at;
+		if (line_wrapping_mode) {
+			text.set_width(wrap_at_column);
+		} else {
+			text.set_width(-1);
+		}
+		text.invalidate_all_lines();
+	}
+
+	_update_caret_wrap_offset();
+}
+
+void TextEdit::_update_caret_wrap_offset() {
+	int first_vis_line = get_first_visible_line();
+	if (is_line_wrapped(first_vis_line)) {
+		caret.wrap_ofs = MIN(caret.wrap_ofs, get_line_wrap_count(first_vis_line));
+	} else {
+		caret.wrap_ofs = 0;
+	}
+	set_line_as_first_visible(caret.line_ofs, caret.wrap_ofs);
+}
+
 TextEdit::TextEdit() {
 	clear();
 	set_focus_mode(FOCUS_ALL);

+ 37 - 22
scene/gui/text_edit.h

@@ -48,12 +48,6 @@ public:
 		CARET_TYPE_BLOCK
 	};
 
-	enum GutterType {
-		GUTTER_TYPE_STRING,
-		GUTTER_TYPE_ICON,
-		GUTTER_TYPE_CUSTOM
-	};
-
 	/* Selection */
 	enum SelectionMode {
 		SELECTION_MODE_NONE,
@@ -63,6 +57,19 @@ public:
 		SELECTION_MODE_LINE
 	};
 
+	/* Line Wrapping.*/
+	enum LineWrappingMode {
+		LINE_WRAPPING_NONE,
+		LINE_WRAPPING_BOUNDARY
+	};
+
+	/* Gutters. */
+	enum GutterType {
+		GUTTER_TYPE_STRING,
+		GUTTER_TYPE_ICON,
+		GUTTER_TYPE_CUSTOM
+	};
+
 private:
 	struct GutterInfo {
 		GutterType type = GutterType::GUTTER_TYPE_STRING;
@@ -183,7 +190,7 @@ private:
 	/* Text manipulation */
 	String cut_copy_line = "";
 
-	/* Caret */
+	/* Caret. */
 	struct Caret {
 		Point2 draw_pos;
 		bool visible = false;
@@ -217,6 +224,7 @@ private:
 	void _reset_caret_blink_timer();
 	void _toggle_draw_caret();
 
+	/* Selection. */
 	struct Selection {
 		SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE;
 		int selecting_line = 0;
@@ -236,6 +244,17 @@ private:
 		bool shiftclick_left = false;
 	} selection;
 
+	/* line wrapping. */
+	LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE;
+
+	int wrap_at_column = 0;
+	int wrap_right_offset = 10;
+
+	void _update_wrap_at_column(bool p_force = false);
+
+	void _update_caret_wrap_offset();
+
+	/* Syntax highlighting. */
 	Map<int, Dictionary> syntax_highlighting_cache;
 
 	struct TextOperation {
@@ -295,10 +314,6 @@ private:
 
 	bool window_has_focus = true;
 
-	bool wrap_enabled = false;
-	int wrap_at = 0;
-	int wrap_right_offset = 10;
-
 	bool first_draw = true;
 	bool draw_tabs = false;
 	bool draw_spaces = false;
@@ -362,10 +377,6 @@ private:
 
 	int _get_minimap_visible_rows() const;
 
-	void _update_caret_wrap_offset();
-	void _update_wrap_at(bool p_force = false);
-	Vector<String> get_wrap_rows_text(int p_line) const;
-
 	double get_scroll_pos_for_line(int p_line, int p_wrap_index = 0) const;
 	void set_line_as_first_visible(int p_line, int p_wrap_index = 0);
 	void set_line_as_center_visible(int p_line, int p_wrap_index = 0);
@@ -537,6 +548,16 @@ public:
 
 	int get_caret_wrap_index() const;
 
+	/* line wrapping. */
+	void set_line_wrapping_mode(LineWrappingMode p_wrapping_mode);
+	LineWrappingMode get_line_wrapping_mode() const;
+
+	bool is_line_wrapped(int p_line) const;
+	int get_line_wrap_count(int p_line) const;
+	int get_line_wrap_index_at_column(int p_line, int p_column) const;
+
+	Vector<String> get_line_wrapped_text(int p_line) const;
+
 	/* Syntax Highlighting. */
 	Ref<SyntaxHighlighter> get_syntax_highlighter();
 	void set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter);
@@ -664,7 +685,6 @@ public:
 	void insert_at(const String &p_text, int at);
 	int get_line_count() const;
 	int get_line_width(int p_line, int p_wrap_offset = -1) const;
-	int get_line_wrap_index_at_col(int p_line, int p_column) const;
 
 	void set_line_as_hidden(int p_line, bool p_hidden);
 	bool is_line_hidden(int p_line) const;
@@ -698,11 +718,6 @@ public:
 	void set_readonly(bool p_readonly);
 	bool is_readonly() const;
 
-	void set_wrap_enabled(bool p_wrap_enabled);
-	bool is_wrap_enabled() const;
-	bool line_wraps(int line) const;
-	int times_line_wraps(int line) const;
-
 	void clear();
 
 	void delete_selection();
@@ -799,8 +814,8 @@ public:
 	~TextEdit();
 };
 
-
 VARIANT_ENUM_CAST(TextEdit::CaretType);
+VARIANT_ENUM_CAST(TextEdit::LineWrappingMode);
 VARIANT_ENUM_CAST(TextEdit::SelectionMode);
 VARIANT_ENUM_CAST(TextEdit::GutterType);
 VARIANT_ENUM_CAST(TextEdit::MenuItems);