From e85e6ec7fcb4d2d542059feb63b18553d1470694 Mon Sep 17 00:00:00 2001 From: Yuri Sizov Date: Thu, 18 Nov 2021 18:55:43 +0300 Subject: [PATCH] Add methods to get position from column and line in TextEdit --- doc/classes/TextEdit.xml | 18 ++++++ doc/classes/TextServer.xml | 8 +++ doc/classes/TextServerExtension.xml | 8 +++ scene/gui/text_edit.cpp | 80 ++++++++++++++++++++++++-- scene/gui/text_edit.h | 21 +++++-- servers/text/text_server_extension.cpp | 9 +++ servers/text/text_server_extension.h | 2 + servers/text_server.cpp | 22 +++++++ servers/text_server.h | 1 + 9 files changed, 160 insertions(+), 9 deletions(-) diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 400fae4aadfe..16d8595b4ee2 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -362,6 +362,24 @@ Returns OpenType feature [code]tag[/code]. + + + + + + Returns the local position for the given [code]line[/code] and [code]column[/code]. If [code]x[/code] or [code]y[/code] of the returned vector equal [code]-1[/code], the position is outside of the viewable area of the control. + [b]Note:[/b] The Y position corresponds to the bottom side of the line. Use [method get_rect_at_line_column] to get the top side position. + + + + + + + + Returns the local position and size for the grapheme at the given [code]line[/code] and [code]column[/code]. If [code]x[/code] or [code]y[/code] position of the returned rect equal [code]-1[/code], the position is outside of the viewable area of the control. + [b]Note:[/b] The Y position of the returned rect corresponds to the top side of the line, unlike [method get_pos_at_line_column] which returns the bottom side. + + diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index e1c05165de27..512078c56c50 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -1028,6 +1028,14 @@ Returns text glyphs in the visual order. + + + + + + Returns composite character's bounds as offsets from the start of the line. + + diff --git a/doc/classes/TextServerExtension.xml b/doc/classes/TextServerExtension.xml index 684a1aa755bd..32f8107e0afe 100644 --- a/doc/classes/TextServerExtension.xml +++ b/doc/classes/TextServerExtension.xml @@ -1037,6 +1037,14 @@ Copies text glyphs in the visual order, into preallocated array of the size returned by [method _shaped_text_get_glyph_count]. + + + + + + Returns composite character's bounds as offsets from the start of the line. + + diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 74268707ff68..ad1790f61dc8 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -608,7 +608,7 @@ void TextEdit::_notification(int p_what) { int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); draw_amount += get_line_wrap_count(first_visible_line + 1); - // minimap + // Draw minimap. if (draw_minimap) { int minimap_visible_lines = get_minimap_visible_lines(); int minimap_line_height = (minimap_char_size.y + minimap_line_spacing); @@ -788,8 +788,9 @@ void TextEdit::_notification(int p_what) { bottom_limit_y -= style_normal->get_margin(SIDE_BOTTOM); } - // draw main text + // Draw main text. caret.visible = false; + line_drawing_cache.clear(); int row_height = get_line_height(); int line = first_visible_line; for (int i = 0; i < draw_amount; i++) { @@ -810,6 +811,8 @@ void TextEdit::_notification(int p_what) { continue; } + LineDrawingCache cache_entry; + Dictionary color_map = _get_line_syntax_highlighting(line); // Ensure we at least use the font color. @@ -899,6 +902,8 @@ void TextEdit::_notification(int p_what) { if (line_wrap_index == 0) { // Only do these if we are on the first wrapped part of a line. + cache_entry.y_offset = ofs_y; + int gutter_offset = style_normal->get_margin(SIDE_LEFT); for (int g = 0; g < gutters.size(); g++) { const GutterInfo gutter = gutters[g]; @@ -1076,6 +1081,10 @@ void TextEdit::_notification(int p_what) { int gl_size = TS->shaped_text_get_glyph_count(rid); ofs_y += ldata->get_line_ascent(line_wrap_index); + + int first_visible_char = TS->shaped_text_get_range(rid).y; + int last_visible_char = TS->shaped_text_get_range(rid).x; + int char_ofs = 0; if (outline_size > 0 && outline_color.a > 0) { for (int j = 0; j < gl_size; j++) { @@ -1143,21 +1152,36 @@ void TextEdit::_notification(int p_what) { } } + bool had_glyphs_drawn = false; for (int k = 0; k < glyphs[j].repeat; k++) { if (!clipped && (char_ofs + char_margin) >= xmargin_beg && (char_ofs + glyphs[j].advance + char_margin) <= xmargin_end) { if (glyphs[j].font_rid != RID()) { TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, current_color); + had_glyphs_drawn = true; } else if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { TS->draw_hex_code_box(ci, glyphs[j].font_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, current_color); + had_glyphs_drawn = true; } } char_ofs += glyphs[j].advance; } + + if (had_glyphs_drawn) { + if (first_visible_char > glyphs[j].start) { + first_visible_char = glyphs[j].start; + } else if (last_visible_char < glyphs[j].end) { + last_visible_char = glyphs[j].end; + } + } + if ((char_ofs + char_margin) >= xmargin_end) { break; } } + cache_entry.first_visible_chars.push_back(first_visible_char); + cache_entry.last_visible_chars.push_back(last_visible_char); + // is_line_folded if (line_wrap_index == line_wrap_amount && line < text.size() - 1 && _is_line_hidden(line + 1)) { int xofs = char_ofs + char_margin + ofs_x + (folded_eol_icon->get_width() / 2); @@ -1308,6 +1332,8 @@ void TextEdit::_notification(int p_what) { } } } + + line_drawing_cache[line] = cache_entry; } if (has_focus()) { @@ -3457,6 +3483,49 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ return Point2i(col, row); } +Point2i TextEdit::get_pos_at_line_column(int p_line, int p_column) const { + Rect2i rect = get_rect_at_line_column(p_line, p_column); + return rect.position + Vector2i(0, get_line_height()); +} + +Rect2i TextEdit::get_rect_at_line_column(int p_line, int p_column) const { + ERR_FAIL_INDEX_V(p_line, text.size(), Rect2i(-1, -1, 0, 0)); + ERR_FAIL_COND_V(p_column < 0, Rect2i(-1, -1, 0, 0)); + ERR_FAIL_COND_V(p_column > text[p_line].length(), Rect2i(-1, -1, 0, 0)); + + if (line_drawing_cache.size() == 0 || !line_drawing_cache.has(p_line)) { + // Line is not in the cache, which means it's outside of the viewing area. + return Rect2i(-1, -1, 0, 0); + } + LineDrawingCache cache_entry = line_drawing_cache[p_line]; + + int wrap_index = get_line_wrap_index_at_column(p_line, p_column); + if (wrap_index >= cache_entry.first_visible_chars.size()) { + // Line seems to be wrapped beyond the viewable area. + return Rect2i(-1, -1, 0, 0); + } + + int first_visible_char = cache_entry.first_visible_chars[wrap_index]; + int last_visible_char = cache_entry.last_visible_chars[wrap_index]; + if (p_column < first_visible_char || p_column > last_visible_char) { + // Character is outside of the viewing area, no point calculating its position. + return Rect2i(-1, -1, 0, 0); + } + + Point2i pos, size; + pos.y = cache_entry.y_offset + get_line_height() * wrap_index; + pos.x = get_total_gutter_width() + style_normal->get_margin(SIDE_LEFT) - get_h_scroll(); + + RID text_rid = text.get_line_data(p_line)->get_line_rid(wrap_index); + Vector2 col_bounds = TS->shaped_text_get_grapheme_bounds(text_rid, p_column); + pos.x += col_bounds.x; + size.x = col_bounds.y - col_bounds.x; + + size.y = get_line_height(); + + return Rect2i(pos, size); +} + int TextEdit::get_minimap_line_at_pos(const Point2i &p_pos) const { float rows = p_pos.y; rows -= style_normal->get_margin(SIDE_TOP); @@ -3897,7 +3966,7 @@ void TextEdit::delete_selection() { update(); } -/* line wrapping. */ +/* Line wrapping. */ void TextEdit::set_line_wrapping_mode(LineWrappingMode p_wrapping_mode) { if (line_wrapping_mode != p_wrapping_mode) { line_wrapping_mode = p_wrapping_mode; @@ -4701,6 +4770,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_word_at_pos", "position"), &TextEdit::get_word_at_pos); ClassDB::bind_method(D_METHOD("get_line_column_at_pos", "position", "allow_out_of_bounds"), &TextEdit::get_line_column_at_pos, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_pos_at_line_column", "line", "column"), &TextEdit::get_pos_at_line_column); + ClassDB::bind_method(D_METHOD("get_rect_at_line_column", "line", "column"), &TextEdit::get_rect_at_line_column); + ClassDB::bind_method(D_METHOD("get_minimap_line_at_pos", "position"), &TextEdit::get_minimap_line_at_pos); ClassDB::bind_method(D_METHOD("is_dragging_cursor"), &TextEdit::is_dragging_cursor); @@ -4778,7 +4850,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("deselect"), &TextEdit::deselect); ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection); - /* line wrapping. */ + /* Line wrapping. */ BIND_ENUM_CONSTANT(LINE_WRAPPING_NONE); BIND_ENUM_CONSTANT(LINE_WRAPPING_BOUNDARY); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 09315ae395c4..b13adb8519ba 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -271,7 +271,7 @@ private: bool virtual_keyboard_enabled = true; bool middle_mouse_paste_enabled = true; - // Overridable actions + // Overridable actions. String cut_copy_line = ""; // Context menu. @@ -336,6 +336,14 @@ private: Variant tooltip_ud; /* Mouse */ + struct LineDrawingCache { + int y_offset = 0; + Vector first_visible_chars; + Vector last_visible_chars; + }; + + Map line_drawing_cache; + int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; /* Caret. */ @@ -415,7 +423,7 @@ private: void _pre_shift_selection(); void _post_shift_selection(); - /* line wrapping. */ + /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; int wrap_at_column = 0; @@ -455,14 +463,14 @@ private: void _scroll_lines_up(); void _scroll_lines_down(); - // Minimap + // Minimap. bool draw_minimap = false; int minimap_width = 80; Point2 minimap_char_size = Point2(1, 2); int minimap_line_spacing = 1; - // minimap scroll + // Minimap scroll. bool minimap_clicked = false; bool hovering_minimap = false; bool dragging_minimap = false; @@ -717,6 +725,9 @@ public: String get_word_at_pos(const Vector2 &p_pos) const; Point2i get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_of_bounds = true) const; + Point2i get_pos_at_line_column(int p_line, int p_column) const; + Rect2i get_rect_at_line_column(int p_line, int p_column) const; + int get_minimap_line_at_pos(const Point2i &p_pos) const; bool is_dragging_cursor() const; @@ -782,7 +793,7 @@ public: void deselect(); void delete_selection(); - /* line wrapping. */ + /* Line wrapping. */ void set_line_wrapping_mode(LineWrappingMode p_wrapping_mode); LineWrappingMode get_line_wrapping_mode() const; diff --git a/servers/text/text_server_extension.cpp b/servers/text/text_server_extension.cpp index 0a7523e33a53..d6d98b4f8f5b 100644 --- a/servers/text/text_server_extension.cpp +++ b/servers/text/text_server_extension.cpp @@ -260,6 +260,7 @@ void TextServerExtension::_bind_methods() { GDVIRTUAL_BIND(_shaped_text_draw, "shaped", "canvas", "pos", "clip_l", "clip_r", "color"); GDVIRTUAL_BIND(_shaped_text_draw_outline, "shaped", "canvas", "pos", "clip_l", "clip_r", "outline_size", "color"); + GDVIRTUAL_BIND(_shaped_text_get_grapheme_bounds, "shaped", "pos"); GDVIRTUAL_BIND(_shaped_text_next_grapheme_pos, "shaped", "pos"); GDVIRTUAL_BIND(_shaped_text_prev_grapheme_pos, "shaped", "pos"); @@ -1292,6 +1293,14 @@ void TextServerExtension::shaped_text_draw_outline(RID p_shaped, RID p_canvas, c shaped_text_draw_outline(p_shaped, p_canvas, p_pos, p_clip_l, p_clip_r, p_outline_size, p_color); } +Vector2 TextServerExtension::shaped_text_get_grapheme_bounds(RID p_shaped, int p_pos) const { + Vector2 ret; + if (GDVIRTUAL_CALL(_shaped_text_get_grapheme_bounds, p_shaped, p_pos, ret)) { + return ret; + } + return TextServer::shaped_text_get_grapheme_bounds(p_shaped, p_pos); +} + int TextServerExtension::shaped_text_next_grapheme_pos(RID p_shaped, int p_pos) const { int ret; if (GDVIRTUAL_CALL(_shaped_text_next_grapheme_pos, p_shaped, p_pos, ret)) { diff --git a/servers/text/text_server_extension.h b/servers/text/text_server_extension.h index e419b4055d08..a2dbd25e0539 100644 --- a/servers/text/text_server_extension.h +++ b/servers/text/text_server_extension.h @@ -428,8 +428,10 @@ public: GDVIRTUAL6C(_shaped_text_draw, RID, RID, const Vector2 &, float, float, const Color &); GDVIRTUAL7C(_shaped_text_draw_outline, RID, RID, const Vector2 &, float, float, int, const Color &); + virtual Vector2 shaped_text_get_grapheme_bounds(RID p_shaped, int p_pos) const override; virtual int shaped_text_next_grapheme_pos(RID p_shaped, int p_pos) const override; virtual int shaped_text_prev_grapheme_pos(RID p_shaped, int p_pos) const override; + GDVIRTUAL2RC(Vector2, _shaped_text_get_grapheme_bounds, RID, int); GDVIRTUAL2RC(int, _shaped_text_next_grapheme_pos, RID, int); GDVIRTUAL2RC(int, _shaped_text_prev_grapheme_pos, RID, int); diff --git a/servers/text_server.cpp b/servers/text_server.cpp index 9034239fe000..8fc1b56ae480 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -403,6 +403,7 @@ void TextServer::_bind_methods() { ClassDB::bind_method(D_METHOD("shaped_text_hit_test_grapheme", "shaped", "coords"), &TextServer::shaped_text_hit_test_grapheme); ClassDB::bind_method(D_METHOD("shaped_text_hit_test_position", "shaped", "coords"), &TextServer::shaped_text_hit_test_position); + ClassDB::bind_method(D_METHOD("shaped_text_get_grapheme_bounds", "shaped", "pos"), &TextServer::shaped_text_get_grapheme_bounds); ClassDB::bind_method(D_METHOD("shaped_text_next_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_next_grapheme_pos); ClassDB::bind_method(D_METHOD("shaped_text_prev_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_prev_grapheme_pos); @@ -1120,6 +1121,27 @@ int TextServer::shaped_text_hit_test_position(RID p_shaped, real_t p_coords) con return 0; } +Vector2 TextServer::shaped_text_get_grapheme_bounds(RID p_shaped, int p_pos) const { + int v_size = shaped_text_get_glyph_count(p_shaped); + const Glyph *glyphs = shaped_text_get_glyphs(p_shaped); + + real_t off = 0.0f; + for (int i = 0; i < v_size; i++) { + if ((glyphs[i].count > 0) && ((glyphs[i].index != 0) || ((glyphs[i].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE))) { + if (glyphs[i].start <= p_pos && glyphs[i].end >= p_pos) { + real_t advance = 0.f; + for (int j = 0; j < glyphs[i].count; j++) { + advance += glyphs[i + j].advance; + } + return Vector2(off, off + advance); + } + } + off += glyphs[i].advance * glyphs[i].repeat; + } + + return Vector2(); +} + int TextServer::shaped_text_next_grapheme_pos(RID p_shaped, int p_pos) const { int v_size = shaped_text_get_glyph_count(p_shaped); const Glyph *glyphs = shaped_text_get_glyphs(p_shaped); diff --git a/servers/text_server.h b/servers/text_server.h index 4f55f881e647..5c994feaaee0 100644 --- a/servers/text_server.h +++ b/servers/text_server.h @@ -438,6 +438,7 @@ public: virtual int shaped_text_hit_test_grapheme(RID p_shaped, float p_coords) const; // Return grapheme index. virtual int shaped_text_hit_test_position(RID p_shaped, float p_coords) const; // Return caret/selection position. + virtual Vector2 shaped_text_get_grapheme_bounds(RID p_shaped, int p_pos) const; virtual int shaped_text_next_grapheme_pos(RID p_shaped, int p_pos) const; virtual int shaped_text_prev_grapheme_pos(RID p_shaped, int p_pos) const;