summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbruvzg <7645683+bruvzg@users.noreply.github.com>2020-09-09 16:00:32 +0300
committerbruvzg <7645683+bruvzg@users.noreply.github.com>2020-11-26 14:25:49 +0200
commitd66eb77c9c4d8906f94180a3ba542a025bc45aae (patch)
tree6d3dac436363686915537a1f9943ce509eb757c6
parent99666de00fb30cb86473257776504ca70b4469c3 (diff)
[Complex Text Layouts] Refactor Label and LineEdit controls.
-rw-r--r--scene/gui/label.cpp716
-rw-r--r--scene/gui/label.h54
-rw-r--r--scene/gui/line_edit.cpp1097
-rw-r--r--scene/gui/line_edit.h127
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);