diff options
Diffstat (limited to 'scene/gui/line_edit.cpp')
-rw-r--r-- | scene/gui/line_edit.cpp | 1947 |
1 files changed, 1145 insertions, 802 deletions
diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 1b8f04297d..d1cd73c803 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,46 +30,219 @@ #include "line_edit.h" -#include "core/message_queue.h" +#include "core/input/input_map.h" +#include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" -#include "core/translation.h" +#include "core/string/print_string.h" +#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::_swap_current_input_direction() { + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + set_cursor_position(get_cursor_position()); + update(); +} + +void LineEdit::_move_cursor_left(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_cursor_position(selection.begin); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = cursor_pos; + + 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; + } + } + + set_cursor_position(cc); + } else { + 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(p_select); +} + +void LineEdit::_move_cursor_right(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_cursor_position(selection.end); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = cursor_pos; + + 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; + } + } + + set_cursor_position(cc); + } else { + 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(p_select); +} + +void LineEdit::_move_cursor_start(bool p_select) { + shift_selection_check_pre(p_select); + set_cursor_position(0); + shift_selection_check_post(p_select); +} + +void LineEdit::_move_cursor_end(bool p_select) { + shift_selection_check_pre(p_select); + set_cursor_position(text.length()); + shift_selection_check_post(p_select); +} + +void LineEdit::_backspace(bool p_word, bool p_all_to_left) { + if (!editable) { + return; + } + + if (p_all_to_left) { + deselect(); + text = text.substr(0, cursor_pos); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + if (p_word) { + int cc = cursor_pos; + + 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; + } + } + + delete_text(cc, cursor_pos); + + set_cursor_position(cc); + } else { + delete_char(); + } +} + +void LineEdit::_delete(bool p_word, bool p_all_to_right) { + if (!editable) { + return; + } + + if (p_all_to_right) { + deselect(); + text = text.substr(cursor_pos, text.length() - cursor_pos); + _shape(); + set_cursor_position(0); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + int text_len = text.length(); + + if (cursor_pos == text_len) { + return; // Nothing to do. + } + + if (p_word) { + int cc = cursor_pos; + 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; + } + } + + delete_text(cursor_pos, cc); + } else { + 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); + } + } } void LineEdit::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseButton> b = p_event; if (b.is_valid()) { - if (b->is_pressed() && b->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. + return; + } + if (b->is_pressed() && b->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { menu->set_position(get_screen_transform().xform(get_local_mouse_position())); menu->set_size(Vector2(1, 1)); - //menu->set_scale(get_global_transform().get_scale()); + _generate_context_menu(); menu->popup(); grab_focus(); accept_event(); return; } - if (b->get_button_index() != BUTTON_LEFT) { + if (b->get_button_index() != MOUSE_BUTTON_LEFT) { return; } _reset_caret_blink_timer(); if (b->is_pressed()) { accept_event(); //don't pass event further when clicked on text field - if (!text.empty() && is_editable() && _is_over_clear_button(b->get_position())) { + if (!text.is_empty() && is_editable() && _is_over_clear_button(b->get_position())) { clear_button_status.press_attempt = true; clear_button_status.pressing_inside = true; + update(); return; } @@ -82,11 +255,30 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { selection.creating = true; } else { - if (b->is_doubleclick() && selecting_enabled) { - selection.enabled = true; - selection.begin = 0; - selection.end = text.length(); - selection.doubleclick = true; + if (selecting_enabled) { + if (!b->is_doubleclick() && (OS::get_singleton()->get_ticks_msec() - selection.last_dblclk) < 600) { + // Triple-click select all. + selection.enabled = true; + selection.begin = 0; + selection.end = text.length(); + selection.doubleclick = true; + selection.last_dblclk = 0; + cursor_pos = selection.begin; + } else if (b->is_doubleclick()) { + // Double-click select word. + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].x < cursor_pos && words[i].y > cursor_pos) { + selection.enabled = true; + selection.begin = words[i].x; + selection.end = words[i].y; + selection.doubleclick = true; + selection.last_dblclk = OS::get_singleton()->get_ticks_msec(); + cursor_pos = selection.end; + break; + } + } + } } selection.drag_attempt = false; @@ -103,7 +295,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { update(); } else { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool press_attempt = clear_button_status.press_attempt; clear_button_status.press_attempt = false; if (press_attempt && clear_button_status.pressing_inside && _is_over_clear_button(b->get_position())) { @@ -118,13 +310,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { selection.creating = false; selection.doubleclick = false; - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } - } + show_virtual_keyboard(); } update(); @@ -133,7 +319,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseMotion> m = p_event; if (m.is_valid()) { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool last_press_inside = clear_button_status.pressing_inside; clear_button_status.pressing_inside = clear_button_status.press_attempt && _is_over_clear_button(m->get_position()); if (last_press_inside != clear_button_status.pressing_inside) { @@ -141,7 +327,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } } - if (m->get_button_mask() & BUTTON_LEFT) { + if (m->get_button_mask() & MOUSE_BUTTON_LEFT) { if (selection.creating) { set_cursor_at_pixel_pos(m->get_position().x); selection_fill_at_cursor(); @@ -156,455 +342,172 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { return; } -#ifdef APPLE_STYLE_KEYS - if (k->get_control() && !k->get_shift() && !k->get_alt() && !k->get_command()) { - uint32_t remap_key = KEY_UNKNOWN; - switch (k->get_keycode()) { - case KEY_F: { - remap_key = KEY_RIGHT; - } break; - case KEY_B: { - remap_key = KEY_LEFT; - } break; - case KEY_P: { - remap_key = KEY_UP; - } break; - case KEY_N: { - remap_key = KEY_DOWN; - } break; - case KEY_D: { - remap_key = KEY_DELETE; - } break; - case KEY_H: { - remap_key = KEY_BACKSPACE; - } break; - case KEY_A: { - remap_key = KEY_HOME; - } break; - case KEY_E: { - remap_key = KEY_END; - } break; + if (context_menu_enabled) { + if (k->is_action("ui_menu", true)) { + 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)); + _generate_context_menu(); + menu->popup(); + menu->grab_focus(); } + } - if (remap_key != KEY_UNKNOWN) { - k->set_keycode(remap_key); - k->set_control(false); + // Default is ENTER, KP_ENTER. Cannot use ui_accept as default includes SPACE + if (k->is_action("ui_text_newline", true)) { + emit_signal("text_entered", text); + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + DisplayServer::get_singleton()->virtual_keyboard_hide(); } } -#endif - - unsigned int code = k->get_keycode(); - if (k->get_command() && is_shortcut_keys_enabled()) { - bool handled = true; - - switch (code) { - case (KEY_X): { // CUT. - - if (editable) { - cut_text(); - } - - } break; - - case (KEY_C): { // COPY. - - copy_text(); - - } break; - - case (KEY_V): { // PASTE. - - if (editable) { - paste_text(); - } - - } break; - - case (KEY_Z): { // Undo/redo. - if (editable) { - if (k->get_shift()) { - redo(); - } else { - undo(); - } - } - } break; - - case (KEY_U): { // Delete from start to cursor. - - if (editable) { - deselect(); - text = text.substr(cursor_pos, text.length() - cursor_pos); - update_cached_width(); - set_cursor_position(0); - _text_changed(); - } - - } break; - - case (KEY_Y): { // PASTE (Yank for unix users). - - if (editable) { - paste_text(); - } - - } break; - case (KEY_K): { // Delete from cursor_pos to end. - - if (editable) { - deselect(); - text = text.substr(0, cursor_pos); - _text_changed(); - } + if (is_shortcut_keys_enabled()) { + if (k->is_action("ui_copy", true)) { + copy_text(); + accept_event(); + return; + } - } break; - case (KEY_A): { // Select all. - select(); + if (k->is_action("ui_text_select_all", true)) { + select(); + accept_event(); + return; + } - } break; -#ifdef APPLE_STYLE_KEYS - case (KEY_LEFT): { // Go to start of text - like HOME key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_RIGHT): { // Go to end of text - like END key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_BACKSPACE): { - if (!editable) - break; + // Cut / Paste + if (k->is_action("ui_cut", true)) { + cut_text(); + accept_event(); + return; + } - // If selected, delete the selection - if (selection.enabled) { - selection_delete(); - break; - } + if (k->is_action("ui_paste", true)) { + paste_text(); + accept_event(); + return; + } - // Otherwise delete from cursor to beginning of text edit - int current_pos = get_cursor_position(); - if (current_pos != 0) { - delete_text(0, current_pos); - } - } break; -#endif - default: { - handled = false; - } + // Undo / Redo + if (k->is_action("ui_undo", true)) { + undo(); + accept_event(); + return; } - if (handled) { + if (k->is_action("ui_redo", true)) { + redo(); accept_event(); return; } } - _reset_caret_blink_timer(); - if (!k->get_metakey()) { - bool handled = true; - switch (code) { - case KEY_KP_ENTER: - case KEY_ENTER: { - emit_signal("text_entered", text); - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_hide(); - } - - } break; - - case KEY_BACKSPACE: { - if (!editable) { - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } 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) { - break; - } - - prev_char = ischar; - cc--; - } - - delete_text(cc, cursor_pos); - - set_cursor_position(cc); - - } else { - delete_char(); - } - - } break; - case KEY_KP_4: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_LEFT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.begin); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(0); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - 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) { - break; - } - - prev_char = ischar; - cc--; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() - 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_KP_6: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_RIGHT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.end); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(text.length()); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - 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) { - break; - } - - prev_char = ischar; - cc++; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() + 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_UP: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == 0) { - handled = false; - } - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DOWN: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == text.length()) { - handled = false; - } - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DELETE: { - if (!editable) { - break; - } - - if (k->get_shift() && !k->get_command() && !k->get_alt()) { - cut_text(); - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - - int text_len = text.length(); - - if (cursor_pos == text_len) { - break; // Nothing to do. - } + // BACKSPACE + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _backspace(); + accept_event(); + return; + } -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor_pos; + // DELETE + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - bool prev_char = false; + // Cursor Movement - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); + k = k->duplicate(); + bool shift_pressed = k->get_shift(); + // Remove shift or else actions will not match. Use above variable for selection. + k->set_shift(false); - if (prev_char && !ischar) { - break; - } - prev_char = ischar; - cc++; - } - - delete_text(cursor_pos, cc); + if (k->is_action("ui_text_caret_word_left", true)) { + _move_cursor_left(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_left", true)) { + _move_cursor_left(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_word_right", true)) { + _move_cursor_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_cursor_right(shift_pressed, false); + accept_event(); + return; + } - } else { - set_cursor_position(cursor_pos + 1); - delete_char(); - } + // Up = Home, Down = End + if (k->is_action("ui_text_caret_up", true) || k->is_action("ui_text_caret_line_start", true) || k->is_action("ui_text_caret_page_up", true)) { + _move_cursor_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true) || k->is_action("ui_text_caret_line_end", true) || k->is_action("ui_text_caret_page_down", true)) { + _move_cursor_end(shift_pressed); + accept_event(); + return; + } - } break; - case KEY_KP_7: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_HOME: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_KP_1: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_END: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } 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); - menu->set_position(get_global_transform().xform(pos)); - menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); - menu->popup(); - menu->grab_focus(); - } - } break; + // Misc + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } - default: { - handled = false; - } break; - } + _reset_caret_blink_timer(); - if (handled) { - accept_event(); - } else if (!k->get_command() || (k->get_command() && k->get_alt())) { - if (k->get_unicode() >= 32 && k->get_keycode() != KEY_DELETE) { - if (editable) { - selection_delete(); - char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; - int prev_len = text.length(); - append_at_cursor(ucodestr); - if (text.length() != prev_len) { - _text_changed(); - } - accept_event(); - } + // Allow unicode handling if: + // * No Modifiers are pressed (except shift) + bool allow_unicode_handling = !(k->get_command() || k->get_control() || k->get_alt() || k->get_metakey()); - } else { - return; - } + if (allow_unicode_handling && editable && k->get_unicode() >= 32) { + // Handle Unicode (if no modifiers active) + selection_delete(); + char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; + int prev_len = text.length(); + append_at_cursor(ucodestr); + if (text.length() != prev_len) { + _text_changed(); } - - update(); + accept_event(); } - - return; } } 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(); } @@ -625,22 +528,23 @@ Variant LineEdit::get_drag_data(const Point2 &p_point) { } bool LineEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const { + bool drop_override = Control::can_drop_data(p_point, p_data); // In case user wants to drop custom data. + if (drop_override) { + return drop_override; + } + return p_data.get_type() == Variant::STRING; } void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { + Control::drop_data(p_point, p_data); + if (p_data.get_type() == Variant::STRING) { 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; @@ -649,7 +553,7 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { } Control::CursorShape LineEdit::get_cursor_shape(const Point2 &p_pos) const { - if (!text.empty() && is_editable() && _is_over_clear_button(p_pos)) { + if (!text.is_empty() && is_editable() && _is_over_clear_button(p_pos)) { return CURSOR_ARROW; } return Control::get_cursor_shape(p_pos); @@ -679,13 +583,18 @@ void LineEdit::_notification(int p_what) { } break; #endif case NOTIFICATION_RESIZED: { - window_pos = 0; + _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: { @@ -704,6 +613,7 @@ void LineEdit::_notification(int p_what) { } int width, height; + bool rtl = is_layout_rtl(); Size2 size = get_size(); width = size.width; @@ -716,7 +626,6 @@ void LineEdit::_notification(int p_what) { style = get_theme_stylebox("read_only"); draw_caret = false; } - Ref<Font> font = get_theme_font("font"); style->draw(ci, Rect2(Point2(), size)); @@ -726,40 +635,45 @@ 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; + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + float text_width = TS->shaped_text_get_size(text_rid).x; + float text_height = TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - x_ofs = style->get_offset().x; + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { + 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(SIDE_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(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } } break; } - int ofs_max = width - style->get_margin(MARGIN_RIGHT); - int char_ofs = window_pos; + int ofs_max = width - style->get_margin(SIDE_RIGHT); 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 font_color = is_editable() ? get_theme_color("font_color") : get_theme_color("font_uneditable_color"); + Color font_selected_color = get_theme_color("font_selected_color"); 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; @@ -777,150 +691,166 @@ void LineEdit::_notification(int p_what) { } } - r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); + r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(SIDE_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); if (align == ALIGN_CENTER) { - if (window_pos == 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); + if (scroll_offset == 0) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); } } else { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } 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) { - // End of string, break. - if (char_ofs >= t.length()) { - break; - } - - 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; - } +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif - 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); + // 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); + } + } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(text_rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + + // Draw text. + ofs.y += TS->shaped_text_get_ascent(text_rid); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + if (outline_size > 0 && font_outline_color.a > 0) { + Vector2 oofs = ofs; + for (int i = 0; i < gl_size; i++) { + for (int j = 0; j < glyphs[i].repeat; j++) { + if (ceil(oofs.x) >= x_ofs && (oofs.x + glyphs[i].advance) <= ofs_max) { + if (glyphs[i].font_rid != RID()) { + TS->font_draw_glyph_outline(glyphs[i].font_rid, ci, glyphs[i].font_size, outline_size, oofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, font_outline_color); } - - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); - - x_ofs += im_char_width; - ofs++; } + oofs.x += glyphs[i].advance; + } + if (oofs.x >= ofs_max) { + break; } } - - 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) { - 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; - drawer.draw_char(ci, Point2(x_ofs, yofs + font_ascent), cchar, next, 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 + } + for (int i = 0; i < gl_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 && (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_selected_color : 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_selected_color : font_color); + } } + ofs.x += glyphs[i].advance; + } + if (ofs.x >= ofs_max) { + break; } - - 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); - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, 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 intermediate 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) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { 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; @@ -935,32 +865,27 @@ void LineEdit::_notification(int p_what) { } } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); Point2 cursor_pos = Point2(get_cursor_position(), 1) * get_minimum_size().height; DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); } - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } - } - + show_virtual_keyboard(); } break; case NOTIFICATION_FOCUS_EXIT: { if (caret_blink_enabled && !caret_force_displayed) { caret_blink_timer->stop(); } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id()); DisplayServer::get_singleton()->window_set_ime_active(false, get_viewport()->get_window_id()); } 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(); @@ -971,6 +896,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; @@ -984,13 +912,17 @@ void LineEdit::copy_text() { } void LineEdit::cut_text() { - if (selection.enabled && !pass) { + if (editable && selection.enabled && !pass) { DisplayServer::get_singleton()->clipboard_set(text.substr(selection.begin, selection.end - selection.begin)); selection_delete(); } } void LineEdit::paste_text() { + if (!editable) { + return; + } + // Strip escape characters like \n and \t as they can't be displayed on LineEdit. String paste_buffer = DisplayServer::get_singleton()->clipboard_get().strip_escapes(); @@ -1011,6 +943,10 @@ void LineEdit::paste_text() { } void LineEdit::undo() { + if (!editable) { + return; + } + if (undo_stack_pos == nullptr) { if (undo_stack.size() <= 1) { return; @@ -1022,18 +958,18 @@ void LineEdit::undo() { undo_stack_pos = undo_stack_pos->prev(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; - window_pos = op.window_pos; + scroll_offset = op.scroll_offset; set_cursor_position(op.cursor_pos); - if (expand_to_text_length) { - minimum_size_changed(); - } - + _shape(); _emit_text_change(); } void LineEdit::redo() { + if (!editable) { + return; + } + if (undo_stack_pos == nullptr) { return; } @@ -1043,14 +979,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; - window_pos = op.window_pos; + scroll_offset = op.scroll_offset; set_cursor_position(op.cursor_pos); - if (expand_to_text_length) { - minimum_size_changed(); - } - + _shape(); _emit_text_change(); } @@ -1070,98 +1002,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 = window_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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { - pixel_ofs = int(style->get_offset().x); + if (scroll_offset != 0) { + 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(SIDE_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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_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.is_empty() && ime_text.is_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(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_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 = window_pos; +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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { - pixel_ofs = int(style->get_offset().x); + if (scroll_offset != 0) { + 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(SIDE_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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_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.is_empty() && ime_text.is_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(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } - ofs++; } - return pixel_ofs; + 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; + } + } else { + ret.y = ret.x; + } + + 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 { @@ -1182,6 +1154,8 @@ void LineEdit::cursor_set_blink_enabled(const bool p_enabled) { } draw_caret = true; + + notify_property_list_changed(); } bool LineEdit::cursor_get_force_displayed() const { @@ -1226,49 +1200,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) { - window_pos = CLAMP(window_pos - 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 (window_pos > cursor_pos) { - window_pos = cursor_pos; - } - - if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { - window_pos = CLAMP(window_pos - (p_to_column - p_from_column), 0, MAX(text.length() - 1, 0)); - } if (!text_changed_dirty) { if (is_inside_tree()) { @@ -1282,18 +1233,120 @@ 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(); - cursor_pos = 0; - window_pos = 0; +} + +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(); + + // This should reset virtual keyboard state if needed. + if (has_focus()) { + show_virtual_keyboard(); + } +} + +void LineEdit::show_virtual_keyboard() { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + if (selection.enabled) { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); + } else { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); + } + } } String LineEdit::get_text() const { @@ -1303,7 +1356,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(); } @@ -1331,57 +1384,68 @@ void LineEdit::set_cursor_position(int p_pos) { cursor_pos = p_pos; + // Fit to window. + if (!is_inside_tree()) { - window_pos = cursor_pos; + scroll_offset = 0; return; } Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); - - if (cursor_pos <= window_pos) { - // Adjust window if cursor goes too much to the left. - set_window_pos(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 = window_pos; + bool rtl = is_layout_rtl(); - if (font.is_valid()) { - int accum_width = 0; - - for (int i = cursor_pos; i >= window_pos; 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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_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(SIDE_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(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } + } break; + } - wp = i; + int ofs_max = get_size().width - style->get_margin(SIDE_RIGHT); + bool using_placeholder = text.is_empty() && ime_text.is_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(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } + ofs_max -= r_icon->get_width(); + } - if (wp != window_pos) { - set_window_pos(wp); - } + // Note: Use two 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(); } @@ -1389,19 +1453,27 @@ int LineEdit::get_cursor_position() const { return cursor_pos; } -void LineEdit::set_window_pos(int p_pos) { - window_pos = p_pos; - if (window_pos < 0) { - window_pos = 0; +void LineEdit::set_scroll_offset(int p_pos) { + scroll_offset = p_pos; + if (scroll_offset < 0) { + scroll_offset = 0; } } +int LineEdit::get_scroll_offset() const { + return scroll_offset; +} + void LineEdit::append_at_cursor(String p_text) { if ((max_length <= 0) || (text.length() + p_text.length() <= max_length)) { 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"); @@ -1411,39 +1483,39 @@ void LineEdit::append_at_cursor(String p_text) { void LineEdit::clear_internal() { deselect(); _clear_undo_stack(); - cached_width = 0; cursor_pos = 0; - window_pos = 0; + scroll_offset = 0; undo_text = ""; text = ""; + _shape(); update(); } Size2 LineEdit::get_minimum_size() const { Ref<StyleBox> style = get_theme_stylebox("normal"); Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); Size2 min_size; // Minimum size of text. - int space_size = font->get_char_size(' ').x; - min_size.width = get_theme_constant("minimum_spaces") * space_size; + int em_space_size = font->get_char_size('M', 0, font_size).x; + min_size.width = get_theme_constant("minimum_character_width") * em_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).x + space_size); + min_size.width = MAX(min_size.width, full_width + em_space_size); } - min_size.height = font->get_height(); + min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM), 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.is_empty() && ime_text.is_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; @@ -1526,8 +1598,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(); } @@ -1540,8 +1614,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(); } @@ -1618,6 +1694,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)); + } } } } @@ -1644,7 +1815,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_window_pos(0); + set_cursor_position(cursor_pos); } bool LineEdit::get_expand_to_text_length() const { @@ -1656,6 +1827,7 @@ void LineEdit::set_clear_button_enabled(bool p_enabled) { return; } clear_button_enabled = p_enabled; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1701,6 +1873,7 @@ void LineEdit::set_right_icon(const Ref<Texture2D> &p_icon) { return; } right_icon = p_icon; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1710,38 +1883,64 @@ Ref<Texture2D> LineEdit::get_right_icon() { } void LineEdit::_text_changed() { - if (expand_to_text_length) { - minimum_size_changed(); - } - _emit_text_change(); _clear_redo(); } void LineEdit::_emit_text_change() { emit_signal("text_changed", text); - _change_notify("text"); 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(SIDE_RIGHT) - style->get_margin(SIDE_LEFT); + bool using_placeholder = text.is_empty() && ime_text.is_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)); } } @@ -1769,31 +1968,118 @@ 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.window_pos = window_pos; + op.scroll_offset = scroll_offset; undo_stack.push_back(op); } +int LineEdit::_get_menu_action_accelerator(const String &p_action) { + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); + if (!events) { + return 0; + } + + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return 0; + } + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return 0; + } + + // Use physical keycode if non-zero + if (event->get_physical_keycode() != 0) { + return event->get_physical_keycode_with_modifiers(); + } else { + return event->get_keycode_with_modifiers(); + } +} + void LineEdit::_generate_context_menu() { // Reorganize context menu. menu->clear(); if (editable) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_X : 0); + menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_C : 0); + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); if (editable) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_V : 0); + menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); } menu->add_separator(); if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_A : 0); + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); } if (editable) { menu->add_item(RTR("Clear"), MENU_CLEAR); menu->add_separator(); - 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_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); + menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 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(); + } + } + notify_property_list_changed(); + 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::_validate_property(PropertyInfo &property) const { + if (!caret_blink_enabled && property.name == "caret_blink_speed") { + property.usage = PROPERTY_USAGE_NOEDITOR; } } @@ -1810,16 +2096,32 @@ 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); ClassDB::bind_method(D_METHOD("get_placeholder_alpha"), &LineEdit::get_placeholder_alpha); ClassDB::bind_method(D_METHOD("set_cursor_position", "position"), &LineEdit::set_cursor_position); ClassDB::bind_method(D_METHOD("get_cursor_position"), &LineEdit::get_cursor_position); + ClassDB::bind_method(D_METHOD("get_scroll_offset"), &LineEdit::get_scroll_offset); ClassDB::bind_method(D_METHOD("set_expand_to_text_length", "enabled"), &LineEdit::set_expand_to_text_length); 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); @@ -1866,6 +2168,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"); @@ -1881,6 +2204,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"); @@ -1889,50 +2218,64 @@ 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; - window_pos = 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); } |