diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index d0cf8017a80..b35eac76ee2 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -1216,6 +1216,9 @@ The shape of the caret to use in the script editor. [b]Line[/b] displays a vertical line to the left of the current character, whereas [b]Block[/b] displays an outline over the current character. + + If [code]true[/code], displays a colored button before any [Color] constructor in the script editor. Clicking on them allows the color to be modified through a color picker. + The column at which to display a subtle line as a line length guideline for scripts. This should generally be greater than [member text_editor/appearance/guidelines/line_length_guideline_soft_column]. diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index a4d5d496210..8580d85a7a6 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -683,6 +683,8 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { _load_godot2_text_editor_theme(); // Appearance + EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "text_editor/appearance/enable_inline_color_picker", true, ""); + // Appearance: Caret EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "text_editor/appearance/caret/type", 0, "Line,Block") _initial_set("text_editor/appearance/caret/caret_blink", true, true); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 06e8833bf7b..6718e1278f9 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -396,8 +396,237 @@ bool ScriptTextEditor::show_members_overview() { return true; } +bool ScriptTextEditor::_is_valid_color_info(const Dictionary &p_info) { + if (p_info.get_valid("color").get_type() != Variant::COLOR) { + return false; + } + if (!p_info.get_valid("color_end").is_num() || !p_info.get_valid("color_mode").is_num()) { + return false; + } + return true; +} + +Array ScriptTextEditor::_inline_object_parse(const String &p_text, int p_line) { + Array result; + int i_end_previous = 0; + int i_start = p_text.find("Color"); + + while (i_start != -1) { + // Ignore words that just have "Color" in them. + if (i_start == 0 || !("_" + p_text.substr(i_start - 1, 1)).is_valid_ascii_identifier()) { + int i_par_start = p_text.find_char('(', i_start + 5); + if (i_par_start != -1) { + int i_par_end = p_text.find_char(')', i_start + 5); + if (i_par_end != -1) { + Dictionary color_info; + color_info["line"] = p_line; + color_info["column"] = i_start; + color_info["width_ratio"] = 1.0; + color_info["color_end"] = i_par_end; + + String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5); + String s_params = p_text.substr(i_par_start + 1, i_par_end - i_par_start - 1); + bool has_added_color = false; + + if (fn_name.is_empty()) { + String stripped = s_params.strip_edges(true, true); + // String constructor. + if (stripped.length() > 0 && (stripped[0] == '\"')) { + String color_string = stripped.substr(1, stripped.length() - 2); + color_info["color"] = Color(color_string); + color_info["color_mode"] = MODE_STRING; + has_added_color = true; + } + // Hex constructor. + else if (stripped.length() == 10 && stripped.substr(0, 2) == "0x") { + color_info["color"] = Color(stripped.substr(2, stripped.length() - 2)); + color_info["color_mode"] = MODE_HEX; + has_added_color = true; + } + // Empty Color() constructor. + else if (stripped.is_empty()) { + color_info["color"] = Color(); + color_info["color_mode"] = MODE_RGB; + has_added_color = true; + } + } + // Float & int parameters. + if (!has_added_color && s_params.size() > 0) { + PackedFloat64Array params = s_params.split_floats(",", false); + if (params.size() == 3) { + params.resize(4); + params.set(3, 1.0); + } + if (params.size() == 4) { + has_added_color = true; + if (fn_name == ".from_ok_hsl") { + color_info["color"] = Color::from_ok_hsl(params[0], params[1], params[2], params[3]); + color_info["color_mode"] = MODE_OKHSL; + } else if (fn_name == ".from_hsv") { + color_info["color"] = Color::from_hsv(params[0], params[1], params[2], params[3]); + color_info["color_mode"] = MODE_HSV; + } else if (fn_name == ".from_rgba8") { + color_info["color"] = Color::from_rgba8(int(params[0]), int(params[1]), int(params[2]), int(params[3])); + color_info["color_mode"] = MODE_RGB8; + } else if (fn_name.is_empty()) { + color_info["color"] = Color(params[0], params[1], params[2], params[3]); + color_info["color_mode"] = MODE_RGB; + } else { + has_added_color = false; + } + } + } + + if (has_added_color) { + result.push_back(color_info); + i_end_previous = i_par_end + 1; + } + } + } + } + i_end_previous = MAX(i_end_previous, i_start); + i_start = p_text.find("Color", i_start + 1); + } + return result; +} + +void ScriptTextEditor::_inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect) { + if (_is_valid_color_info(p_info)) { + Rect2 col_rect = p_rect.grow(-4); + if (color_alpha_texture.is_null()) { + color_alpha_texture = inline_color_picker->get_theme_icon("sample_bg", "ColorPicker"); + } + code_editor->get_text_editor()->draw_texture_rect(color_alpha_texture, col_rect, false); + code_editor->get_text_editor()->draw_rect(col_rect, Color(p_info["color"])); + code_editor->get_text_editor()->draw_rect(col_rect, Color(1, 1, 1), false, 1); + } +} + +void ScriptTextEditor::_inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect) { + if (_is_valid_color_info(p_info)) { + inline_color_picker->set_pick_color(p_info["color"]); + inline_color_line = p_info["line"]; + inline_color_start = p_info["column"]; + inline_color_end = p_info["color_end"]; + + // Reset tooltip hover timer. + code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(false); + code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true); + + _update_color_constructor_options(); + inline_color_options->select(p_info["color_mode"]); + EditorNode::get_singleton()->setup_color_picker(inline_color_picker); + + // Move popup above the line if it's too low. + float_t view_h = get_viewport_rect().size.y; + float_t pop_h = inline_color_popup->get_contents_minimum_size().y; + float_t pop_y = p_rect.get_end().y; + float_t pop_x = p_rect.position.x; + if (pop_y + pop_h > view_h) { + pop_y = p_rect.position.y - pop_h; + } + // Move popup to the right if it's too high. + if (pop_y < 0) { + pop_x = p_rect.get_end().x; + } + + inline_color_popup->popup(Rect2(pop_x, pop_y, 0, 0)); + } +} + +String ScriptTextEditor::_picker_color_stringify(const Color &p_color, COLOR_MODE p_mode) { + String result; + String fname; + Vector str_params; + switch (p_mode) { + case ScriptTextEditor::MODE_STRING: { + str_params.push_back("\"" + p_color.to_html() + "\""); + } break; + case ScriptTextEditor::MODE_HEX: { + str_params.push_back("0x" + p_color.to_html()); + } break; + case ScriptTextEditor::MODE_RGB: { + str_params = { + String::num(p_color.r, 3), + String::num(p_color.g, 3), + String::num(p_color.b, 3), + String::num(p_color.a, 3) + }; + } break; + case ScriptTextEditor::MODE_HSV: { + str_params = { + String::num(p_color.get_h(), 3), + String::num(p_color.get_s(), 3), + String::num(p_color.get_v(), 3), + String::num(p_color.a, 3) + }; + fname = ".from_hsv"; + } break; + case ScriptTextEditor::MODE_OKHSL: { + str_params = { + String::num(p_color.get_ok_hsl_h(), 3), + String::num(p_color.get_ok_hsl_s(), 3), + String::num(p_color.get_ok_hsl_l(), 3), + String::num(p_color.a, 3) + }; + fname = ".from_ok_hsl"; + } break; + case ScriptTextEditor::MODE_RGB8: { + str_params = { + itos(p_color.get_r8()), + itos(p_color.get_g8()), + itos(p_color.get_b8()), + itos(p_color.get_a8()) + }; + fname = ".from_rgba8"; + } break; + default: { + } break; + } + result = "Color" + fname + "(" + String(", ").join(str_params) + ")"; + return result; +} + +void ScriptTextEditor::_picker_color_changed(const Color &p_color) { + _update_color_constructor_options(); + _update_color_text(); +} + +void ScriptTextEditor::_update_color_constructor_options() { + int item_count = inline_color_options->get_item_count(); + // Update or add each constructor as an option. + for (int i = 0; i < MODE_MAX; i++) { + String option_text = _picker_color_stringify(inline_color_picker->get_pick_color(), (COLOR_MODE)i); + if (i >= item_count) { + inline_color_options->add_item(option_text); + } else { + inline_color_options->set_item_text(i, option_text); + } + } +} + +void ScriptTextEditor::_update_color_text() { + if (inline_color_line < 0) { + return; + } + String result = inline_color_options->get_item_text(inline_color_options->get_selected_id()); + code_editor->get_text_editor()->begin_complex_operation(); + code_editor->get_text_editor()->remove_text(inline_color_line, inline_color_start, inline_color_line, inline_color_end + 1); + inline_color_end = inline_color_start + result.size() - 2; + code_editor->get_text_editor()->insert_text(result, inline_color_line, inline_color_start); + code_editor->get_text_editor()->end_complex_operation(); +} + void ScriptTextEditor::update_settings() { code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter")); + if (EDITOR_GET("text_editor/appearance/enable_inline_color_picker")) { + code_editor->get_text_editor()->set_inline_object_handlers( + callable_mp(this, &ScriptTextEditor::_inline_object_parse), + callable_mp(this, &ScriptTextEditor::_inline_object_draw), + callable_mp(this, &ScriptTextEditor::_inline_object_handle_click)); + } else { + code_editor->get_text_editor()->set_inline_object_handlers(Callable(), Callable(), Callable()); + } code_editor->update_editor_settings(); } @@ -1812,6 +2041,9 @@ void ScriptTextEditor::_notification(int p_what) { [[fallthrough]]; case NOTIFICATION_ENTER_TREE: { code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height()); + Ref code_font = get_theme_font("font", "CodeEdit"); + inline_color_options->add_theme_font_override("font", code_font); + inline_color_options->get_popup()->add_theme_font_override("font", code_font); } break; } } @@ -2594,6 +2826,23 @@ ScriptTextEditor::ScriptTextEditor() { bookmarks_menu = memnew(PopupMenu); breakpoints_menu = memnew(PopupMenu); + inline_color_popup = memnew(PopupPanel); + add_child(inline_color_popup); + + inline_color_picker = memnew(ColorPicker); + inline_color_picker->set_mouse_filter(MOUSE_FILTER_STOP); + inline_color_picker->set_deferred_mode(true); + inline_color_picker->set_hex_visible(false); + inline_color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_picker_color_changed)); + inline_color_popup->add_child(inline_color_picker); + + inline_color_options = memnew(OptionButton); + inline_color_options->set_h_size_flags(SIZE_FILL); + inline_color_options->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); + inline_color_options->set_fit_to_longest_item(false); + inline_color_options->connect("item_selected", callable_mp(this, &ScriptTextEditor::_update_color_text).unbind(1)); + inline_color_picker->get_slider(ColorPicker::SLIDER_COUNT)->get_parent()->add_sibling(inline_color_options); + connection_info_dialog = memnew(ConnectionInfoDialog); SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 7ddb7a0af83..8ce4b9aa715 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -35,6 +35,7 @@ #include "editor/code_editor.h" #include "scene/gui/color_picker.h" #include "scene/gui/dialogs.h" +#include "scene/gui/option_button.h" #include "scene/gui/tree.h" class RichTextLabel; @@ -84,6 +85,14 @@ class ScriptTextEditor : public ScriptEditorBase { PopupMenu *highlighter_menu = nullptr; PopupMenu *context_menu = nullptr; + int inline_color_line = -1; + int inline_color_start = -1; + int inline_color_end = -1; + PopupPanel *inline_color_popup = nullptr; + ColorPicker *inline_color_picker = nullptr; + OptionButton *inline_color_options = nullptr; + Ref color_alpha_texture; + GotoLinePopup *goto_line_popup = nullptr; ScriptEditorQuickOpen *quick_open = nullptr; ConnectionInfoDialog *connection_info_dialog = nullptr; @@ -160,6 +169,16 @@ class ScriptTextEditor : public ScriptEditorBase { EDIT_EMOJI_AND_SYMBOL, }; + enum COLOR_MODE { + MODE_RGB, + MODE_STRING, + MODE_HSV, + MODE_OKHSL, + MODE_RGB8, + MODE_HEX, + MODE_MAX + }; + void _enable_code_editor(); protected: @@ -185,6 +204,15 @@ protected: void _error_clicked(const Variant &p_line); void _warning_clicked(const Variant &p_line); + bool _is_valid_color_info(const Dictionary &p_info); + Array _inline_object_parse(const String &p_text, int p_line); + void _inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect); + void _inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect); + String _picker_color_stringify(const Color &p_color, COLOR_MODE p_mode); + void _picker_color_changed(const Color &p_color); + void _update_color_constructor_options(); + void _update_color_text(); + void _notification(int p_what); HashMap> highlighters; diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index a685aa10658..e437ad6e8b5 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -5204,7 +5204,8 @@ bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const S int32_t bidi_run_end = _convert_pos(p_sd, ov_start + start + _bidi_run_start + _bidi_run_length); for (int j = 0; j < sd_size; j++) { - if ((sd_glyphs[j].start >= bidi_run_start) && (sd_glyphs[j].end <= bidi_run_end)) { + int col_key_off = (sd_glyphs[j].start == sd_glyphs[j].end) ? 1 : 0; + if ((sd_glyphs[j].start >= bidi_run_start) && (sd_glyphs[j].end <= bidi_run_end - col_key_off)) { // Copy glyphs. Glyph gl = sd_glyphs[j]; if (gl.span_index >= 0) { @@ -5994,7 +5995,11 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { while (i < span_size) { String language = sd->spans[i].language; int r_start = sd->spans[i].start; - while (i + 1 < span_size && language == sd->spans[i + 1].language) { + if (r_start == sd->spans[i].end) { + i++; + continue; + } + while (i + 1 < span_size && (language == sd->spans[i + 1].language || sd->spans[i + 1].start == sd->spans[i + 1].end)) { i++; } int r_end = sd->spans[i].end; @@ -6122,6 +6127,11 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { continue; } } + // Do not add extra space for color picker object. + if (((sd_glyphs[i].flags & GRAPHEME_IS_EMBEDDED_OBJECT) == GRAPHEME_IS_EMBEDDED_OBJECT && sd_glyphs[i].start == sd_glyphs[i].end) || (uint32_t(i + 1) < sd->glyphs.size() && (sd_glyphs[i + 1].flags & GRAPHEME_IS_EMBEDDED_OBJECT) == GRAPHEME_IS_EMBEDDED_OBJECT && sd_glyphs[i + 1].start == sd_glyphs[i + 1].end)) { + i += (sd_glyphs[i].count - 1); + continue; + } Glyph gl; gl.span_index = sd_glyphs[i].span_index; gl.start = sd_glyphs[i].start; @@ -6991,7 +7001,8 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { for (int k = spn_from; k != spn_to; k += spn_delta) { const ShapedTextDataAdvanced::Span &span = sd->spans[k]; - if (span.start - sd->start >= script_run_end || span.end - sd->start <= script_run_start) { + int col_key_off = (span.start == span.end) ? 1 : 0; + if (span.start - sd->start >= script_run_end || span.end - sd->start <= script_run_start - col_key_off) { continue; } if (span.embedded_key != Variant()) { diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 714eba9e42f..b39c517758b 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -104,6 +104,12 @@ void TextEdit::Text::set_draw_control_chars(bool p_enabled) { is_dirty = true; } +void TextEdit::Text::set_inline_object_parser(const Callable &p_parser) { + inline_object_parser = p_parser; + is_dirty = true; + invalidate_all(); +} + int TextEdit::Text::get_line_width(int p_line, int p_wrap_index) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); if (p_wrap_index != -1) { @@ -242,6 +248,17 @@ void TextEdit::Text::update_accessibility(int p_line, RID p_root) { } } +inline bool is_inline_info_valid(const Variant &p_info) { + if (p_info.get_type() != Variant::DICTIONARY) { + return false; + } + Dictionary info = p_info; + if (!info.get_valid("column").is_num() || !info.get_valid("line").is_num() || !info.get_valid("width_ratio").is_num()) { + return false; + } + return true; +} + void TextEdit::Text::invalidate_cache(int p_line, bool p_text_changed) { ERR_FAIL_INDEX(p_line, text.size()); @@ -278,7 +295,41 @@ void TextEdit::Text::invalidate_cache(int p_line, bool p_text_changed) { const Array &bidi_override_with_ime = (!text_line.ime_data.is_empty()) ? text_line.ime_bidi_override : text_line.bidi_override; if (p_text_changed) { - text_line.data_buf->add_string(text_with_ime, font, font_size, language); + int from = 0; + if (inline_object_parser.is_valid()) { + // Insert inline object. + Variant parsed_result = inline_object_parser.call(text_with_ime, p_line); + if (parsed_result.is_array()) { + Array object_infos = parsed_result; + for (Variant val : object_infos) { + if (!is_inline_info_valid(val)) { + continue; + } + Dictionary info = val; + int start = info["column"]; + float width_ratio = info["width_ratio"]; + String left_string = text_with_ime.substr(from, start - from); + text_line.data_buf->add_string(left_string, font, font_size, language); + text_line.data_buf->add_object(info, Vector2(font_height * width_ratio, font_height), INLINE_ALIGNMENT_TOP, 0); + from = start; + } + } + } + String remaining_string = text_with_ime.substr(from); + text_line.data_buf->add_string(remaining_string, font, font_size, language); + + } else { + // Update inline object sizes. + for (int i = 0; i < text_line.data_buf->get_line_count(); i++) { + for (Variant key : text_line.data_buf->get_line_objects(i)) { + if (!is_inline_info_valid(key)) { + continue; + } + Dictionary info = key; + float width_ratio = info["width_ratio"]; + text_line.data_buf->resize_object(info, Vector2(font_height * width_ratio, font_height), INLINE_ALIGNMENT_TOP, 0); + } + } } if (!bidi_override_with_ime.is_empty()) { TS->shaped_text_set_bidi_override(text_line.data_buf->get_rid(), bidi_override_with_ime); @@ -1432,6 +1483,17 @@ void TextEdit::_notification(int p_what) { char_margin += wrap_indent; } + // Validate inline objects. + Vector object_keys; + if (inline_object_drawer.is_valid()) { + for (Variant k : ldata->get_line_objects(line_wrap_index)) { + if (!is_inline_info_valid(k)) { + continue; + } + object_keys.push_back(k); + } + } + // Draw selections. float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width; for (int c = 0; c < get_caret_count(); c++) { @@ -1449,9 +1511,17 @@ void TextEdit::_notification(int p_what) { sel.push_back(Vector2(line_end, line_end + char_w)); } } + // Show selection for inline objects. + for (Dictionary info : object_keys) { + int info_column = info["column"]; + if (info_column >= sel_from && info_column < sel_to) { + Rect2 orect = TS->shaped_text_get_object_rect(rid, info); + sel.push_back(Vector2(orect.position.x, orect.position.x + orect.size.x)); + } + } for (int j = 0; j < sel.size(); j++) { - Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, Math::ceil(sel[j].y) - sel[j].x, row_height); + Rect2 rect = Rect2(Math::ceil(sel[j].x) + char_margin + ofs_x, ofs_y, Math::ceil(sel[j].y) - Math::ceil(sel[j].x), row_height); if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { continue; } @@ -1570,6 +1640,16 @@ void TextEdit::_notification(int p_what) { } char_ofs = 0; } + + // Draw inline objects. + for (Dictionary k : object_keys) { + Rect2 col_rect = TS->shaped_text_get_object_rect(rid, k); + col_rect.position += Vector2(char_margin + ofs_x, ofs_y); + if (!clipped && (col_rect.position.x) >= xmargin_beg && (col_rect.position.x + col_rect.size.x) <= xmargin_end) { + inline_object_drawer.call(k, col_rect); + } + } + for (int j = 0; j < gl_size; j++) { for (const Pair &color_data : color_map) { if (color_data.first <= glyphs[j].start) { @@ -2292,6 +2372,37 @@ void TextEdit::gui_input(const Ref &p_gui_input) { last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } + + // Click inline objects. + if (inline_object_click_handler.is_valid()) { + int xmargin_beg = Math::ceil(theme_cache.style_normal->get_margin(SIDE_LEFT)) + gutters_width + gutter_padding; + int wrap_i = get_line_wrap_index_at_column(pos.y, pos.x); + int first_indent_line = 0; + if (text.is_indent_wrapped_lines()) { + _get_wrapped_indent_level(pos.y, first_indent_line); + } + float wrap_indent = wrap_i > first_indent_line ? MIN(text.get_indent_offset(pos.y, is_layout_rtl()), wrap_at_column * 0.6) : 0.0; + + Ref ldata = text.get_line_data(line); + for (Variant k : ldata->get_line_objects(wrap_i)) { + if (!is_inline_info_valid(k)) { + continue; + } + Dictionary info = k; + Rect2 obj_rect = ldata->get_line_object_rect(wrap_i, k); + obj_rect.position.x += xmargin_beg + wrap_indent - first_visible_col; + + if (mpos.x > obj_rect.position.x && mpos.x < obj_rect.get_end().x) { + Rect2 col_rect = get_rect_at_line_column(line, col); + col_rect.position += get_screen_position() + Vector2(col_rect.size.x, 0); + col_rect.size = obj_rect.size; + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_NONE); + inline_object_click_handler.call(info, col_rect); + break; + } + } + } + queue_accessibility_update(); queue_redraw(); } @@ -3485,6 +3596,31 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { if (draw_minimap && p_pos.x > xmargin_end - minimap_width && p_pos.x <= xmargin_end) { return CURSOR_ARROW; } + + // Hover inline objects. + if (inline_object_click_handler.is_valid()) { + Point2i pos = get_line_column_at_pos(p_pos); + int xmargin_beg = Math::ceil(theme_cache.style_normal->get_margin(SIDE_LEFT)) + gutters_width + gutter_padding; + int wrap_i = get_line_wrap_index_at_column(pos.y, pos.x); + int first_indent_line = 0; + if (text.is_indent_wrapped_lines()) { + _get_wrapped_indent_level(pos.y, first_indent_line); + } + float wrap_indent = wrap_i > first_indent_line ? MIN(text.get_indent_offset(pos.y, is_layout_rtl()), wrap_at_column * 0.6) : 0.0; + + Ref ldata = text.get_line_data(pos.y); + for (Variant k : ldata->get_line_objects(wrap_i)) { + if (!is_inline_info_valid(k)) { + continue; + } + Rect2 obj_rect = ldata->get_line_object_rect(wrap_i, k); + obj_rect.position.x += xmargin_beg + wrap_indent - first_visible_col; + if (p_pos.x > obj_rect.position.x && p_pos.x < obj_rect.get_end().x) { + return CURSOR_POINTING_HAND; + } + } + } + return get_default_cursor_shape(); } @@ -4277,6 +4413,12 @@ Point2i TextEdit::get_next_visible_line_index_offset_from(int p_line_from, int p return Point2i(num_total, wrap_index); } +void TextEdit::set_inline_object_handlers(const Callable &p_parser, const Callable &p_drawer, const Callable &p_click_handler) { + inline_object_drawer = p_drawer; + inline_object_click_handler = p_click_handler; + text.set_inline_object_parser(p_parser); +} + // Overridable actions void TextEdit::handle_unicode_input(const uint32_t p_unicode, int p_caret) { if (GDVIRTUAL_CALL(_handle_unicode_input, p_unicode, p_caret)) { diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index e0b785e7cf1..26a3ec643f6 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -185,6 +185,7 @@ private: String custom_word_separators; bool use_default_word_separators = true; bool use_custom_word_separators = false; + Callable inline_object_parser; mutable bool max_line_width_dirty = true; mutable bool max_line_height_dirty = true; @@ -207,6 +208,7 @@ private: void set_font_size(int p_font_size); void set_direction_and_language(TextServer::Direction p_direction, const String &p_language); void set_draw_control_chars(bool p_enabled); + void set_inline_object_parser(const Callable &p_parser); int get_line_height() const; int get_line_width(int p_line, int p_wrap_index = -1) const; @@ -354,6 +356,9 @@ private: PopupMenu *menu_dir = nullptr; PopupMenu *menu_ctl = nullptr; + Callable inline_object_drawer; + Callable inline_object_click_handler; + Key _get_menu_action_accelerator(const String &p_action); void _generate_context_menu(); void _update_context_menu(); @@ -863,6 +868,8 @@ public: int get_next_visible_line_offset_from(int p_line_from, int p_visible_amount) const; Point2i get_next_visible_line_index_offset_from(int p_line_from, int p_wrap_index_from, int p_visible_amount) const; + void set_inline_object_handlers(const Callable &p_parser, const Callable &p_drawer, const Callable &p_click_handler); + // Overridable actions void handle_unicode_input(const uint32_t p_unicode, int p_caret = -1); void backspace(int p_caret = -1); diff --git a/servers/text_server.cpp b/servers/text_server.cpp index f91e19aa711..fde34ae4cef 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -1238,6 +1238,7 @@ CaretInfo TextServer::shaped_text_get_carets(const RID &p_shaped, int64_t p_posi real_t height = (ascent + descent) / 2; real_t off = 0.0f; + real_t obj_off = -1.0f; CaretInfo caret; caret.l_dir = DIRECTION_AUTO; caret.t_dir = DIRECTION_AUTO; @@ -1247,6 +1248,11 @@ CaretInfo TextServer::shaped_text_get_carets(const RID &p_shaped, int64_t p_posi for (int i = 0; i < v_size; i++) { if (glyphs[i].count > 0) { + // Skip inline objects. + if ((glyphs[i].flags & GRAPHEME_IS_EMBEDDED_OBJECT) == GRAPHEME_IS_EMBEDDED_OBJECT && glyphs[i].start == glyphs[i].end) { + obj_off = glyphs[i].advance; + continue; + } // Caret before grapheme (top / left). if (p_position == glyphs[i].start && ((glyphs[i].flags & GRAPHEME_IS_VIRTUAL) != GRAPHEME_IS_VIRTUAL)) { real_t advance = 0.f; @@ -1335,6 +1341,7 @@ CaretInfo TextServer::shaped_text_get_carets(const RID &p_shaped, int64_t p_posi cr.size.y = char_adv; } } + cr.position.x += MAX(0.0, obj_off); // Prevent split caret when on an inline object. caret.l_caret = cr; } // Caret inside grapheme (middle). @@ -1371,6 +1378,10 @@ CaretInfo TextServer::shaped_text_get_carets(const RID &p_shaped, int64_t p_posi } } off += glyphs[i].advance * glyphs[i].repeat; + if (obj_off >= 0.0) { + off += obj_off; + obj_off = -1.0; + } } return caret; }