diff options
| -rw-r--r-- | scene/gui/label.cpp | 716 | ||||
| -rw-r--r-- | scene/gui/label.h | 54 | ||||
| -rw-r--r-- | scene/gui/line_edit.cpp | 1097 | ||||
| -rw-r--r-- | scene/gui/line_edit.h | 127 | 
4 files changed, 1232 insertions, 762 deletions
diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index a15db9528f..e83c062e8a 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -34,13 +34,13 @@  #include "core/string/print_string.h"  #include "core/string/translation.h" +#include "servers/text_server.h" +  void Label::set_autowrap(bool p_autowrap) { -	if (autowrap == p_autowrap) { -		return; +	if (autowrap != p_autowrap) { +		autowrap = p_autowrap; +		lines_dirty = true;  	} - -	autowrap = p_autowrap; -	word_cache_dirty = true;  	update();  	if (clip) { @@ -54,7 +54,8 @@ bool Label::has_autowrap() const {  void Label::set_uppercase(bool p_uppercase) {  	uppercase = p_uppercase; -	word_cache_dirty = true; +	dirty = true; +  	update();  } @@ -62,8 +63,95 @@ bool Label::is_uppercase() const {  	return uppercase;  } -int Label::get_line_height() const { -	return get_theme_font("font")->get_height(); +int Label::get_line_height(int p_line) const { +	if (p_line >= 0 && p_line < lines_rid.size()) { +		return TS->shaped_text_get_size(lines_rid[p_line]).y; +	} else if (lines_rid.size() > 0) { +		int h = 0; +		for (int i = 0; i < lines_rid.size(); i++) { +			h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y); +		} +		return h; +	} else { +		return get_theme_font("font")->get_height(get_theme_font_size("font_size")); +	} +} + +void Label::_shape() { +	Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); +	int width = (get_size().width - style->get_minimum_size().width); + +	if (dirty) { +		TS->shaped_text_clear(text_rid); +		if (text_direction == Control::TEXT_DIRECTION_INHERITED) { +			TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); +		} else { +			TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); +		} +		TS->shaped_text_add_string(text_rid, (uppercase) ? xl_text.to_upper() : xl_text, get_theme_font("font")->get_rids(), get_theme_font_size("font_size"), opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +		TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, xl_text)); +		dirty = false; +		lines_dirty = true; +	} +	if (lines_dirty) { +		for (int i = 0; i < lines_rid.size(); i++) { +			TS->free(lines_rid[i]); +		} +		lines_rid.clear(); + +		Vector<Vector2i> lines = TS->shaped_text_get_line_breaks(text_rid, width, 0, (autowrap) ? (TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND) : TextServer::BREAK_MANDATORY); +		for (int i = 0; i < lines.size(); i++) { +			RID line = TS->shaped_text_substr(text_rid, lines[i].x, lines[i].y - lines[i].x); +			lines_rid.push_back(line); +		} +	} + +	if (xl_text.length() == 0) { +		minsize = Size2(1, get_line_height()); +		return; +	} +	if (!autowrap) { +		minsize.width = 0.0f; +		for (int i = 0; i < lines_rid.size(); i++) { +			if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) { +				minsize.width = TS->shaped_text_get_size(lines_rid[i]).x; +			} +		} +	} + +	if (lines_dirty) { // Fill after min_size calculation. +		if (align == ALIGN_FILL) { +			for (int i = 0; i < lines_rid.size(); i++) { +				TS->shaped_text_fit_to_width(lines_rid.write[i], width); +			} +		} +		lines_dirty = false; +	} + +	_update_visible(); + +	if (!autowrap || !clip) { +		minimum_size_changed(); +	} +} + +void Label::_update_visible() { +	int line_spacing = get_theme_constant("line_spacing", "Label"); +	Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); +	int lines_visible = lines_rid.size(); + +	if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { +		lines_visible = max_lines_visible; +	} + +	minsize.height = 0; +	int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); +	for (int64_t i = lines_skipped; i < last_line; i++) { +		minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; +		if (minsize.height > (get_size().height - style->get_minimum_size().height + line_spacing)) { +			break; +		} +	}  }  void Label::_notification(int p_what) { @@ -73,8 +161,8 @@ void Label::_notification(int p_what) {  			return; //nothing new  		}  		xl_text = new_text; +		dirty = true; -		regenerate_word_cache();  		update();  	} @@ -83,8 +171,8 @@ void Label::_notification(int p_what) {  			RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true);  		} -		if (word_cache_dirty) { -			regenerate_word_cache(); +		if (dirty || lines_dirty) { +			_shape();  		}  		RID ci = get_canvas_item(); @@ -93,54 +181,61 @@ void Label::_notification(int p_what) {  		Size2 size = get_size();  		Ref<StyleBox> style = get_theme_stylebox("normal");  		Ref<Font> font = get_theme_font("font"); -		int font_size = get_theme_font_size("font_size");  		Color font_color = get_theme_color("font_color");  		Color font_color_shadow = get_theme_color("font_color_shadow"); -		bool use_outline = get_theme_constant("shadow_as_outline");  		Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y"));  		int line_spacing = get_theme_constant("line_spacing"); -		//Color font_outline_modulate = get_theme_color("font_outline_modulate"); +		Color font_outline_modulate = get_theme_color("font_outline_modulate"); +		int outline_size = get_theme_constant("outline_size"); +		int shadow_outline_size = get_theme_constant("shadow_outline_size"); +		bool rtl = is_layout_rtl();  		style->draw(ci, Rect2(Point2(0, 0), get_size())); -		//RenderingServer::get_singleton()->canvas_item_set_distance_field_mode(get_canvas_item(), font.is_valid() && font->is_distance_field_hint()); - -		int font_h = font->get_height() + line_spacing; - -		int lines_visible = (size.y + line_spacing) / font_h; - -		real_t space_w = font->get_char_size(' ').width; -		int chars_total = 0; +		float total_h = 0; +		int lines_visible = 0; -		int vbegin = 0, vsep = 0; - -		if (lines_visible > line_count) { -			lines_visible = line_count; +		// Get number of lines to fit to the height. +		for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { +			total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; +			if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { +				break; +			} +			lines_visible++;  		}  		if (max_lines_visible >= 0 && lines_visible > max_lines_visible) {  			lines_visible = max_lines_visible;  		} +		int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + +		// Get real total height. +		total_h = 0; +		for (int64_t i = lines_skipped; i < last_line; i++) { +			total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; +		} + +		int vbegin = 0, vsep = 0;  		if (lines_visible > 0) {  			switch (valign) {  				case VALIGN_TOP: {  					//nothing  				} break;  				case VALIGN_CENTER: { -					vbegin = (size.y - (lines_visible * font_h - line_spacing)) / 2; +					vbegin = (size.y - (total_h - line_spacing)) / 2;  					vsep = 0;  				} break;  				case VALIGN_BOTTOM: { -					vbegin = size.y - (lines_visible * font_h - line_spacing); +					vbegin = size.y - (total_h - line_spacing);  					vsep = 0;  				} break;  				case VALIGN_FILL: {  					vbegin = 0;  					if (lines_visible > 1) { -						vsep = (size.y - (lines_visible * font_h - line_spacing)) / (lines_visible - 1); +						vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1);  					} else {  						vsep = 0;  					} @@ -149,139 +244,109 @@ void Label::_notification(int p_what) {  			}  		} -		WordCache *wc = word_cache; -		if (!wc) { -			return; -		} - -		int line = 0; -		int line_to = lines_skipped + (lines_visible > 0 ? lines_visible : 1); -		//FontDrawer drawer(font, font_outline_modulate); -		while (wc) { -			/* handle lines not meant to be drawn quickly */ -			if (line >= line_to) { -				break; -			} -			if (line < lines_skipped) { -				while (wc && wc->char_pos >= 0) { -					wc = wc->next; -				} -				if (wc) { -					wc = wc->next; -				} -				line++; -				continue; -			} - -			/* handle lines normally */ - -			if (wc->char_pos < 0) { -				//empty line -				wc = wc->next; -				line++; -				continue; -			} - -			WordCache *from = wc; -			WordCache *to = wc; - -			int taken = 0; -			int spaces = 0; -			while (to && to->char_pos >= 0) { -				taken += to->pixel_width; -				if (to->space_count) { -					spaces += to->space_count; +		int visible_glyphs = -1; +		int glyhps_drawn = 0; +		if (percent_visible < 1) { +			int total_glyphs = 0; +			for (int i = lines_skipped; i < last_line; i++) { +				const Vector<TextServer::Glyph> glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); +				for (int j = 0; j < glyphs.size(); j++) { +					if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { +						total_glyphs++; +					}  				} -				to = to->next;  			} +			visible_glyphs = total_glyphs * percent_visible; +		} -			bool can_fill = to && to->char_pos == WordCache::CHAR_WRAPLINE; - -			float x_ofs = 0; - +		Vector2 ofs; +		ofs.y = style->get_offset().y + vbegin; +		for (int i = lines_skipped; i < last_line; i++) { +			ofs.x = 0; +			ofs.y += TS->shaped_text_get_ascent(lines_rid[i]);  			switch (align) {  				case ALIGN_FILL:  				case ALIGN_LEFT: { -					x_ofs = style->get_offset().x; +					if (rtl) { +						ofs.x = int(size.width - style->get_margin(MARGIN_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); +					} else { +						ofs.x = style->get_offset().x; +					}  				} break;  				case ALIGN_CENTER: { -					x_ofs = int(size.width - (taken + spaces * space_w)) / 2; +					ofs.x = int(size.width - TS->shaped_text_get_size(lines_rid[i]).x) / 2;  				} break;  				case ALIGN_RIGHT: { -					x_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (taken + spaces * space_w)); +					if (rtl) { +						ofs.x = style->get_offset().x; +					} else { +						ofs.x = int(size.width - style->get_margin(MARGIN_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); +					}  				} break;  			} -			float y_ofs = style->get_offset().y; -			y_ofs += (line - lines_skipped) * font_h + font->get_ascent(); -			y_ofs += vbegin + line * vsep; - -			while (from != to) { -				// draw a word -				int pos = from->char_pos; -				if (from->char_pos < 0) { -					ERR_PRINT("BUG"); -					return; -				} -				if (from->space_count) { -					/* spacing */ -					x_ofs += space_w * from->space_count; -					if (can_fill && align == ALIGN_FILL && spaces) { -						x_ofs += int((size.width - (taken + space_w * spaces)) / spaces); +			const Vector<TextServer::Glyph> glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); + +			float x = ofs.x; +			int outlines_drawn = glyhps_drawn; +			for (int j = 0; j < glyphs.size(); j++) { +				for (int k = 0; k < glyphs[j].repeat; k++) { +					if (glyphs[j].font_rid != RID()) { +						if (font_color_shadow.a > 0) { +							TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + shadow_ofs, glyphs[j].index, font_color_shadow); +							if (shadow_outline_size > 0) { +								//draw shadow +								TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, shadow_ofs.y), glyphs[j].index, font_color_shadow); +								TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_color_shadow); +								TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_color_shadow); +							} +						} +						if (font_outline_modulate.a != 0.0 && outline_size > 0) { +							TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_outline_modulate); +						}  					} +					ofs.x += glyphs[j].advance;  				} - -				if (font_color_shadow.a > 0) { -					int chars_total_shadow = chars_total; //save chars drawn -					float x_ofs_shadow = x_ofs; -					for (int i = 0; i < from->word_len; i++) { -						if (visible_chars < 0 || chars_total_shadow < visible_chars) { -							char32_t c = xl_text[i + pos]; -							char32_t n = xl_text[i + pos + 1]; -							if (uppercase) { -								c = String::char_uppercase(c); -								n = String::char_uppercase(n); -							} - -							//TODO replace with TS -							float move = font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_size, font_color_shadow); -							if (use_outline) { -								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, shadow_ofs.y), c, n, font_size, font_color_shadow); -								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(shadow_ofs.x, -shadow_ofs.y), c, n, font_size, font_color_shadow); -								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, -shadow_ofs.y), c, n, font_size, font_color_shadow); -							} -							x_ofs_shadow += move; -							chars_total_shadow++; +				if (visible_glyphs != -1) { +					if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { +						outlines_drawn++; +						if (outlines_drawn >= visible_glyphs) { +							break;  						}  					}  				} -				for (int i = 0; i < from->word_len; i++) { -					if (visible_chars < 0 || chars_total < visible_chars) { -						char32_t c = xl_text[i + pos]; -						char32_t n = xl_text[i + pos + 1]; -						if (uppercase) { -							c = String::char_uppercase(c); -							n = String::char_uppercase(n); +			} +			ofs.x = x; + +			for (int j = 0; j < glyphs.size(); j++) { +				for (int k = 0; k < glyphs[j].repeat; k++) { +					if (glyphs[j].font_rid != RID()) { +						TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); +					} else if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { +						TS->draw_hex_code_box(ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); +					} +					ofs.x += glyphs[j].advance; +				} +				if (visible_glyphs != -1) { +					if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { +						glyhps_drawn++; +						if (glyhps_drawn >= visible_glyphs) { +							return;  						} - -						x_ofs += font->draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_size, font_color); -						chars_total++;  					}  				} -				from = from->next;  			} -			wc = to ? to->next : nullptr; -			line++; +			ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing;  		}  	}  	if (p_what == NOTIFICATION_THEME_CHANGED) { -		word_cache_dirty = true; +		dirty = true;  		update();  	}  	if (p_what == NOTIFICATION_RESIZED) { -		word_cache_dirty = true; +		lines_dirty = true;  	}  } @@ -289,8 +354,8 @@ Size2 Label::get_minimum_size() const {  	Size2 min_style = get_theme_stylebox("normal")->get_minimum_size();  	// don't want to mutable everything -	if (word_cache_dirty) { -		const_cast<Label *>(this)->regenerate_word_cache(); +	if (dirty || lines_dirty) { +		const_cast<Label *>(this)->_shape();  	}  	if (autowrap) { @@ -304,56 +369,32 @@ Size2 Label::get_minimum_size() const {  	}  } -int Label::get_longest_line_width() const { -	Ref<Font> font = get_theme_font("font"); -	real_t max_line_width = 0; -	real_t line_width = 0; - -	for (int i = 0; i < xl_text.size(); i++) { -		char32_t current = xl_text[i]; -		if (uppercase) { -			current = String::char_uppercase(current); -		} - -		if (current < 32) { -			if (current == '\n') { -				if (line_width > max_line_width) { -					max_line_width = line_width; -				} -				line_width = 0; -			} -		} else { -			real_t char_width = font->get_char_size(current, xl_text[i + 1]).width; -			line_width += char_width; -		} -	} - -	if (line_width > max_line_width) { -		max_line_width = line_width; -	} - -	// ceiling to ensure autowrapping does not cut text -	return Math::ceil(max_line_width); -} -  int Label::get_line_count() const {  	if (!is_inside_tree()) {  		return 1;  	} -	if (word_cache_dirty) { -		const_cast<Label *>(this)->regenerate_word_cache(); +	if (dirty || lines_dirty) { +		const_cast<Label *>(this)->_shape();  	} -	return line_count; +	return lines_rid.size();  }  int Label::get_visible_line_count() const { +	Ref<StyleBox> style = get_theme_stylebox("normal");  	int line_spacing = get_theme_constant("line_spacing"); -	int font_h = get_theme_font("font")->get_height() + line_spacing; -	int lines_visible = (get_size().height - get_theme_stylebox("normal")->get_minimum_size().height + line_spacing) / font_h; +	int lines_visible = 0; +	float total_h = 0; +	for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { +		total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; +		if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { +			break; +		} +		lines_visible++; +	} -	if (lines_visible > line_count) { -		lines_visible = line_count; +	if (lines_visible > lines_rid.size()) { +		lines_visible = lines_rid.size();  	}  	if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { @@ -363,171 +404,14 @@ int Label::get_visible_line_count() const {  	return lines_visible;  } -void Label::regenerate_word_cache() { -	while (word_cache) { -		WordCache *current = word_cache; -		word_cache = current->next; -		memdelete(current); -	} - -	int width; -	if (autowrap) { -		Ref<StyleBox> style = get_theme_stylebox("normal"); -		width = MAX(get_size().width, get_custom_minimum_size().width) - style->get_minimum_size().width; -	} else { -		width = get_longest_line_width(); -	} - -	Ref<Font> font = get_theme_font("font"); - -	real_t current_word_size = 0; -	int word_pos = 0; -	real_t line_width = 0; -	int space_count = 0; -	real_t space_width = font->get_char_size(' ').width; -	int line_spacing = get_theme_constant("line_spacing"); -	line_count = 1; -	total_char_cache = 0; - -	WordCache *last = nullptr; - -	for (int i = 0; i <= xl_text.length(); i++) { -		char32_t current = i < xl_text.length() ? xl_text[i] : L' '; //always a space at the end, so the algo works - -		if (uppercase) { -			current = String::char_uppercase(current); -		} - -		// ranges taken from http://www.unicodemap.org/ -		// if your language is not well supported, consider helping improve -		// the unicode support in Godot. -		bool separatable = (current >= 0x2E08 && current <= 0xFAFF) || (current >= 0xFE30 && current <= 0xFE4F); -		//current>=33 && (current < 65||current >90) && (current<97||current>122) && (current<48||current>57); -		bool insert_newline = false; -		real_t char_width = 0; - -		if (current < 33) { -			if (current_word_size > 0) { -				WordCache *wc = memnew(WordCache); -				if (word_cache) { -					last->next = wc; -				} else { -					word_cache = wc; -				} -				last = wc; - -				wc->pixel_width = current_word_size; -				wc->char_pos = word_pos; -				wc->word_len = i - word_pos; -				wc->space_count = space_count; -				current_word_size = 0; -				space_count = 0; -			} else if ((i == xl_text.length() || current == '\n') && last != nullptr && space_count != 0) { -				//in case there are trailing white spaces we add a placeholder word cache with just the spaces -				WordCache *wc = memnew(WordCache); -				if (word_cache) { -					last->next = wc; -				} else { -					word_cache = wc; -				} -				last = wc; - -				wc->pixel_width = 0; -				wc->char_pos = 0; -				wc->word_len = 0; -				wc->space_count = space_count; -				current_word_size = 0; -				space_count = 0; -			} - -			if (current == '\n') { -				insert_newline = true; -			} else if (current != ' ') { -				total_char_cache++; -			} - -			if (i < xl_text.length() && xl_text[i] == ' ') { -				if (line_width > 0 || last == nullptr || last->char_pos != WordCache::CHAR_WRAPLINE) { -					space_count++; -					line_width += space_width; -				} else { -					space_count = 0; -				} -			} - -		} else { -			// latin characters -			if (current_word_size == 0) { -				word_pos = i; -			} -			char_width = font->get_char_size(current, xl_text[i + 1]).width; -			current_word_size += char_width; -			line_width += char_width; -			total_char_cache++; - -			// allow autowrap to cut words when they exceed line width -			if (autowrap && (current_word_size > width)) { -				separatable = true; -			} -		} - -		if ((autowrap && (line_width >= width) && ((last && last->char_pos >= 0) || separatable)) || insert_newline) { -			if (separatable) { -				if (current_word_size > 0) { -					WordCache *wc = memnew(WordCache); -					if (word_cache) { -						last->next = wc; -					} else { -						word_cache = wc; -					} -					last = wc; - -					wc->pixel_width = current_word_size - char_width; -					wc->char_pos = word_pos; -					wc->word_len = i - word_pos; -					wc->space_count = space_count; -					current_word_size = char_width; -					word_pos = i; -				} -			} - -			WordCache *wc = memnew(WordCache); -			if (word_cache) { -				last->next = wc; -			} else { -				word_cache = wc; -			} -			last = wc; - -			wc->pixel_width = 0; -			wc->char_pos = insert_newline ? WordCache::CHAR_NEWLINE : WordCache::CHAR_WRAPLINE; - -			line_width = current_word_size; -			line_count++; -			space_count = 0; -		} -	} - -	if (!autowrap) { -		minsize.width = width; -	} - -	if (max_lines_visible > 0 && line_count > max_lines_visible) { -		minsize.height = (font->get_height() * max_lines_visible) + (line_spacing * (max_lines_visible - 1)); -	} else { -		minsize.height = (font->get_height() * line_count) + (line_spacing * (line_count - 1)); -	} - -	if (!autowrap || !clip) { -		//helps speed up some labels that may change a lot, as no resizing is requested. Do not change. -		minimum_size_changed(); -	} -	word_cache_dirty = false; -} -  void Label::set_align(Align p_align) {  	ERR_FAIL_INDEX((int)p_align, 4); -	align = p_align; +	if (align != p_align) { +		if (align == ALIGN_FILL || p_align == ALIGN_FILL) { +			lines_dirty = true; // Reshape lines. +		} +		align = p_align; +	}  	update();  } @@ -551,13 +435,83 @@ void Label::set_text(const String &p_string) {  	}  	text = p_string;  	xl_text = tr(p_string); -	word_cache_dirty = true; +	dirty = true;  	if (percent_visible < 1) {  		visible_chars = get_total_character_count() * percent_visible;  	}  	update();  } +void Label::set_text_direction(Control::TextDirection p_text_direction) { +	ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); +	if (text_direction != p_text_direction) { +		text_direction = p_text_direction; +		dirty = true; +		update(); +	} +} + +void Label::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +	if (st_parser != p_parser) { +		st_parser = p_parser; +		dirty = true; +		update(); +	} +} + +Control::StructuredTextParser Label::get_structured_text_bidi_override() const { +	return st_parser; +} + +void Label::set_structured_text_bidi_override_options(Array p_args) { +	st_args = p_args; +	dirty = true; +	update(); +} + +Array Label::get_structured_text_bidi_override_options() const { +	return st_args; +} + +Control::TextDirection Label::get_text_direction() const { +	return text_direction; +} + +void Label::clear_opentype_features() { +	opentype_features.clear(); +	dirty = true; +	update(); +} + +void Label::set_opentype_feature(const String &p_name, int p_value) { +	int32_t tag = TS->name_to_tag(p_name); +	if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { +		opentype_features[tag] = p_value; +		dirty = true; +		update(); +	} +} + +int Label::get_opentype_feature(const String &p_name) const { +	int32_t tag = TS->name_to_tag(p_name); +	if (!opentype_features.has(tag)) { +		return -1; +	} +	return opentype_features[tag]; +} + +void Label::set_language(const String &p_language) { +	if (language != p_language) { +		language = p_language; +		dirty = true; +		update(); +	} +} + +String Label::get_language() const { +	return language; +} +  void Label::set_clip_text(bool p_clip) {  	clip = p_clip;  	update(); @@ -575,7 +529,7 @@ String Label::get_text() const {  void Label::set_visible_characters(int p_amount) {  	visible_chars = p_amount;  	if (get_total_character_count() > 0) { -		percent_visible = (float)p_amount / (float)total_char_cache; +		percent_visible = (float)p_amount / (float)get_total_character_count();  	}  	_change_notify("percent_visible");  	update(); @@ -604,6 +558,7 @@ float Label::get_percent_visible() const {  void Label::set_lines_skipped(int p_lines) {  	lines_skipped = p_lines; +	_update_visible();  	update();  } @@ -613,6 +568,7 @@ int Label::get_lines_skipped() const {  void Label::set_max_lines_visible(int p_lines) {  	max_lines_visible = p_lines; +	_update_visible();  	update();  } @@ -621,11 +577,61 @@ int Label::get_max_lines_visible() const {  }  int Label::get_total_character_count() const { -	if (word_cache_dirty) { -		const_cast<Label *>(this)->regenerate_word_cache(); +	if (dirty || lines_dirty) { +		const_cast<Label *>(this)->_shape();  	} -	return total_char_cache; +	return xl_text.length(); +} + +bool Label::_set(const StringName &p_name, const Variant &p_value) { +	String str = p_name; +	if (str.begins_with("opentype_features/")) { +		String name = str.get_slicec('/', 1); +		int32_t tag = TS->name_to_tag(name); +		double value = p_value; +		if (value == -1) { +			if (opentype_features.has(tag)) { +				opentype_features.erase(tag); +				dirty = true; +				update(); +			} +		} else { +			if ((double)opentype_features[tag] != value) { +				opentype_features[tag] = value; +				dirty = true; +				update(); +			} +		} +		_change_notify(); +		return true; +	} + +	return false; +} + +bool Label::_get(const StringName &p_name, Variant &r_ret) const { +	String str = p_name; +	if (str.begins_with("opentype_features/")) { +		String name = str.get_slicec('/', 1); +		int32_t tag = TS->name_to_tag(name); +		if (opentype_features.has(tag)) { +			r_ret = opentype_features[tag]; +			return true; +		} else { +			r_ret = -1; +			return true; +		} +	} +	return false; +} + +void Label::_get_property_list(List<PropertyInfo> *p_list) const { +	for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { +		String name = TS->tag_to_name(*ftr); +		p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); +	} +	p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR));  }  void Label::_bind_methods() { @@ -635,13 +641,20 @@ void Label::_bind_methods() {  	ClassDB::bind_method(D_METHOD("get_valign"), &Label::get_valign);  	ClassDB::bind_method(D_METHOD("set_text", "text"), &Label::set_text);  	ClassDB::bind_method(D_METHOD("get_text"), &Label::get_text); +	ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &Label::set_text_direction); +	ClassDB::bind_method(D_METHOD("get_text_direction"), &Label::get_text_direction); +	ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &Label::set_opentype_feature); +	ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &Label::get_opentype_feature); +	ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Label::clear_opentype_features); +	ClassDB::bind_method(D_METHOD("set_language", "language"), &Label::set_language); +	ClassDB::bind_method(D_METHOD("get_language"), &Label::get_language);  	ClassDB::bind_method(D_METHOD("set_autowrap", "enable"), &Label::set_autowrap);  	ClassDB::bind_method(D_METHOD("has_autowrap"), &Label::has_autowrap);  	ClassDB::bind_method(D_METHOD("set_clip_text", "enable"), &Label::set_clip_text);  	ClassDB::bind_method(D_METHOD("is_clipping_text"), &Label::is_clipping_text);  	ClassDB::bind_method(D_METHOD("set_uppercase", "enable"), &Label::set_uppercase);  	ClassDB::bind_method(D_METHOD("is_uppercase"), &Label::is_uppercase); -	ClassDB::bind_method(D_METHOD("get_line_height"), &Label::get_line_height); +	ClassDB::bind_method(D_METHOD("get_line_height", "line"), &Label::get_line_height, DEFVAL(-1));  	ClassDB::bind_method(D_METHOD("get_line_count"), &Label::get_line_count);  	ClassDB::bind_method(D_METHOD("get_visible_line_count"), &Label::get_visible_line_count);  	ClassDB::bind_method(D_METHOD("get_total_character_count"), &Label::get_total_character_count); @@ -653,6 +666,10 @@ void Label::_bind_methods() {  	ClassDB::bind_method(D_METHOD("get_lines_skipped"), &Label::get_lines_skipped);  	ClassDB::bind_method(D_METHOD("set_max_lines_visible", "lines_visible"), &Label::set_max_lines_visible);  	ClassDB::bind_method(D_METHOD("get_max_lines_visible"), &Label::get_max_lines_visible); +	ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &Label::set_structured_text_bidi_override); +	ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &Label::get_structured_text_bidi_override); +	ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &Label::set_structured_text_bidi_override_options); +	ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &Label::get_structured_text_bidi_override_options);  	BIND_ENUM_CONSTANT(ALIGN_LEFT);  	BIND_ENUM_CONSTANT(ALIGN_CENTER); @@ -665,6 +682,8 @@ void Label::_bind_methods() {  	BIND_ENUM_CONSTANT(VALIGN_FILL);  	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); +	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); +	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");  	ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_align", "get_align");  	ADD_PROPERTY(PropertyInfo(Variant::INT, "valign", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_valign", "get_valign");  	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autowrap"), "set_autowrap", "has_autowrap"); @@ -674,18 +693,23 @@ void Label::_bind_methods() {  	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible");  	ADD_PROPERTY(PropertyInfo(Variant::INT, "lines_skipped", PROPERTY_HINT_RANGE, "0,999,1"), "set_lines_skipped", "get_lines_skipped");  	ADD_PROPERTY(PropertyInfo(Variant::INT, "max_lines_visible", PROPERTY_HINT_RANGE, "-1,999,1"), "set_max_lines_visible", "get_max_lines_visible"); +	ADD_GROUP("Structured Text", "structured_text_"); +	ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); +	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options");  }  Label::Label(const String &p_text) { +	text_rid = TS->create_shaped_text(); +  	set_mouse_filter(MOUSE_FILTER_IGNORE);  	set_text(p_text);  	set_v_size_flags(SIZE_SHRINK_CENTER);  }  Label::~Label() { -	while (word_cache) { -		WordCache *current = word_cache; -		word_cache = current->next; -		memdelete(current); +	for (int i = 0; i < lines_rid.size(); i++) { +		TS->free(lines_rid[i]);  	} +	lines_rid.clear(); +	TS->free(text_rid);  } diff --git a/scene/gui/label.h b/scene/gui/label.h index df78a1b34c..386297f582 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -59,39 +59,37 @@ private:  	bool autowrap = false;  	bool clip = false;  	Size2 minsize; -	int line_count = 0;  	bool uppercase = false; -	int get_longest_line_width() const; - -	struct WordCache { -		enum { -			CHAR_NEWLINE = -1, -			CHAR_WRAPLINE = -2 -		}; -		int char_pos = 0; // if -1, then newline -		int word_len = 0; -		int pixel_width = 0; -		int space_count = 0; -		WordCache *next = nullptr; -	}; +	bool lines_dirty = true; +	bool dirty = true; +	RID text_rid; +	Vector<RID> lines_rid; -	bool word_cache_dirty = true; -	void regenerate_word_cache(); +	Dictionary opentype_features; +	String language; +	TextDirection text_direction = TEXT_DIRECTION_AUTO; +	Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; +	Array st_args;  	float percent_visible = 1; -	WordCache *word_cache = nullptr; -	int total_char_cache = 0;  	int visible_chars = -1;  	int lines_skipped = 0;  	int max_lines_visible = -1; +	void _update_visible(); +	void _shape(); +  protected:  	void _notification(int p_what);  	static void _bind_methods(); -	// bind helpers + +	bool _set(const StringName &p_name, const Variant &p_value); +	bool _get(const StringName &p_name, Variant &r_ret) const; +	void _get_property_list(List<PropertyInfo> *p_list) const; +  public:  	virtual Size2 get_minimum_size() const override; @@ -104,6 +102,22 @@ public:  	void set_text(const String &p_string);  	String get_text() const; +	void set_text_direction(TextDirection p_text_direction); +	TextDirection get_text_direction() const; + +	void set_opentype_feature(const String &p_name, int p_value); +	int get_opentype_feature(const String &p_name) const; +	void clear_opentype_features(); + +	void set_language(const String &p_language); +	String get_language() const; + +	void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); +	Control::StructuredTextParser get_structured_text_bidi_override() const; + +	void set_structured_text_bidi_override_options(Array p_args); +	Array get_structured_text_bidi_override_options() const; +  	void set_autowrap(bool p_autowrap);  	bool has_autowrap() const; @@ -126,7 +140,7 @@ public:  	void set_max_lines_visible(int p_lines);  	int get_max_lines_visible() const; -	int get_line_height() const; +	int get_line_height(int p_line = -1) const;  	int get_line_count() const;  	int get_visible_line_count() const; diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 1502f1cbfa..2eaa814419 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -37,19 +37,21 @@  #include "core/string/translation.h"  #include "label.h"  #include "servers/display_server.h" +#include "servers/text_server.h"  #ifdef TOOLS_ENABLED  #include "editor/editor_scale.h"  #include "editor/editor_settings.h"  #endif  #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { -	return !is_symbol(c); -}  void LineEdit::_gui_input(Ref<InputEvent> p_event) {  	Ref<InputEventMouseButton> b = p_event;  	if (b.is_valid()) { +		if (ime_text.length() != 0) { +			// Ignore mouse clicks in IME input mode. +			return; +		}  		if (b->is_pressed() && b->get_button_index() == BUTTON_RIGHT && context_menu_enabled) {  			menu->set_position(get_screen_transform().xform(get_local_mouse_position()));  			menu->set_size(Vector2(1, 1)); @@ -200,6 +202,18 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  			bool handled = true;  			switch (code) { +				case (KEY_QUOTELEFT): { // Swap current input direction (primary cursor) + +					if (input_direction == TEXT_DIRECTION_LTR) { +						input_direction = TEXT_DIRECTION_RTL; +					} else { +						input_direction = TEXT_DIRECTION_LTR; +					} +					set_cursor_position(get_cursor_position()); +					update(); + +				} break; +  				case (KEY_X): { // CUT.  					if (editable) { @@ -237,7 +251,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  					if (editable) {  						deselect();  						text = text.substr(cursor_pos, text.length() - cursor_pos); -						update_cached_width(); +						_shape();  						set_cursor_position(0);  						_text_changed();  					} @@ -335,17 +349,13 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  					} else if (k->get_command()) {  #endif  						int cc = cursor_pos; -						bool prev_char = false; -						while (cc > 0) { -							bool ischar = _is_text_char(text[cc - 1]); - -							if (prev_char && !ischar) { +						Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); +						for (int i = words.size() - 1; i >= 0; i--) { +							if (words[i].x < cc) { +								cc = words[i].x;  								break;  							} - -							prev_char = ischar; -							cc--;  						}  						delete_text(cc, cursor_pos); @@ -390,24 +400,24 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  						break;  					} else if (k->get_command()) {  #endif -						bool prev_char = false;  						int cc = cursor_pos; -						while (cc > 0) { -							bool ischar = _is_text_char(text[cc - 1]); - -							if (prev_char && !ischar) { +						Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); +						for (int i = words.size() - 1; i >= 0; i--) { +							if (words[i].x < cc) { +								cc = words[i].x;  								break;  							} - -							prev_char = ischar; -							cc--;  						}  						set_cursor_position(cc);  					} else { -						set_cursor_position(get_cursor_position() - 1); +						if (mid_grapheme_caret_enabled) { +							set_cursor_position(get_cursor_position() - 1); +						} else { +							set_cursor_position(TS->shaped_text_prev_grapheme_pos(text_rid, get_cursor_position())); +						}  					}  					shift_selection_check_post(k->get_shift()); @@ -446,24 +456,24 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  						break;  					} else if (k->get_command()) {  #endif -						bool prev_char = false;  						int cc = cursor_pos; -						while (cc < text.length()) { -							bool ischar = _is_text_char(text[cc]); - -							if (prev_char && !ischar) { +						Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); +						for (int i = 0; i < words.size(); i++) { +							if (words[i].y > cc) { +								cc = words[i].y;  								break;  							} - -							prev_char = ischar; -							cc++;  						}  						set_cursor_position(cc);  					} else { -						set_cursor_position(get_cursor_position() + 1); +						if (mid_grapheme_caret_enabled) { +							set_cursor_position(get_cursor_position() + 1); +						} else { +							set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, get_cursor_position())); +						}  					}  					shift_selection_check_post(k->get_shift()); @@ -516,23 +526,25 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  #endif  						int cc = cursor_pos; -						bool prev_char = false; - -						while (cc < text.length()) { -							bool ischar = _is_text_char(text[cc]); - -							if (prev_char && !ischar) { +						Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); +						for (int i = 0; i < words.size(); i++) { +							if (words[i].y > cc) { +								cc = words[i].y;  								break;  							} -							prev_char = ischar; -							cc++;  						}  						delete_text(cursor_pos, cc);  					} else { -						set_cursor_position(cursor_pos + 1); -						delete_char(); +						if (mid_grapheme_caret_enabled) { +							set_cursor_position(cursor_pos + 1); +							delete_char(); +						} else { +							int cc = cursor_pos; +							set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, cursor_pos)); +							delete_text(cc, cursor_pos); +						}  					}  				} break; @@ -562,10 +574,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  				} break;  				case KEY_MENU: {  					if (context_menu_enabled) { -						Point2 pos = Point2(get_cursor_pixel_pos(), (get_size().y + get_theme_font("font")->get_height()) / 2); +						Point2 pos = Point2(get_cursor_pixel_pos().x, (get_size().y + get_theme_font("font")->get_height(get_theme_font_size("font_size"))) / 2);  						menu->set_position(get_global_transform().xform(pos));  						menu->set_size(Vector2(1, 1)); -						//						menu->set_scale(get_global_transform().get_scale()); +						//menu->set_scale(get_global_transform().get_scale());  						menu->popup();  						menu->grab_focus();  					} @@ -605,7 +617,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) {  void LineEdit::set_align(Align p_align) {  	ERR_FAIL_INDEX((int)p_align, 4); -	align = p_align; +	if (align != p_align) { +		align = p_align; +		_shape(); +	}  	update();  } @@ -634,14 +649,8 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) {  		set_cursor_at_pixel_pos(p_point.x);  		int selected = selection.end - selection.begin; -		Ref<Font> font = get_theme_font("font"); -		if (font != nullptr) { -			for (int i = selection.begin; i < selection.end; i++) { -				cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; -			} -		} -  		text.erase(selection.begin, selected); +		_shape();  		append_at_cursor(p_data);  		selection.begin = cursor_pos - selected; @@ -680,13 +689,18 @@ void LineEdit::_notification(int p_what) {  		} break;  #endif  		case NOTIFICATION_RESIZED: { +			_fit_to_width();  			scroll_offset = 0;  			set_cursor_position(get_cursor_position()); - +		} break; +		case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: +		case NOTIFICATION_THEME_CHANGED: { +			_shape(); +			update();  		} break;  		case NOTIFICATION_TRANSLATION_CHANGED: {  			placeholder_translated = tr(placeholder); -			update_placeholder_width(); +			_shape();  			update();  		} break;  		case NOTIFICATION_WM_WINDOW_FOCUS_IN: { @@ -705,6 +719,7 @@ void LineEdit::_notification(int p_what) {  			}  			int width, height; +			bool rtl = is_layout_rtl();  			Size2 size = get_size();  			width = size.width; @@ -718,9 +733,6 @@ void LineEdit::_notification(int p_what) {  				draw_caret = false;  			} -			Ref<Font> font = get_theme_font("font"); -			int font_size = get_theme_font_size("font_size"); -  			style->draw(ci, Rect2(Point2(), size));  			if (has_focus()) { @@ -729,39 +741,44 @@ void LineEdit::_notification(int p_what) {  			int x_ofs = 0;  			bool using_placeholder = text.empty() && ime_text.empty(); -			int cached_text_width = using_placeholder ? cached_placeholder_width : cached_width; +			float text_width = TS->shaped_text_get_size(text_rid).x; +			float text_height = TS->shaped_text_get_size(text_rid).y;  			switch (align) {  				case ALIGN_FILL:  				case ALIGN_LEFT: { -					x_ofs = style->get_offset().x; +					if (rtl) { +						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - style->get_margin(MARGIN_RIGHT) - (text_width))); +					} else { +						x_ofs = style->get_offset().x; +					}  				} break;  				case ALIGN_CENTER: {  					if (scroll_offset != 0) {  						x_ofs = style->get_offset().x;  					} else { -						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - (cached_text_width)) / 2); +						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - (text_width)) / 2);  					}  				} break;  				case ALIGN_RIGHT: { -					x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_text_width))); +					if (rtl) { +						x_ofs = style->get_offset().x; +					} else { +						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - style->get_margin(MARGIN_RIGHT) - (text_width))); +					}  				} break;  			}  			int ofs_max = width - style->get_margin(MARGIN_RIGHT); -			int char_ofs = scroll_offset;  			int y_area = height - style->get_minimum_size().height; -			int y_ofs = style->get_offset().y + (y_area - font->get_height()) / 2; - -			int font_ascent = font->get_ascent(); +			int y_ofs = style->get_offset().y + (y_area - text_height) / 2;  			Color selection_color = get_theme_color("selection_color");  			Color font_color = is_editable() ? get_theme_color("font_color") : get_theme_color("font_color_uneditable");  			Color font_color_selected = get_theme_color("font_color_selected");  			Color cursor_color = get_theme_color("cursor_color"); -			const String &t = using_placeholder ? placeholder_translated : text;  			// Draw placeholder color.  			if (using_placeholder) {  				font_color.a *= placeholder_alpha; @@ -783,7 +800,7 @@ void LineEdit::_notification(int p_what) {  				if (align == ALIGN_CENTER) {  					if (scroll_offset == 0) { -						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - cached_text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2); +						x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2);  					}  				} else {  					x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT)); @@ -792,139 +809,133 @@ void LineEdit::_notification(int p_what) {  				ofs_max -= r_icon->get_width();  			} -			int caret_height = font->get_height() > y_area ? y_area : font->get_height(); -			//FontDrawer drawer(font, Color(1, 1, 1)); -			while (true) { -				//TODO replace with TS +#ifdef TOOLS_ENABLED +			int caret_width = Math::round(EDSCALE); +#else +			int caret_width = 1; +#endif -				// End of string, break. -				if (char_ofs >= t.length()) { -					break; +			// Draw selections rects. +			Vector2 ofs = Point2(x_ofs + scroll_offset, y_ofs); +			if (selection.enabled) { +				Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, selection.begin, selection.end); +				for (int i = 0; i < sel.size(); i++) { +					Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); +					if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { +						continue; +					} +					if (rect.position.x < x_ofs) { +						rect.size.x -= (x_ofs - rect.position.x); +						rect.position.x = x_ofs; +					} else if (rect.position.x + rect.size.x > ofs_max) { +						rect.size.x = ofs_max - rect.position.x; +					} +					RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_color);  				} - -				if (char_ofs == cursor_pos) { -					if (ime_text.length() > 0) { -						int ofs = 0; -						while (true) { -							if (ofs >= ime_text.length()) { -								break; -							} - -							char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; -							char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; -							int im_char_width = font->get_char_size(cchar, next).width; - -							if ((x_ofs + im_char_width) > ofs_max) { -								break; -							} - -							bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; -							if (selected) { -								RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); -							} else { -								RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); -							} - -							font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_size, font_color); - -							x_ofs += im_char_width; -							ofs++; +			} +			const Vector<TextServer::Glyph> glyphs = TS->shaped_text_get_glyphs(text_rid); + +			// Draw text. +			ofs.y += TS->shaped_text_get_ascent(text_rid); +			for (int i = 0; i < glyphs.size(); i++) { +				bool selected = selection.enabled && glyphs[i].start >= selection.begin && glyphs[i].end <= selection.end; +				for (int j = 0; j < glyphs[i].repeat; j++) { +					if (ceil(ofs.x) >= x_ofs && floor(ofs.x + glyphs[i].advance) <= ofs_max) { +						if (glyphs[i].font_rid != RID()) { +							TS->font_draw_glyph(glyphs[i].font_rid, ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_color_selected : font_color); +						} else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { +							TS->draw_hex_code_box(ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_color_selected : font_color);  						}  					} +					ofs.x += glyphs[i].advance;  				} - -				char32_t cchar = (pass && !text.empty()) ? secret_character[0] : t[char_ofs]; -				char32_t next = (pass && !text.empty()) ? secret_character[0] : t[char_ofs + 1]; -				int char_width = font->get_char_size(cchar, next).width; - -				// End of widget, break. -				if ((x_ofs + char_width) > ofs_max) { +				if (ofs.x >= ofs_max) {  					break;  				} - -				bool selected = selection.enabled && char_ofs >= selection.begin && char_ofs < selection.end; - -				if (selected) { -					RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(char_width, caret_height)), selection_color); -				} - -				int yofs = y_ofs + (caret_height - font->get_height()) / 2; -				font->draw_char(ci, Point2(x_ofs, yofs + font_ascent), cchar, next, font_size, selected ? font_color_selected : font_color); - -				if (char_ofs == cursor_pos && draw_caret && !using_placeholder) { -					if (ime_text.length() == 0) { -#ifdef TOOLS_ENABLED -						RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else -						RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif -					} -				} - -				x_ofs += char_width; -				char_ofs++;  			} -			if (char_ofs == cursor_pos) { -				if (ime_text.length() > 0) { -					int ofs = 0; -					while (true) { -						if (ofs >= ime_text.length()) { -							break; +			// Draw carets. +			ofs.x = x_ofs + scroll_offset; +			if (draw_caret) { +				if (ime_text.length() == 0) { +					// Normal caret. +					Rect2 l_caret, t_caret; +					TextServer::Direction l_dir, t_dir; +					TS->shaped_text_get_carets(text_rid, cursor_pos, l_caret, l_dir, t_caret, t_dir); + +					if (l_caret == Rect2() && t_caret == Rect2()) { +						// No carets, add one at the start. +						int h = get_theme_font("font")->get_height(get_theme_font_size("font_size")); +						int y = style->get_offset().y + (y_area - h) / 2; +						if (rtl) { +							l_dir = TextServer::DIRECTION_RTL; +							l_caret = Rect2(Vector2(ofs_max, y), Size2(caret_width, h)); +						} else { +							l_dir = TextServer::DIRECTION_LTR; +							l_caret = Rect2(Vector2(x_ofs, y), Size2(caret_width, h));  						} - -						char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; -						char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; -						int im_char_width = font->get_char_size(cchar, next).width; - -						if ((x_ofs + im_char_width) > ofs_max) { -							break; +						RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, cursor_color); +					} else { +						if (l_caret != Rect2() && l_dir == TextServer::DIRECTION_AUTO) { +							// Draw extra marker on top of mid caret. +							Rect2 trect = Rect2(l_caret.position.x - 3 * caret_width, l_caret.position.y, 6 * caret_width, caret_width); +							trect.position += ofs; +							RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, cursor_color);  						} -						bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; -						if (selected) { -							RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); -						} else { -							RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); -						} +						l_caret.position += ofs; +						l_caret.size.x = caret_width; +						RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, cursor_color); -						font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_size, font_color); +						t_caret.position += ofs; +						t_caret.size.x = caret_width; -						x_ofs += im_char_width; -						ofs++; +						RenderingServer::get_singleton()->canvas_item_add_rect(ci, t_caret, cursor_color);  					} -				} -			} - -			if ((char_ofs == cursor_pos || using_placeholder) && draw_caret) { // May be at the end, or placeholder. -				if (ime_text.length() == 0) { -					int caret_x_ofs = x_ofs; -					if (using_placeholder) { -						switch (align) { -							case ALIGN_LEFT: -							case ALIGN_FILL: { -								caret_x_ofs = style->get_offset().x; -							} break; -							case ALIGN_CENTER: { -								caret_x_ofs = ofs_max / 2; -							} break; -							case ALIGN_RIGHT: { -								caret_x_ofs = ofs_max; -							} break; +				} else { +					{ +						// IME intermidiet text range. +						Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos, cursor_pos + ime_text.length()); +						for (int i = 0; i < sel.size(); i++) { +							Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); +							if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { +								continue; +							} +							if (rect.position.x < x_ofs) { +								rect.size.x -= (x_ofs - rect.position.x); +								rect.position.x = x_ofs; +							} else if (rect.position.x + rect.size.x > ofs_max) { +								rect.size.x = ofs_max - rect.position.x; +							} +							rect.size.y = caret_width; +							RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color); +						} +					} +					{ +						// IME caret. +						Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos + ime_selection.x, cursor_pos + ime_selection.x + ime_selection.y); +						for (int i = 0; i < sel.size(); i++) { +							Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); +							if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { +								continue; +							} +							if (rect.position.x < x_ofs) { +								rect.size.x -= (x_ofs - rect.position.x); +								rect.position.x = x_ofs; +							} else if (rect.position.x + rect.size.x > ofs_max) { +								rect.size.x = ofs_max - rect.position.x; +							} +							rect.size.y = caret_width * 3; +							RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color);  						}  					} -#ifdef TOOLS_ENABLED -					RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else -					RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif  				}  			}  			if (has_focus()) {  				if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) {  					DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); -					DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + caret_height), get_viewport()->get_window_id()); +					DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + TS->shaped_text_get_size(text_rid).y), get_viewport()->get_window_id());  				}  			}  		} break; @@ -965,6 +976,8 @@ void LineEdit::_notification(int p_what) {  			}  			ime_text = "";  			ime_selection = Point2(); +			_shape(); +			set_cursor_position(cursor_pos); // Update scroll_offset  			if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) {  				DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -975,6 +988,9 @@ void LineEdit::_notification(int p_what) {  			if (has_focus()) {  				ime_text = DisplayServer::get_singleton()->ime_get_text();  				ime_selection = DisplayServer::get_singleton()->ime_get_selection(); +				_shape(); +				set_cursor_position(cursor_pos); // Update scroll_offset +  				update();  			}  		} break; @@ -1026,14 +1042,10 @@ void LineEdit::undo() {  	undo_stack_pos = undo_stack_pos->prev();  	TextOperation op = undo_stack_pos->get();  	text = op.text; -	cached_width = op.cached_width;  	scroll_offset = op.scroll_offset;  	set_cursor_position(op.cursor_pos); -	if (expand_to_text_length) { -		minimum_size_changed(); -	} - +	_shape();  	_emit_text_change();  } @@ -1047,14 +1059,10 @@ void LineEdit::redo() {  	undo_stack_pos = undo_stack_pos->next();  	TextOperation op = undo_stack_pos->get();  	text = op.text; -	cached_width = op.cached_width;  	scroll_offset = op.scroll_offset;  	set_cursor_position(op.cursor_pos); -	if (expand_to_text_length) { -		minimum_size_changed(); -	} - +	_shape();  	_emit_text_change();  } @@ -1074,98 +1082,138 @@ void LineEdit::shift_selection_check_post(bool p_shift) {  }  void LineEdit::set_cursor_at_pixel_pos(int p_x) { -	Ref<Font> font = get_theme_font("font"); -	int ofs = scroll_offset;  	Ref<StyleBox> style = get_theme_stylebox("normal"); -	int pixel_ofs = 0; -	Size2 size = get_size(); -	bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; -	int r_icon_width = Control::get_theme_icon("clear")->get_width(); +	bool rtl = is_layout_rtl(); +	int x_ofs = 0; +	float text_width = TS->shaped_text_get_size(text_rid).x;  	switch (align) {  		case ALIGN_FILL:  		case ALIGN_LEFT: { -			pixel_ofs = int(style->get_offset().x); +			if (rtl) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width))); +			} else { +				x_ofs = style->get_offset().x; +			}  		} break;  		case ALIGN_CENTER: {  			if (scroll_offset != 0) { -				pixel_ofs = int(style->get_offset().x); +				x_ofs = style->get_offset().x;  			} else { -				pixel_ofs = int(size.width - (cached_width)) / 2; -			} - -			if (display_clear_icon) { -				pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - (text_width)) / 2);  			}  		} break;  		case ALIGN_RIGHT: { -			pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - -			if (display_clear_icon) { -				pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); +			if (rtl) { +				x_ofs = style->get_offset().x; +			} else { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width)));  			}  		} break;  	} -	while (ofs < text.length()) { -		int char_w = 0; -		if (font != nullptr) { -			char_w = font->get_char_size(pass ? secret_character[0] : text[ofs]).width; -		} -		pixel_ofs += char_w; - -		if (pixel_ofs > p_x) { // Found what we look for. -			break; +	bool using_placeholder = text.empty() && ime_text.empty(); +	bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; +	if (right_icon.is_valid() || display_clear_icon) { +		Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; +		if (align == ALIGN_CENTER) { +			if (scroll_offset == 0) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2); +			} +		} else { +			x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT));  		} - -		ofs++;  	} +	int ofs = TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset);  	set_cursor_position(ofs);  } -int LineEdit::get_cursor_pixel_pos() { -	Ref<Font> font = get_theme_font("font"); -	int ofs = scroll_offset; +Vector2i LineEdit::get_cursor_pixel_pos() {  	Ref<StyleBox> style = get_theme_stylebox("normal"); -	int pixel_ofs = 0; -	Size2 size = get_size(); -	bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; -	int r_icon_width = Control::get_theme_icon("clear")->get_width(); +	bool rtl = is_layout_rtl(); +	int x_ofs = 0; +	float text_width = TS->shaped_text_get_size(text_rid).x;  	switch (align) {  		case ALIGN_FILL:  		case ALIGN_LEFT: { -			pixel_ofs = int(style->get_offset().x); +			if (rtl) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width))); +			} else { +				x_ofs = style->get_offset().x; +			}  		} break;  		case ALIGN_CENTER: {  			if (scroll_offset != 0) { -				pixel_ofs = int(style->get_offset().x); +				x_ofs = style->get_offset().x;  			} else { -				pixel_ofs = int(size.width - (cached_width)) / 2; -			} - -			if (display_clear_icon) { -				pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - (text_width)) / 2);  			}  		} break;  		case ALIGN_RIGHT: { -			pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - -			if (display_clear_icon) { -				pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); +			if (rtl) { +				x_ofs = style->get_offset().x; +			} else { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width)));  			}  		} break;  	} -	while (ofs < cursor_pos) { -		if (font != nullptr) { -			pixel_ofs += font->get_char_size(pass ? secret_character[0] : text[ofs]).width; +	bool using_placeholder = text.empty() && ime_text.empty(); +	bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; +	if (right_icon.is_valid() || display_clear_icon) { +		Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; +		if (align == ALIGN_CENTER) { +			if (scroll_offset == 0) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2); +			} +		} else { +			x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT)); +		} +	} + +	Vector2i ret; +	Rect2 l_caret, t_caret; +	TextServer::Direction l_dir, t_dir; +	// Get position of the start of caret. +	if (ime_text.length() != 0 && ime_selection.x != 0) { +		TS->shaped_text_get_carets(text_rid, cursor_pos + ime_selection.x, l_caret, l_dir, t_caret, t_dir); +	} else { +		TS->shaped_text_get_carets(text_rid, cursor_pos, l_caret, l_dir, t_caret, t_dir); +	} + +	if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { +		ret.x = x_ofs + l_caret.position.x + scroll_offset; +	} else { +		ret.x = x_ofs + t_caret.position.x + scroll_offset; +	} + +	// Get position of the end of caret. +	if (ime_text.length() != 0) { +		if (ime_selection.y != 0) { +			TS->shaped_text_get_carets(text_rid, cursor_pos + ime_selection.x + ime_selection.y, l_caret, l_dir, t_caret, t_dir); +		} else { +			TS->shaped_text_get_carets(text_rid, cursor_pos + ime_text.size(), l_caret, l_dir, t_caret, t_dir); +		} +		if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { +			ret.y = x_ofs + l_caret.position.x + scroll_offset; +		} else { +			ret.y = x_ofs + t_caret.position.x + scroll_offset;  		} -		ofs++; +	} else { +		ret.y = ret.x;  	} -	return pixel_ofs; +	return ret; +} + +void LineEdit::set_mid_grapheme_caret_enabled(const bool p_enabled) { +	mid_grapheme_caret_enabled = p_enabled; +} + +bool LineEdit::get_mid_grapheme_caret_enabled() const { +	return mid_grapheme_caret_enabled;  }  bool LineEdit::cursor_get_blink_enabled() const { @@ -1230,49 +1278,26 @@ void LineEdit::delete_char() {  		return;  	} -	Ref<Font> font = get_theme_font("font"); -	if (font != nullptr) { -		cached_width -= font->get_char_size(pass ? secret_character[0] : text[cursor_pos - 1]).width; -	} -  	text.erase(cursor_pos - 1, 1); +	_shape();  	set_cursor_position(get_cursor_position() - 1); -	if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { -		scroll_offset = CLAMP(scroll_offset - 1, 0, MAX(text.length() - 1, 0)); -	} -  	_text_changed();  }  void LineEdit::delete_text(int p_from_column, int p_to_column) {  	ERR_FAIL_COND_MSG(p_from_column < 0 || p_from_column > p_to_column || p_to_column > text.length(),  			vformat("Positional parameters (from: %d, to: %d) are inverted or outside the text length (%d).", p_from_column, p_to_column, text.length())); -	if (text.size() > 0) { -		Ref<Font> font = get_theme_font("font"); -		if (font != nullptr) { -			for (int i = p_from_column; i < p_to_column; i++) { -				cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; -			} -		} -	} else { -		cached_width = 0; -	}  	text.erase(p_from_column, p_to_column - p_from_column); +	_shape(); +  	cursor_pos -= CLAMP(cursor_pos - p_from_column, 0, p_to_column - p_from_column);  	if (cursor_pos >= text.length()) {  		cursor_pos = text.length();  	} -	if (scroll_offset > cursor_pos) { -		scroll_offset = cursor_pos; -	} - -	if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { -		scroll_offset = CLAMP(scroll_offset - (p_to_column - p_from_column), 0, MAX(text.length() - 1, 0)); -	}  	if (!text_changed_dirty) {  		if (is_inside_tree()) { @@ -1286,15 +1311,102 @@ void LineEdit::set_text(String p_text) {  	clear_internal();  	append_at_cursor(p_text); -	if (expand_to_text_length) { -		minimum_size_changed(); -	} -  	update();  	cursor_pos = 0;  	scroll_offset = 0;  } +void LineEdit::set_text_direction(Control::TextDirection p_text_direction) { +	ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); +	if (text_direction != p_text_direction) { +		text_direction = p_text_direction; +		if (text_direction != TEXT_DIRECTION_AUTO && text_direction != TEXT_DIRECTION_INHERITED) { +			input_direction = text_direction; +		} +		_shape(); + +		menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); +		menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); +		menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); +		menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); +		update(); +	} +} + +Control::TextDirection LineEdit::get_text_direction() const { +	return text_direction; +} + +void LineEdit::clear_opentype_features() { +	opentype_features.clear(); +	_shape(); +	update(); +} + +void LineEdit::set_opentype_feature(const String &p_name, int p_value) { +	int32_t tag = TS->name_to_tag(p_name); +	if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { +		opentype_features[tag] = p_value; +		_shape(); +		update(); +	} +} + +int LineEdit::get_opentype_feature(const String &p_name) const { +	int32_t tag = TS->name_to_tag(p_name); +	if (!opentype_features.has(tag)) { +		return -1; +	} +	return opentype_features[tag]; +} + +void LineEdit::set_language(const String &p_language) { +	if (language != p_language) { +		language = p_language; +		_shape(); +		update(); +	} +} + +String LineEdit::get_language() const { +	return language; +} + +void LineEdit::set_draw_control_chars(bool p_draw_control_chars) { +	if (draw_control_chars != p_draw_control_chars) { +		draw_control_chars = p_draw_control_chars; +		menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); +		_shape(); +		update(); +	} +} + +bool LineEdit::get_draw_control_chars() const { +	return draw_control_chars; +} + +void LineEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +	if (st_parser != p_parser) { +		st_parser = p_parser; +		_shape(); +		update(); +	} +} + +Control::StructuredTextParser LineEdit::get_structured_text_bidi_override() const { +	return st_parser; +} + +void LineEdit::set_structured_text_bidi_override_options(Array p_args) { +	st_args = p_args; +	_shape(); +	update(); +} + +Array LineEdit::get_structured_text_bidi_override_options() const { +	return st_args; +} +  void LineEdit::clear() {  	clear_internal();  	_text_changed(); @@ -1307,7 +1419,7 @@ String LineEdit::get_text() const {  void LineEdit::set_placeholder(String p_text) {  	placeholder = p_text;  	placeholder_translated = tr(placeholder); -	update_placeholder_width(); +	_shape();  	update();  } @@ -1335,57 +1447,68 @@ void LineEdit::set_cursor_position(int p_pos) {  	cursor_pos = p_pos; +	// Fit to window. +  	if (!is_inside_tree()) { -		scroll_offset = cursor_pos; +		scroll_offset = 0;  		return;  	}  	Ref<StyleBox> style = get_theme_stylebox("normal"); -	Ref<Font> font = get_theme_font("font"); +	bool rtl = is_layout_rtl(); -	if (cursor_pos <= scroll_offset) { -		// Adjust window if cursor goes too much to the left. -		set_scroll_offset(MAX(0, cursor_pos - 1)); -	} else { -		// Adjust window if cursor goes too much to the right. -		int window_width = get_size().width - style->get_minimum_size().width; -		bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; -		if (right_icon.is_valid() || display_clear_icon) { -			Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; -			window_width -= r_icon->get_width(); -		} - -		if (window_width < 0) { -			return; -		} -		int wp = scroll_offset; - -		if (font.is_valid()) { -			int accum_width = 0; - -			for (int i = cursor_pos; i >= scroll_offset; i--) { -				if (i >= text.length()) { -					// Do not do this, because if the cursor is at the end, its just fine that it takes no space. -					// accum_width = font->get_char_size(' ').width; -				} else { -					if (pass) { -						accum_width += font->get_char_size(secret_character[0], i + 1 < text.length() ? secret_character[0] : 0).width; -					} else { -						accum_width += font->get_char_size(text[i], i + 1 < text.length() ? text[i + 1] : 0).width; // Anything should do. -					} -				} -				if (accum_width > window_width) { -					break; -				} +	int x_ofs = 0; +	float text_width = TS->shaped_text_get_size(text_rid).x; +	switch (align) { +		case ALIGN_FILL: +		case ALIGN_LEFT: { +			if (rtl) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width))); +			} else { +				x_ofs = style->get_offset().x; +			} +		} break; +		case ALIGN_CENTER: { +			if (scroll_offset != 0) { +				x_ofs = style->get_offset().x; +			} else { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - (text_width)) / 2); +			} +		} break; +		case ALIGN_RIGHT: { +			if (rtl) { +				x_ofs = style->get_offset().x; +			} else { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - style->get_margin(MARGIN_RIGHT) - (text_width))); +			} +		} break; +	} -				wp = i; +	int ofs_max = get_size().width - style->get_margin(MARGIN_RIGHT); +	bool using_placeholder = text.empty() && ime_text.empty(); +	bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; +	if (right_icon.is_valid() || display_clear_icon) { +		Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; +		if (align == ALIGN_CENTER) { +			if (scroll_offset == 0) { +				x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2);  			} +		} else { +			x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT));  		} +		ofs_max -= r_icon->get_width(); +	} -		if (wp != scroll_offset) { -			set_scroll_offset(wp); -		} +	// Note: Use too coordinates to fit IME input range. +	Vector2i primary_catret_offset = get_cursor_pixel_pos(); + +	if (MIN(primary_catret_offset.x, primary_catret_offset.y) <= x_ofs) { +		scroll_offset += (x_ofs - MIN(primary_catret_offset.x, primary_catret_offset.y)); +	} else if (MAX(primary_catret_offset.x, primary_catret_offset.y) >= ofs_max) { +		scroll_offset += (ofs_max - MAX(primary_catret_offset.x, primary_catret_offset.y));  	} +	scroll_offset = MIN(0, scroll_offset); +  	update();  } @@ -1409,7 +1532,11 @@ void LineEdit::append_at_cursor(String p_text) {  		String pre = text.substr(0, cursor_pos);  		String post = text.substr(cursor_pos, text.length() - cursor_pos);  		text = pre + p_text + post; -		update_cached_width(); +		_shape(); +		TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text_rid, cursor_pos, cursor_pos + p_text.length()); +		if (dir != TextServer::DIRECTION_AUTO) { +			input_direction = (TextDirection)dir; +		}  		set_cursor_position(cursor_pos + p_text.length());  	} else {  		emit_signal("text_change_rejected"); @@ -1419,11 +1546,11 @@ void LineEdit::append_at_cursor(String p_text) {  void LineEdit::clear_internal() {  	deselect();  	_clear_undo_stack(); -	cached_width = 0;  	cursor_pos = 0;  	scroll_offset = 0;  	undo_text = "";  	text = ""; +	_shape();  	update();  } @@ -1435,24 +1562,23 @@ Size2 LineEdit::get_minimum_size() const {  	Size2 min_size;  	// Minimum size of text. -	int space_size = font->get_char_size(' ').x; +	int space_size = font->get_char_size('m', 0, font_size).x;  	min_size.width = get_theme_constant("minimum_spaces") * space_size;  	if (expand_to_text_length) {  		// Add a space because some fonts are too exact, and because cursor needs a bit more when at the end. -		min_size.width = MAX(min_size.width, font->get_string_size(text, font_size).x + space_size); +		min_size.width = MAX(min_size.width, full_width + space_size);  	} -	min_size.height = font->get_height(); +	min_size.height = MAX(TS->shaped_text_get_size(text_rid).y, font->get_height(font_size));  	// Take icons into account. -	if (!text.empty() && is_editable() && clear_button_enabled) { -		min_size.width = MAX(min_size.width, Control::get_theme_icon("clear")->get_width()); -		min_size.height = MAX(min_size.height, Control::get_theme_icon("clear")->get_height()); -	} -	if (right_icon.is_valid()) { -		min_size.width = MAX(min_size.width, right_icon->get_width()); -		min_size.height = MAX(min_size.height, right_icon->get_height()); +	bool using_placeholder = text.empty() && ime_text.empty(); +	bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; +	if (right_icon.is_valid() || display_clear_icon) { +		Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; +		min_size.width += r_icon->get_width(); +		min_size.height = MAX(min_size.height, r_icon->get_height());  	}  	return style->get_minimum_size() + min_size; @@ -1535,8 +1661,10 @@ bool LineEdit::is_editable() const {  }  void LineEdit::set_secret(bool p_secret) { -	pass = p_secret; -	update_cached_width(); +	if (pass != p_secret) { +		pass = p_secret; +		_shape(); +	}  	update();  } @@ -1549,8 +1677,10 @@ void LineEdit::set_secret_character(const String &p_string) {  	// It also wouldn't make sense to use multiple characters as the secret character.  	ERR_FAIL_COND_MSG(p_string.length() != 1, "Secret character must be exactly one character long (" + itos(p_string.length()) + " characters given)."); -	secret_character = p_string; -	update_cached_width(); +	if (secret_character != p_string) { +		secret_character = p_string; +		_shape(); +	}  	update();  } @@ -1627,6 +1757,101 @@ void LineEdit::menu_option(int p_option) {  			if (editable) {  				redo();  			} +		} break; +		case MENU_DIR_INHERITED: { +			set_text_direction(TEXT_DIRECTION_INHERITED); +		} break; +		case MENU_DIR_AUTO: { +			set_text_direction(TEXT_DIRECTION_AUTO); +		} break; +		case MENU_DIR_LTR: { +			set_text_direction(TEXT_DIRECTION_LTR); +		} break; +		case MENU_DIR_RTL: { +			set_text_direction(TEXT_DIRECTION_RTL); +		} break; +		case MENU_DISPLAY_UCC: { +			set_draw_control_chars(!get_draw_control_chars()); +		} break; +		case MENU_INSERT_LRM: { +			if (editable) { +				append_at_cursor(String::chr(0x200E)); +			} +		} break; +		case MENU_INSERT_RLM: { +			if (editable) { +				append_at_cursor(String::chr(0x200F)); +			} +		} break; +		case MENU_INSERT_LRE: { +			if (editable) { +				append_at_cursor(String::chr(0x202A)); +			} +		} break; +		case MENU_INSERT_RLE: { +			if (editable) { +				append_at_cursor(String::chr(0x202B)); +			} +		} break; +		case MENU_INSERT_LRO: { +			if (editable) { +				append_at_cursor(String::chr(0x202D)); +			} +		} break; +		case MENU_INSERT_RLO: { +			if (editable) { +				append_at_cursor(String::chr(0x202E)); +			} +		} break; +		case MENU_INSERT_PDF: { +			if (editable) { +				append_at_cursor(String::chr(0x202C)); +			} +		} break; +		case MENU_INSERT_ALM: { +			if (editable) { +				append_at_cursor(String::chr(0x061C)); +			} +		} break; +		case MENU_INSERT_LRI: { +			if (editable) { +				append_at_cursor(String::chr(0x2066)); +			} +		} break; +		case MENU_INSERT_RLI: { +			if (editable) { +				append_at_cursor(String::chr(0x2067)); +			} +		} break; +		case MENU_INSERT_FSI: { +			if (editable) { +				append_at_cursor(String::chr(0x2068)); +			} +		} break; +		case MENU_INSERT_PDI: { +			if (editable) { +				append_at_cursor(String::chr(0x2069)); +			} +		} break; +		case MENU_INSERT_ZWJ: { +			if (editable) { +				append_at_cursor(String::chr(0x200D)); +			} +		} break; +		case MENU_INSERT_ZWNJ: { +			if (editable) { +				append_at_cursor(String::chr(0x200C)); +			} +		} break; +		case MENU_INSERT_WJ: { +			if (editable) { +				append_at_cursor(String::chr(0x2060)); +			} +		} break; +		case MENU_INSERT_SHY: { +			if (editable) { +				append_at_cursor(String::chr(0x00AD)); +			}  		}  	}  } @@ -1653,7 +1878,7 @@ void LineEdit::_editor_settings_changed() {  void LineEdit::set_expand_to_text_length(bool p_enabled) {  	expand_to_text_length = p_enabled;  	minimum_size_changed(); -	set_scroll_offset(0); +	set_cursor_position(cursor_pos);  }  bool LineEdit::get_expand_to_text_length() const { @@ -1665,6 +1890,7 @@ void LineEdit::set_clear_button_enabled(bool p_enabled) {  		return;  	}  	clear_button_enabled = p_enabled; +	_fit_to_width();  	minimum_size_changed();  	update();  } @@ -1710,6 +1936,7 @@ void LineEdit::set_right_icon(const Ref<Texture2D> &p_icon) {  		return;  	}  	right_icon = p_icon; +	_fit_to_width();  	minimum_size_changed();  	update();  } @@ -1719,10 +1946,6 @@ Ref<Texture2D> LineEdit::get_right_icon() {  }  void LineEdit::_text_changed() { -	if (expand_to_text_length) { -		minimum_size_changed(); -	} -  	_emit_text_change();  	_clear_redo();  } @@ -1733,24 +1956,55 @@ void LineEdit::_emit_text_change() {  	text_changed_dirty = false;  } -void LineEdit::update_cached_width() { -	Ref<Font> font = get_theme_font("font"); -	cached_width = 0; -	if (font != nullptr) { -		String text = get_text(); -		for (int i = 0; i < text.length(); i++) { -			cached_width += font->get_char_size(pass ? secret_character[0] : text[i]).width; +void LineEdit::_shape() { +	Size2 old_size = TS->shaped_text_get_size(text_rid); +	TS->shaped_text_clear(text_rid); + +	String t; +	if (text.length() == 0) { +		t = placeholder_translated; +	} else if (pass) { +		t = secret_character.repeat(text.length() + ime_text.length()); +	} else { +		if (ime_text.length() > 0) { +			t = text.substr(0, cursor_pos) + ime_text + text.substr(cursor_pos, text.length()); +		} else { +			t = text;  		}  	} +	if (text_direction == Control::TEXT_DIRECTION_INHERITED) { +		TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); +	} else { +		TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); +	} +	TS->shaped_text_set_preserve_control(text_rid, draw_control_chars); + +	const Ref<Font> &font = get_theme_font("font"); +	int font_size = get_theme_font_size("font_size"); +	TS->shaped_text_add_string(text_rid, t, font->get_rids(), font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +	TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, t)); + +	full_width = TS->shaped_text_get_size(text_rid).x; +	_fit_to_width(); + +	Size2 size = TS->shaped_text_get_size(text_rid); + +	if ((expand_to_text_length && old_size.x != size.x) || (old_size.y != size.y)) { +		minimum_size_changed(); +	}  } -void LineEdit::update_placeholder_width() { -	Ref<Font> font = get_theme_font("font"); -	cached_placeholder_width = 0; -	if (font != nullptr) { -		for (int i = 0; i < placeholder_translated.length(); i++) { -			cached_placeholder_width += font->get_char_size(placeholder_translated[i]).width; +void LineEdit::_fit_to_width() { +	if (align == ALIGN_FILL) { +		Ref<StyleBox> style = get_theme_stylebox("normal"); +		int t_width = get_size().width - style->get_margin(MARGIN_RIGHT) - style->get_margin(MARGIN_LEFT); +		bool using_placeholder = text.empty() && ime_text.empty(); +		bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; +		if (right_icon.is_valid() || display_clear_icon) { +			Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; +			t_width -= r_icon->get_width();  		} +		TS->shaped_text_fit_to_width(text_rid, MAX(t_width, full_width));  	}  } @@ -1778,7 +2032,6 @@ void LineEdit::_clear_undo_stack() {  void LineEdit::_create_undo_state() {  	TextOperation op;  	op.text = text; -	op.cached_width = cached_width;  	op.cursor_pos = cursor_pos;  	op.scroll_offset = scroll_offset;  	undo_stack.push_back(op); @@ -1804,6 +2057,63 @@ void LineEdit::_generate_context_menu() {  		menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0);  		menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0);  	} +	menu->add_separator(); +	menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); +	menu->add_separator(); +	menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); +	if (editable) { +		menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); +	} +} + +bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { +	String str = p_name; +	if (str.begins_with("opentype_features/")) { +		String name = str.get_slicec('/', 1); +		int32_t tag = TS->name_to_tag(name); +		double value = p_value; +		if (value == -1) { +			if (opentype_features.has(tag)) { +				opentype_features.erase(tag); +				_shape(); +				update(); +			} +		} else { +			if ((double)opentype_features[tag] != value) { +				opentype_features[tag] = value; +				_shape(); +				update(); +			} +		} +		_change_notify(); +		return true; +	} + +	return false; +} + +bool LineEdit::_get(const StringName &p_name, Variant &r_ret) const { +	String str = p_name; +	if (str.begins_with("opentype_features/")) { +		String name = str.get_slicec('/', 1); +		int32_t tag = TS->name_to_tag(name); +		if (opentype_features.has(tag)) { +			r_ret = opentype_features[tag]; +			return true; +		} else { +			r_ret = -1; +			return true; +		} +	} +	return false; +} + +void LineEdit::_get_property_list(List<PropertyInfo> *p_list) const { +	for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { +		String name = TS->tag_to_name(*ftr); +		p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); +	} +	p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR));  }  void LineEdit::_bind_methods() { @@ -1819,6 +2129,19 @@ void LineEdit::_bind_methods() {  	ClassDB::bind_method(D_METHOD("deselect"), &LineEdit::deselect);  	ClassDB::bind_method(D_METHOD("set_text", "text"), &LineEdit::set_text);  	ClassDB::bind_method(D_METHOD("get_text"), &LineEdit::get_text); +	ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &LineEdit::get_draw_control_chars); +	ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &LineEdit::set_draw_control_chars); +	ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &LineEdit::set_text_direction); +	ClassDB::bind_method(D_METHOD("get_text_direction"), &LineEdit::get_text_direction); +	ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &LineEdit::set_opentype_feature); +	ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &LineEdit::get_opentype_feature); +	ClassDB::bind_method(D_METHOD("clear_opentype_features"), &LineEdit::clear_opentype_features); +	ClassDB::bind_method(D_METHOD("set_language", "language"), &LineEdit::set_language); +	ClassDB::bind_method(D_METHOD("get_language"), &LineEdit::get_language); +	ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &LineEdit::set_structured_text_bidi_override); +	ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &LineEdit::get_structured_text_bidi_override); +	ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &LineEdit::set_structured_text_bidi_override_options); +	ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &LineEdit::get_structured_text_bidi_override_options);  	ClassDB::bind_method(D_METHOD("set_placeholder", "text"), &LineEdit::set_placeholder);  	ClassDB::bind_method(D_METHOD("get_placeholder"), &LineEdit::get_placeholder);  	ClassDB::bind_method(D_METHOD("set_placeholder_alpha", "alpha"), &LineEdit::set_placeholder_alpha); @@ -1830,6 +2153,8 @@ void LineEdit::_bind_methods() {  	ClassDB::bind_method(D_METHOD("get_expand_to_text_length"), &LineEdit::get_expand_to_text_length);  	ClassDB::bind_method(D_METHOD("cursor_set_blink_enabled", "enabled"), &LineEdit::cursor_set_blink_enabled);  	ClassDB::bind_method(D_METHOD("cursor_get_blink_enabled"), &LineEdit::cursor_get_blink_enabled); +	ClassDB::bind_method(D_METHOD("set_mid_grapheme_caret_enabled", "enabled"), &LineEdit::set_mid_grapheme_caret_enabled); +	ClassDB::bind_method(D_METHOD("get_mid_grapheme_caret_enabled"), &LineEdit::get_mid_grapheme_caret_enabled);  	ClassDB::bind_method(D_METHOD("cursor_set_force_displayed", "enabled"), &LineEdit::cursor_set_force_displayed);  	ClassDB::bind_method(D_METHOD("cursor_get_force_displayed"), &LineEdit::cursor_get_force_displayed);  	ClassDB::bind_method(D_METHOD("cursor_set_blink_speed", "blink_speed"), &LineEdit::cursor_set_blink_speed); @@ -1876,6 +2201,27 @@ void LineEdit::_bind_methods() {  	BIND_ENUM_CONSTANT(MENU_SELECT_ALL);  	BIND_ENUM_CONSTANT(MENU_UNDO);  	BIND_ENUM_CONSTANT(MENU_REDO); +	BIND_ENUM_CONSTANT(MENU_DIR_INHERITED); +	BIND_ENUM_CONSTANT(MENU_DIR_AUTO); +	BIND_ENUM_CONSTANT(MENU_DIR_LTR); +	BIND_ENUM_CONSTANT(MENU_DIR_RTL); +	BIND_ENUM_CONSTANT(MENU_DISPLAY_UCC); +	BIND_ENUM_CONSTANT(MENU_INSERT_LRM); +	BIND_ENUM_CONSTANT(MENU_INSERT_RLM); +	BIND_ENUM_CONSTANT(MENU_INSERT_LRE); +	BIND_ENUM_CONSTANT(MENU_INSERT_RLE); +	BIND_ENUM_CONSTANT(MENU_INSERT_LRO); +	BIND_ENUM_CONSTANT(MENU_INSERT_RLO); +	BIND_ENUM_CONSTANT(MENU_INSERT_PDF); +	BIND_ENUM_CONSTANT(MENU_INSERT_ALM); +	BIND_ENUM_CONSTANT(MENU_INSERT_LRI); +	BIND_ENUM_CONSTANT(MENU_INSERT_RLI); +	BIND_ENUM_CONSTANT(MENU_INSERT_FSI); +	BIND_ENUM_CONSTANT(MENU_INSERT_PDI); +	BIND_ENUM_CONSTANT(MENU_INSERT_ZWJ); +	BIND_ENUM_CONSTANT(MENU_INSERT_ZWNJ); +	BIND_ENUM_CONSTANT(MENU_INSERT_WJ); +	BIND_ENUM_CONSTANT(MENU_INSERT_SHY);  	BIND_ENUM_CONSTANT(MENU_MAX);  	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); @@ -1891,6 +2237,12 @@ void LineEdit::_bind_methods() {  	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled");  	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled");  	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_right_icon", "get_right_icon"); +	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); +	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); +	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars"); +	ADD_GROUP("Structured Text", "structured_text_"); +	ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); +	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options");  	ADD_GROUP("Placeholder", "placeholder_");  	ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text"), "set_placeholder", "get_placeholder");  	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "placeholder_alpha", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_placeholder_alpha", "get_placeholder_alpha"); @@ -1899,50 +2251,67 @@ void LineEdit::_bind_methods() {  	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "cursor_set_blink_speed", "cursor_get_blink_speed");  	ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_position"), "set_cursor_position", "get_cursor_position");  	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_force_displayed"), "cursor_set_force_displayed", "cursor_get_force_displayed"); +	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_mid_grapheme_caret_enabled", "get_mid_grapheme_caret_enabled");  }  LineEdit::LineEdit() { -	undo_stack_pos = nullptr; +	text_rid = TS->create_shaped_text();  	_create_undo_state(); -	align = ALIGN_LEFT; -	cached_width = 0; -	cached_placeholder_width = 0; -	cursor_pos = 0; -	scroll_offset = 0; -	window_has_focus = true; -	max_length = 0; -	pass = false; -	secret_character = "*"; -	text_changed_dirty = false; -	placeholder_alpha = 0.6; -	clear_button_enabled = false; +  	clear_button_status.press_attempt = false;  	clear_button_status.pressing_inside = false; -	shortcut_keys_enabled = true; -	selecting_enabled = true;  	deselect();  	set_focus_mode(FOCUS_ALL);  	set_default_cursor_shape(CURSOR_IBEAM);  	set_mouse_filter(MOUSE_FILTER_STOP); -	draw_caret = true; -	caret_blink_enabled = false; -	caret_force_displayed = false;  	caret_blink_timer = memnew(Timer);  	add_child(caret_blink_timer);  	caret_blink_timer->set_wait_time(0.65);  	caret_blink_timer->connect("timeout", callable_mp(this, &LineEdit::_toggle_draw_caret));  	cursor_set_blink_enabled(false); -	context_menu_enabled = true;  	menu = memnew(PopupMenu);  	add_child(menu); -	editable = false; // Initialise to opposite first, so we get past the early-out in set_editable. -	set_editable(true); + +	menu_dir = memnew(PopupMenu); +	menu_dir->set_name("DirMenu"); +	menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); +	menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); +	menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); +	menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); +	menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), true); +	menu->add_child(menu_dir); + +	menu_ctl = memnew(PopupMenu); +	menu_ctl->set_name("CTLMenu"); +	menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); +	menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); +	menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); +	menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); +	menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); +	menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); +	menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); +	menu_ctl->add_separator(); +	menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); +	menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); +	menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); +	menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); +	menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); +	menu_ctl->add_separator(); +	menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); +	menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); +	menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); +	menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); +	menu->add_child(menu_ctl); + +	set_editable(true); // Initialise to opposite first, so we get past the early-out in set_editable.  	menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); -	expand_to_text_length = false; +	menu_dir->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); +	menu_ctl->connect("id_pressed", callable_mp(this, &LineEdit::menu_option));  }  LineEdit::~LineEdit() { +	TS->free(text_rid);  } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index 5fceedbf26..e7b2a34eed 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -53,41 +53,76 @@ public:  		MENU_SELECT_ALL,  		MENU_UNDO,  		MENU_REDO, +		MENU_DIR_INHERITED, +		MENU_DIR_AUTO, +		MENU_DIR_LTR, +		MENU_DIR_RTL, +		MENU_DISPLAY_UCC, +		MENU_INSERT_LRM, +		MENU_INSERT_RLM, +		MENU_INSERT_LRE, +		MENU_INSERT_RLE, +		MENU_INSERT_LRO, +		MENU_INSERT_RLO, +		MENU_INSERT_PDF, +		MENU_INSERT_ALM, +		MENU_INSERT_LRI, +		MENU_INSERT_RLI, +		MENU_INSERT_FSI, +		MENU_INSERT_PDI, +		MENU_INSERT_ZWJ, +		MENU_INSERT_ZWNJ, +		MENU_INSERT_WJ, +		MENU_INSERT_SHY,  		MENU_MAX -  	};  private: -	Align align; +	Align align = ALIGN_LEFT; -	bool editable; -	bool pass; -	bool text_changed_dirty; +	bool editable = false; +	bool pass = false; +	bool text_changed_dirty = false;  	String undo_text;  	String text;  	String placeholder;  	String placeholder_translated; -	String secret_character; -	float placeholder_alpha; +	String secret_character = "*"; +	float placeholder_alpha = 0.6;  	String ime_text;  	Point2 ime_selection; -	bool selecting_enabled; +	RID text_rid; +	float full_width = 0; + +	bool selecting_enabled = true; + +	bool context_menu_enabled = true; +	PopupMenu *menu = nullptr; +	PopupMenu *menu_dir = nullptr; +	PopupMenu *menu_ctl = nullptr; -	bool context_menu_enabled; -	PopupMenu *menu; +	bool mid_grapheme_caret_enabled = false; -	int cursor_pos; -	int scroll_offset; -	int max_length; // 0 for no maximum. +	int cursor_pos = 0; +	int scroll_offset = 0; +	int max_length = 0; // 0 for no maximum. -	int cached_width; -	int cached_placeholder_width; +	Dictionary opentype_features; +	String language; +	TextDirection text_direction = TEXT_DIRECTION_AUTO; +	TextDirection input_direction = TEXT_DIRECTION_LTR; +	Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; +	Array st_args; +	bool draw_control_chars = false; -	bool clear_button_enabled; +	bool expand_to_text_length = false; +	bool window_has_focus = true; -	bool shortcut_keys_enabled; +	bool clear_button_enabled = false; + +	bool shortcut_keys_enabled = true;  	bool virtual_keyboard_enabled = true; @@ -110,13 +145,18 @@ private:  		String text;  	};  	List<TextOperation> undo_stack; -	List<TextOperation>::Element *undo_stack_pos; +	List<TextOperation>::Element *undo_stack_pos = nullptr;  	struct ClearButtonStatus { -		bool press_attempt; -		bool pressing_inside; +		bool press_attempt = false; +		bool pressing_inside = false;  	} clear_button_status; +	bool caret_blink_enabled = false; +	bool caret_force_displayed = false; +	bool draw_caret = true; +	Timer *caret_blink_timer = nullptr; +  	bool _is_over_clear_button(const Point2 &p_pos) const;  	void _clear_undo_stack(); @@ -125,19 +165,10 @@ private:  	void _generate_context_menu(); -	Timer *caret_blink_timer; - +	void _shape(); +	void _fit_to_width();  	void _text_changed();  	void _emit_text_change(); -	bool expand_to_text_length; - -	void update_cached_width(); -	void update_placeholder_width(); - -	bool caret_blink_enabled; -	bool caret_force_displayed; -	bool draw_caret; -	bool window_has_focus;  	void shift_selection_check_pre(bool);  	void shift_selection_check_post(bool); @@ -147,7 +178,7 @@ private:  	int get_scroll_offset() const;  	void set_cursor_at_pixel_pos(int p_x); -	int get_cursor_pixel_pos(); +	Vector2i get_cursor_pixel_pos();  	void _reset_caret_blink_timer();  	void _toggle_draw_caret(); @@ -163,6 +194,10 @@ private:  protected:  	static void _bind_methods(); +	bool _set(const StringName &p_name, const Variant &p_value); +	bool _get(const StringName &p_name, Variant &r_ret) const; +	void _get_property_list(List<PropertyInfo> *p_list) const; +  public:  	void set_align(Align p_align);  	Align get_align() const; @@ -185,19 +220,47 @@ public:  	void delete_char();  	void delete_text(int p_from_column, int p_to_column); +  	void set_text(String p_text);  	String get_text() const; + +	void set_text_direction(TextDirection p_text_direction); +	TextDirection get_text_direction() const; + +	void set_opentype_feature(const String &p_name, int p_value); +	int get_opentype_feature(const String &p_name) const; +	void clear_opentype_features(); + +	void set_language(const String &p_language); +	String get_language() const; + +	void set_draw_control_chars(bool p_draw_control_chars); +	bool get_draw_control_chars() const; + +	void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); +	Control::StructuredTextParser get_structured_text_bidi_override() const; + +	void set_structured_text_bidi_override_options(Array p_args); +	Array get_structured_text_bidi_override_options() const; +  	void set_placeholder(String p_text);  	String get_placeholder() const; +  	void set_placeholder_alpha(float p_alpha);  	float get_placeholder_alpha() const; +  	void set_cursor_position(int p_pos);  	int get_cursor_position() const; +  	void set_max_length(int p_max_length);  	int get_max_length() const; +  	void append_at_cursor(String p_text);  	void clear(); +	void set_mid_grapheme_caret_enabled(const bool p_enabled); +	bool get_mid_grapheme_caret_enabled() const; +  	bool cursor_get_blink_enabled() const;  	void cursor_set_blink_enabled(const bool p_enabled);  |